ty19880929/DeepTrade · CHANGELOG.md
更新日志
构建期从主仓 CHANGELOG.md 自动同步;网络异常时退化到仓内基线。
All notable changes to DeepTrade. Format follows Keep a Changelog and SemVer.
[v0.16.0] — 2026-05-30 — 框架层配置导入 / 导出
新增 deeptrade config export / deeptrade config import,用于备份、迁移、审计和复制框架层配置。范围严格限定为 ConfigService 管理的框架配置:app_config 中的非敏感键,以及 secret_store 中由 is_secret_key() 路由的框架 secret;不导出插件安装、插件私有配置、插件业务表、运行记录、LLM 调用日志、Tushare 同步状态或报告文件。
默认导出持久化配置且不写出 secret 明文,只记录 secret 的存在状态。显式使用 --include-secrets 时才会把 token / API key 明文写入文件;--effective 可导出当前进程解析后的有效值(包含默认值和环境变量覆盖),--include-defaults 可在持久化导出中补齐默认值。
Added
- 服务层
deeptrade.core.config_io:ConfigExportOptions/ConfigImportOptions/ConfigImportPlanConfigIOService.export_payload()/plan_import()/apply_import_plan()- 文件格式
schema_version=1,顶层包含tool="deeptrade"、exported_at、mode、config、secrets。
- CLI:
deeptrade config export [PATH] [--format json|yaml] [--include-secrets] [--effective] [--include-defaults] [--force]deeptrade config import PATH [--format json|yaml] [--mode merge|replace] [--include-secrets] [--dry-run] [--yes]PATH=-导出到 stdout 时只输出 payload,摘要走 stderr,便于脚本管道消费。
- 导入安全语义:
merge默认只覆盖文件中出现的配置项;缺失项保持本地值。replace --yes会删除所有框架层持久化配置和框架层 secret,再写入文件内容。- 默认忽略文件中的 secret value;只有
--include-secrets且 value 为非空字符串时才写入。 - 拒绝 masked secret(如
********1234)导入。 - 校验
tool/schema_version/ key 命名空间 / Pydantic 配置值 / LLM provider 名称及环境变量归一化冲突。
- 测试:
- 核心单元测试覆盖默认不泄露 secret、显式导出 secret、persisted/effective 差异、merge/replace、dry-run 计划、secret 导入开关、非法 key 和 provider 名称冲突。
- CLI 测试覆盖文件导出、已存在文件保护、stdout JSON、dry-run 不写入、导入 secret 后仍脱敏展示、replace 必须显式
--yes。
Notes
- 非敏感配置写入仍复用
ConfigService.set()/delete(),保持 Pydantic 校验和现有路由。 - Secret 写入仍复用
ConfigService.set(secret_key, value),保持 keyring 优先和 plaintext fallback 行为;跨 keyring 与 DuckDB 的事务一致性不做额外承诺。
[v0.15.2] — 2026-05-27 — set-llm 新增 OpenRouter base_url 预设
给交互式 deeptrade config set-llm 的 base_url 预设表(cli_config.py::_DEFAULT_BASE_URLS)补一条 OpenRouter。OpenRouter 是标准 OpenAI 兼容端点,本来就能用——base_url 不命中 _TRANSPORT_BY_BASE_URL 路由表,自动落到 GenericOpenAITransport(thinking 标志按契约静默丢弃)。这次纯属录入便利:provider 名取 openrouter(或 openrouter-xxx,name.split("-")[0] 仍命中)时自动带出 base_url,省去手填。无新增能力、无 transport 代码、不动配置 schema。
Added
_DEFAULT_BASE_URLS增加"openrouter": "https://openrouter.ai/api/v1"。_select_transport_class对该 base_url →GenericOpenAITransport的路由早有测试覆盖(tests/core/test_llm_client.py),本次仅补预设,行为不变。
Notes
- OpenRouter 模型 ID 带命名空间(如
deepseek/deepseek-r1、anthropic/claude-opus-4、openai/gpt-5),录入"模型名称"时填全名,不是裸名。 - 框架强制
response_format={"type":"json_object"};OpenRouter 上并非所有模型都支持 JSON 模式,请选标注支持 structured/JSON output 的模型,否则complete_json会频繁触发修复重试或抛LLMValidationError。 - OpenRouter 的可选归因头(
HTTP-Referer/X-Title)框架不发送,仅影响排行榜署名,不影响调用。
[v0.15.1] — 2026-05-27 — 上限 typer<0.26(vendored click 导致 CLI 整树构建失败)
修复一个安装期回归:deeptrade 任意命令启动即崩。Typer 0.26.0 把 click 硬 fork 成自带的 vendored 副本(typer/_click/),并从依赖里删掉了对独立 click 的声明。本仓库的 CLI 设计同时用了两边:cli.py::_DeepTradeGroup 继承 typer.core.TyperGroup(>=0.26 走 vendored click),而 db_cmd(ctx: click.Context)、插件 pass-through 的裸 @click.command 与 @click.pass_context 用的是独立 click。两个 click 是两个不同的类,Typer 构建命令树时用 lenient_issubclass(param.annotation, typer._click.Context) 识别 context 形参,独立的 click.core.Context 识别不出来 → 当成普通 CLI 参数 → RuntimeError: Type not yet supported: <class 'click.core.Context'>。这会让整棵命令树构建失败,任何子命令(用户实测 plugin upgrade)都进不去。
dev/CI 没暴露是因为 uv.lock 锁的是 typer 0.25.0(最后一个未 vendor、仍 click>=8.2.1 的版本);只有 pipx install 这类按 typer>=0.12 重新解析的环境会拉到 0.26.2。
Fixed
pyproject.toml把typer>=0.12收紧为typer>=0.12,<0.26。 恢复"Typer 与本仓库共用同一个 click"的不变量。这同时堵住一个潜伏的运行期 bug:插件分发(deeptrade <plugin_id> …)从 Typer group 里返回的是独立 click 构造的命令,在 vendored-click Typer 下同样会行为异常。- 已有 pipx 安装的临时自救(无需等发布):
pipx runpip deeptrade-quant install "typer<0.26"把 venv 内的 typer 降回 0.25.x 即可恢复。
[v0.15.0] — 2026-05-25 — yaml cache_overrides 自动注入(消除"声明僵尸文本")
修复一个跨插件的契约 bug:插件在 deeptrade_plugin.yaml::permissions.tushare_apis.cache_overrides 里声明了某个 Tushare API 的 cache class(例如 cyq_perf: trade_day_mutable),但 yaml 只参与安装期校验,运行时并不会被框架自动注入到 TushareClient。需要插件作者在调用 make_tushare_client(cache_overrides=...) 时显式传一遍同样的 dict —— 一旦插件绕开 helper、直接 TushareClient(...)(出于自定义 transport / event_cb / rps 的需要),yaml 声明就完全变成"僵尸文本",运行时仍会打 Tushare API 'cyq_perf' has no explicit cache class; defaulting to trade_day_immutable 的 INFO 警告,且实际使用的也是默认 trade_day_immutable 而非声明值。已知 limit-up-board / accumulation-probe-washout / checkmate 三个插件都中招。
v0.15 把"声明即生效"做实:dispatch 时框架把 PluginMetadata 灌进 PluginContext,make_tushare_client 自动合并 yaml 声明的 overrides,并新开 declared_cache_overrides() 公开方法供裸构造 TushareClient 的插件转发。完全向后兼容 —— 现有显式传参的插件不受影响,老的 dispatch 行为保持。
Added
PluginContext.metadata: PluginMetadata | None—— 在cli.py::_dispatch(api_version=2 dispatch 时)和plugin_manager._build_validate_ctx(validate_static时)两处构造点注入。框架自检 context(__framework__)保持None,所有读路径要做 None 守护。PluginContext.declared_cache_overrides() -> dict[str, CacheClass]—— 公开 API:返回 yaml 声明的 cache_overrides 副本,无 metadata 时返回{}。给裸构造TushareClient的插件用作转发出口:
返回值是 dict 副本,调用方修改不会污染缓存的TushareClient(db, transport, plugin_id=..., cache_overrides=ctx.declared_cache_overrides())PluginMetadata。make_tushare_client自动合并 yaml + 新增transport/rps入参:- yaml 声明的
cache_overrides永远 merge 进 client;调用方显式传的cache_overrides=per-key 覆盖 yaml(与_resolve_cache_class的 caller-wins 优先级语义一致)。 - 新入参
transport: TushareTransport | None = None—— 传入时跳过tushare.token配置检查,可直接注入 fixture / sandbox transport;不传保留默认TushareSDKTransport。 - 新入参
rps: float | None = None—— 覆盖app_config.tushare_rps;不传保留原值。 - 这俩入参补全后,原本要绕开 helper 的三个刚需场景(自定义 transport / 自定义 rps / 自定义 event_cb)全部可以回到 helper 路径,yaml 自动注入随之自动生效。
- yaml 声明的
- 测试
tests/plugins_api/test_cache_overrides_plumbing.py(11 例)锁住:declared_cache_overrides()三例(返回 yaml dict / 无 metadata 返回{}/ 返回副本不污染 metadata);make_tushare_client合并行为四例(自动注入 / caller 显式 per-key 覆盖 / 无 metadata 仅用显式 / 全空就是空 dict);INFO 日志抑制一例(用户报告的具体回归);transport / rps escape hatch 三例(带 transport 跳过 token 校验 / rps override / 默认路径仍要求 token)。
Implementation notes
- caller-wins per-key 而非整体替换:merge 语义是
{**declared, **explicit},与_resolve_cache_class的优先级链(caller > framework table > default)一致。yaml 声明了{cyq_perf: mutable, moneyflow_dc: mutable}、调用方显式传{cyq_perf: static},最终 client 拿到{cyq_perf: static, moneyflow_dc: mutable}。任何老代码已经显式cache_overrides=的不会丢字段。 PluginContext.metadata只是个可选 dataclass 字段:现有api_version=1路径根本不构造PluginContext,零影响;现有api_version=2插件因为metadata默认None也不感知。整个 v0.15 的行为变化只发生在调用方没传cache_overrides=时——原来得到空 dict,现在得到 yaml 的 dict,这是修复目标本身。transport=/rps=入参故意叠加在显式cache_overrides=之上:保证三个插件可以从TushareClient(rt.db, transport, plugin_id=..., rps=..., event_cb=...)直接平移到ctx.make_tushare_client(transport=..., rps=..., event_cb=...),把 yaml 转发由"插件自己 forward dict"退化为"啥也不用做,helper 包办"。
Migration notes
- 插件作者:建议优先把裸构造
TushareClient(...)改成ctx.make_tushare_client(transport=..., rps=..., event_cb=...),yaml 自动生效,不需要再 forwardcache_overrides。若刚需保持裸构造(少见),改用ctx.declared_cache_overrides()转发即可。不升级也不会出错,只是 INFO 警告会继续显示。 - 现有部署:零迁移。新行为只在调用
make_tushare_client()时生效,不动 DB schema、不改配置键、不改 CLI 表面。
[v0.14.0] — 2026-05-25 — 同花顺行业 / 概念 / 地域板块(框架级参考数据)
往框架层补齐一类此前一直绕过的能力:跨插件共享的参考数据。第一类落地的就是同花顺行业 / 概念 / 地域板块(ths_index + ths_member)——多个策略都会用到的"某只股票属于哪些概念"的反查,过去要么每个插件自己再造一遍 sync,要么靠手工导入。现在框架提供 deeptrade data sync-concepts 一键拉取(一次性同步两张表),并通过 PluginContext.make_concept_repository() 把只读查询接口开放给所有插件。
这是对 CLAUDE.md "no tushare-derived business tables" 的有限松绑:仅限于"低基数、跨插件、无业务归属"的参考数据。daily / daily_basic / limit_list_* 这类带策略形状的业务表仍归插件所有,不会回流到框架。
完全向后兼容:纯新增表 + 新增 CLI 子命令 + 新增 plugins_api 暴露,没有改任何现有数据契约 / 配置键 / 命令表面。
Added
- 两张框架表(migration
20260601_002_ths_concepts.sql):ths_concept_board(ts_code PK, name, count, exchange, list_date, type, synced_at)ths_concept_member(board_ts_code, stock_ts_code, stock_name, weight, in_date, out_date, is_new, synced_at, PRIMARY KEY(board_ts_code, stock_ts_code))+idx_ths_concept_member_stock索引覆盖个股反查。
- CLI
deeptrade data sync-concepts [--type N,I,R] [--quiet]:- 调用 Tushare
ths_index(按type循环:默认 N=概念 / I=行业 / R=地域,可选 S=同花顺特色)+ths_member(每个板块一次),全量覆盖写回两张表,整体落在单个 DuckDB 事务里。 - 默认在 stderr 打"
同步成份股 idx/total (board_code)"进度行,节流 0.3s;--quiet关闭。 - 同步链路走
plugin_id = __framework__,tushare_calls/tushare_cache_blob与任何插件互不干扰。
- 调用 Tushare
- plugins_api 新增
PluginContext.make_concept_repository()→ConceptRepository,其上方法:boards_by_stock(stock_ts_code, *, type=None)—— 主诉求:返回某股票当前所属的所有板块;可选 type 过滤。get_board(board_ts_code)/list_boards(*, type=None)/members_of(board_ts_code)/counts()—— 互补的反向 / 全表 / 聚合查询。ConceptBoard/ConceptMember两个 frozen dataclass 与ConceptRepository一并通过from deeptrade.plugins_api import ...导入。
- TushareClient.API_CACHE_CLASS 注册
ths_index/ths_member为"static",抑制"unknown api"的 INFO 日志。 - 测试
tests/core/test_ths_concepts.py(8 例)覆盖:全量覆盖原子性 / type 过滤 / 单板块失败不致命 / 空类型列表报错 / Repository 在空快照下不抛错 /boards_by_stock反查 + type 过滤 / 字段别名code与con_code、name与con_name在同一次 sync 中混用的归一化。
Implementation notes
- pandas 仅在 sync 路径内 lazy import:
deeptrade/core/ths_concepts.py顶部不导入 pandas,ConceptRepository与ConceptBoard / ConceptMember都是 stdlib + DuckDB only。这是为了保住"框架 wheel 不依赖 pandas"的硬约束——pandas 是plugin-runtimeextras,只有真正要做 sync 的环境(CLI 主流程)才需要。from deeptrade.plugins_api import ConceptRepository不会把 pandas 拽进任何只读路径。 - 字段归一化点放在 per-board 阶段:Tushare
ths_member在不同时期 / 不同账号上把"个股代码"叫code或con_code、"个股名称"叫name或con_name。早期实现尝试在 concat 之后用df.rename(columns={"code": "stock_ts_code", "con_code": "stock_ts_code"})一把搞定,结果两个别名同时存在时会被映成两个同名列,下游 projection 拿不到任何一行。现在_normalize_member_columns在 concat 之前对每个板块的 frame 单独折叠con_code → stock_ts_code → code、con_name → stock_name → name三级优先级,保证 concat 看到的永远是已经收敛过的 canonical schema。 - 单板块失败只 warn,不中止整次 sync:
ths_member一次拉一个板块,板块数量上千;某个板块的偶发 5xx / 权限缺失被吞掉只输出 warning + 推进下一个,避免任何一颗"坏苹果"作废整轮快照。tushare_calls审计行仍会落(由 TushareClient 自己处理),运维可以事后翻日志补拉。 - 全量覆盖语义:每次 sync 先
DELETE FROM ths_concept_member→DELETE FROM ths_concept_board再批量 INSERT,整段在一个 transaction 里——避免出现"读到新 board 但 join 不到 member"的中间态。增量 / 分批 UPSERT 的语义放到未来真有需要时再做。 - CLAUDE.md 同步更新:新增 "Reference data exception" 段落,说明这条豁免的边界(共享、低基数、无业务归属),并写明
daily / limit_list_* / daily_basic等仍属插件。后续要往框架里加新表的人需要先过这条 bar。
Migration notes
- 现有插件零改动:新表是纯增量,原有表 / 列 / 索引一字未动。
deeptrade data sync占位文案改为同时指引"<plugin_id> sync ..."和"data sync-concepts"两条路径;任何调用旧data sync的脚本会得到 exit 2 + 引导文本,行为与 v0.13.x 一致。- 升级路径就是一次普通的
apply_core_migrations:新加的20260601_002在第一次deeptrade <任何命令>时自动建表,无人工动作。
[v0.13.1] — 2026-05-24 — 修复 core 迁移在已填充 llm_calls 上 COMMIT 失败
任何升级到 v0.13 / v0.12 的用户、其 DB 中 llm_calls 表已经积累了真实数据,第一次执行任何 CLI 命令时都会被 apply_core_migrations 卡住,报 TransactionException: Failed to commit: Attempting to modify table llm_calls but another transaction has altered this table。原因是 20260525_002 一次性发出 11 条 ALTER TABLE ADD COLUMN,被框架包在单个 BEGIN/COMMIT 内,触发 DuckDB 1.5.x 的提交期表变更检测 bug。Database 构造函数会无条件调用 apply_core_migrations,所以 plugin upgrade / plugin run / plugin list 全部被堵死,等于把所有已经在用 v0.12+ 框架的用户挡在升级路径外。
Fixed
apply_core_migrations不再用单事务包裹整条迁移:把 SQL 文件拆成单条语句,每条以 DuckDB 隐式自动提交方式执行;全部成功后再写schema_migrations(version)。原子性下降换来与 DuckDB 1.5.x 的兼容。ALTER TABLE ADD COLUMN失败时若是"列已存在"(CatalogException+ 消息含already...column),吞掉并ROLLBACK清掉隐式事务的 aborted 状态,让后续语句继续跑——这样上一次跑到一半但没写schema_migrations的迁移在下次启动可以幂等地补完。其它 core 迁移动作要么IF (NOT) EXISTS、要么写入幂等(UPDATE ... SET = 常量),不需要额外保护。- 新增
_split_sql_statements辅助:先剥--行注释(避免注释里;把语句切坏)再按;切。
Added (tests)
tests/core/test_db.py::test_apply_core_migrations_on_populated_llm_calls—— 应用 pre-v0.12 迁移、向llm_calls灌 50 条记录,再触发 auto-migrate,断言20260525_002 / _003落地、11 个新列全部就位、历史数据未丢。锁住根因。tests/core/test_db.py::test_apply_core_migrations_idempotent_on_partial_add_column—— 手工预添两个列后不写schema_migrations,模拟中途崩溃;再次 auto-migrate 必须无错完成,覆盖"列已存在"幂等路径。
Notes
- 完全向后兼容;纯框架级修复,未触及任何插件契约、配置键、CLI 表面、迁移版本号。
- 若已经被这个 bug 卡住,可临时
DEEPTRADE_SKIP_AUTO_MIGRATE=1 deeptrade ...绕开做 DB 修复,但更推荐直接升级到 v0.13.1。
[v0.13.0] — 2026-05-24 — 跨插件 run 诊断 CLI
补齐 v0.12 复现性基础设施的命令面:deeptrade run compare / run show 让运维 / 调试者跨 run 看 4 段 diff(基本字段 / 输入指纹 / LLM 调用 / artifact 摘要),deeptrade db llm-cache list / purge / inspect 提供缓存的横向观测和过滤维护。v0.12 只交付了 API 层基础设施;本版本是把这些数据“拉出来看”的 UX 收口。
完全向后兼容:纯新增 CLI 表面,未触及任何运行时调用路径与数据契约;现有插件、配置键、迁移版本、complete_json 入口与 v0.12 完全一致。
Added
deeptrade run compare <run_a> <run_b> [--plugin <id>] [--format human|json]—— 把两个 run 的框架元数据(run_metadata/run_artifacts/llm_calls)配对成 4 段 diff:- basic:
plugin_id/key_date不一致就标✗。created_at故意不参与对比(两个不同 run 必然不同时执行,这一列纯噪声)。 - input fingerprint:
input_fingerprint不同时直接报;同时存在input_payload_json时会算 top-level key diff,给出{key: {a, b}}的精细变化。 - llm calls:按
(stage, model)配对,比prompt_hash/final_prompt_hash/validation_status三项;任一项不一致 match=False。这意味着 A 真实调用 + B cache 命中(同 prompt,validation_status="ok" vs "cached")会被识别为状态不同,便于排查"是不是哪边漏走了 cache 路径"。 - artifacts:按
name配对,只比sha256;不同即 ✗。 --format json给出机器可读输出,便于 CI 与脚本消费。
- basic:
deeptrade run show <run_id> [--plugin <id>] [--format human|json]—— 单 run 视图:基本字段 + 所有 LLM 调用表 + artifacts 表。deeptrade db llm-cache list [--plugin <id>] [--stage <s>] [--limit <N>] [--offset <N>] [--format human|json]—— 浏览缓存条目,按created_at DESC排。deeptrade db llm-cache purge [--plugin <id>] [--stage <s>] [--before <Nd>] --yes—— 删除缓存行;必须 给至少一个过滤器、必须 显式--yes。无过滤器时直接退出 2 + 中文报错,把ReplayCacheStore.purge()的硬约束在 UX 层翻译给用户。deeptrade db llm-cache inspect <cache_key> [--full] [--format human|json]—— 单条 cache 详情;cache_key支持唯一前缀解析(12 字符通常够用),歧义时退出 2 并列出所有候选。response_json默认截断到 200 字符 / list 截断到 10 元素,--full展开。RESERVED_PLUGIN_IDS新增"run":现在拒绝plugin_id == "run"的插件 install,避免 plugin 命名空间与新顶层命令撞车。tests/cli/test_run_cmd.py(9 用例) /tests/cli/test_db_llm_cache.py(13 用例) —— 覆盖 4 段 diff、JSON 格式、跨插件歧义处理、purge双闸(过滤器 +--yes)、inspect前缀唯一性、长 response 截断。共 +22 测试通过,0 回归。
Changed
deeptrade/cli.py注册两个新 typer 子组:run挂在顶层,db llm-cache挂在既有db_app下;保持db init/db upgrade既有行为不变。tests/core/test_plugin_install.py::test_reserved_words_are_those_documented同步加"run"到期望集合。
Migration notes
- 现有插件零改动。
run现在是保留 plugin_id:如果你的某个未发布插件碰巧叫run,重命名(plugin install 会在RESERVED_PLUGIN_IDS检查时报错)。 - 跨插件
run compare/run show只能看到走过 v0.12PluginContext.set_run_*/add_run_artifact写入的 run;老版本运行的 run(没在run_metadata表里留行)查不到。这是预期行为——run_metadata表本就是 v0.12 才引入的横向视图。 - 没有数据库迁移:本版本不动 schema,只读 v0.12 已有的三张表(
run_metadata/run_artifacts/llm_calls)和 cache 表(llm_replay_cache)。
[v0.12.0] — 2026-05-24 — 跨插件 LLM 复现性基础设施
把"同输入、同 profile、同 schema 的 LLM 调用第二次执行直接命中已验证响应"做成框架级能力,让所有插件无需各自造轮子即可获得 reproducible run。配套地,沉淀一套稳定 fingerprint 工具、跨插件 run 元数据 API 与稳定多 worker 并发原语——分别覆盖《limit-up-board 插件连续执行结果不一致问题的修复方案》§5 列出的全部框架层条目。
完全向后兼容:现有 api_version=1/2 插件零改动可继续运行,因为 LLMClient.complete_json(replay=None) 的默认行为是 不读不写缓存,行为与 v0.12 前完全一致;只有显式传入 replay=LLMReplayPolicy() 并提供 stage / schema_version 的调用才进入新缓存路径。
Added
- Fingerprint 工具 ——
deeptrade.core.fingerprint:canonical_json/hash_json/hash_text/hash_dataframe/hash_file。严格规则(NaN/Inf / set / generator 拒绝;Decimal 保留字面量;datetime 时区不静默改写;DataFrame 强制sort_by;pd.NaT显式拒绝)作为冻结契约——任何规则调整必须 bumpFRAMEWORK_LLM_CACHE_VERSION。 - LLM Replay Cache ——
deeptrade.core.llm_replay:LLMReplayPolicy(disabled / fresh / replay_only_helper)、LLMReplayMiss、ReplayCacheStore(make_cache_key / lookup / store / purge / iter_entries)。cache_key = sha256(canonical_json({plugin_id, stage, provider, model, system_sha256, user_sha256, profile, schema_name, schema_version, input_fingerprint, framework cache version}))。 LLMClient.complete_json重放接入 —— 新增四个可选关键字参数replay / stage / schema_version / input_fingerprint。三分支编排:cache 命中 → 不调 transport,写validation_status='cached'审计行;replay_only缺失 → 写validation_status='replay_miss'审计行后 raiseLLMReplayMiss;其余 → 真实调用 + repair retry 后写缓存(缓存 final prompt / response,而非首次失败那一次)。meta 新增cache_hit / cache_key / source_run_id / source_call_id / attempt_count / final_prompt_hash / first_error_class / repair_hint_hash / call_id字段;命中行 meta 含tokens_from_cache=true,不冒充本次消耗。新增异常LLMConfigError:replay 激活但缺stage/schema_version/provider立即报错而非静默缓存错误的 key。- PluginContext run 元数据 API —— 五个新方法
set_run_key_date(run_id, key_date)/set_run_input_fingerprint(run_id, fingerprint, payload=None)/add_run_artifact(run_id, *, name, path, sha256=None)/get_run_metadata(run_id)/list_run_artifacts(run_id),写入框架表run_metadata/run_artifacts。匿名 context(plugin_id=None)任一方法 raiseRuntimeError。所有写方法 UPSERT 幂等;两个set_*单列独立更新互不擦写。add_run_artifact在sha256=None时自动通过hash_file计算,外部 URL 路径下size_bytes兜底为 None。返回类型RunMetadata/RunArtifact(@dataclass(frozen=True))从plugins_api导出。 - 并发 helper ——
deeptrade.core.parallel.run_parallel_ordered(providers, worker_fn, *, max_workers, on_complete, timeout):返回 list 按 输入顺序 重组,on_complete按 完成顺序 触发;任一 worker 异常封装为ProviderWorkResult(ok=False, error=...)不中断其他 worker;timeout触发后未完成 slot 标FutureTimeoutError并尽力 cancel 未启动 future(已运行线程无法强中断,docstring 已明示)。LLM-agnostic:任意字符串身份的 worker 都可用。 llm.replay.*配置 key ——AppConfig.llm_replay_enabled(默认 True) /llm_replay_write(默认 True) /llm_replay_ttl_days(默认 None),对应配置键llm.replay.{enabled,write,ttl_days};三键全部支持DEEPTRADE_LLM_REPLAY_*env 覆盖,bool 接受1/true/yes/on/0/false/no/off/empty,ttl_days 接受null/none/empty转 None。新助手policy_from_app_config(app_config) -> LLMReplayPolicy:包装"write必须在enabled之下"的规则,并刻意 不 编码replay_only(那是 per-invocation 决定,仅由DEEPTRADE_REPLAY_ONLYenv 或LLMReplayPolicy.replay_only_()触发)。- 环境变量复现性开关 ——
policy_from_env(base=None)助手:DEEPTRADE_REPLAY_ONLY > DEEPTRADE_NO_LLM_REPLAY > DEEPTRADE_FRESH_LLM优先级覆盖任意 base policy。框架的 plugin dispatch 路径用ignore_unknown_options=True,无法拦截 plugin argv;env 是统一全局开关的实现路径。 - 三张框架表 ——
llm_replay_cache(迁移20260525_001_llm_replay_cache.sql,PRIMARY KEYcache_key,索引(plugin_id, stage, provider, model, prompt_hash)/created_at)。run_metadata(迁移20260525_003_run_metadata.sql,PRIMARY KEY(plugin_id, run_id),索引(plugin_id, key_date))。run_artifacts(同迁移内,序列run_artifacts_id_seq+ UNIQUE(plugin_id, run_id, name),索引(plugin_id, run_id))。
llm_calls审计列扩展 —— 迁移20260525_002_extend_llm_calls.sql新增 11 列cache_hit / cache_key / source_run_id / source_call_id / stage / schema_version / input_fingerprint / attempt_count / final_prompt_hash / repair_hint_hash / first_error_class。validation_status在应用层新增cached / replay_miss取值。llm_calls.jsonl同步携带这些字段 +tokens_from_cache标志。- 测试 —— 新增 6 个测试文件覆盖:fingerprint 规则 + 跨进程稳定性(30)/ llm_replay 数据层 + policy + env(33)/ LLMClient replay 端到端(18 含 ExplodingTransport 验证 cache hit 绝不触达 transport)/ PluginContext run 元数据(26 含跨插件隔离 + 同名 artifact 跨 run 并存)/ parallel helper(14 含 callback 异常不中断 + timeout 部分超时)/ config replay keys 与
policy_from_app_config(32)。合计 +153 测试通过,0 回归。
Changed
LLMClient构造新增可选参数provider: str | None = None;LLMManager.get_client自动透传name作为 provider。直接构造LLMClient不走 replay 路径的旧用法零影响。LLMClient._with_retry重构以贯通 v0.12 audit 字段(attempt_count/first_error_class/final_prompt_hash/repair_hint_hash/call_id);retry 行也带这些字段,便于 audit 关联同一对话的两次 attempt。_record_call/_append_jsonl形参扩展同步落 DB + JSONL。_record_call移除内部uuid.uuid4()默认call_id,改为参数传入,便于 cache hit / replay miss 路径用预生成的 UUID 贯通 meta。PluginContext增加内部缓存字段_run_metadata_store与_run_store()helper,与既有_llm_manager缓存模式一致。plugins_api/llm.py重新导出LLMReplayMiss / LLMReplayPolicy / policy_from_env / policy_from_app_config;plugins_api/__init__.py同步加入__all__。tests/core/test_db.py同步更新框架表清单(llm_replay_cache / run_metadata / run_artifacts)与迁移版本断言。
Migration notes
- 完全向后兼容:插件
complete_json(...)不传replay时行为与 v0.11 完全一致——不读不写缓存,不要求 stage/schema_version。 - 三张新框架表 +
llm_callsALTER 在首次打开 DB 时自动apply_core_migrations,无需手动deeptrade db upgrade。 llm_calls新列默认值 NULL/FALSE,旧行被自动回填到这些值;任何只 SELECT 老列的下游查询不变。- 推荐迁移路径:插件升
api_version不变,分两步:(1) 调ctx.set_run_key_date/ctx.set_run_input_fingerprint/ctx.add_run_artifact给跨插件run compare喂数据;(2) 在complete_json调用点显式传replay=policy_from_env(policy_from_app_config(cfg))+stage="..."+schema_version="lub-llm-schema-v1"+input_fingerprint=hash_json(...)启用缓存。schema_version是插件自己定义的语义版本,schema 字段或 prompt 输出契约破坏时 bump。 - 任何对 canonical_json 规则(如 datetime 编码、NaN 处理)的未来调整必须 bump
deeptrade.core.llm_replay.FRAMEWORK_LLM_CACHE_VERSION,让历史 cache 自然失效。当前值"1"。 - CLI 新顶层命令(
deeptrade run compare/deeptrade db llm-cache list/purge)在本版本 未 实现,留待 v0.13.0;本版本只交付 API 层基础设施。
[v0.11.0] — 2026-05-24 — 报告上传抬升为框架基础能力
把 limit-up-board 等策略插件各自实现的"执行报告 JSON 上传"链路抬到框架层,成为与 LLMManager / TushareClient 同级的公共服务。插件不再需要自带 uploader.py 或 multipart 实现,统一通过 PluginContext.make_report_uploader() 拿到 ReportUploader 实例;上传配置(URL / 超时 / 全局开关)落 app_config,token 走 secret_store,每次调用在新审计表 report_uploads 留一行。
完全向后兼容:现有任意 api_version=1/2 插件零改动,因为新增的全是 additive surface(新公共类型 + 新 context 工厂方法 + 新配置键 + 新审计表)。
Added
deeptrade.core.report_uploader.ReportUploader— stdlib-only multipart/form-data 上传服务;自带 enable 闸、文件存在性闸、token 注入;HTTPError / URLError / TimeoutError / JSONDecodeError 等失败统统转为UploadResult(status="failed", error_class=..., error=...),永不抛。deeptrade.core.report_uploader.UploadResult—frozendataclass:status∈{ok, skipped_disabled, skipped_missing_file, failed},附带public_url / public_path / public_index / public_date / http_status / duration_ms / error_class / error。deeptrade.core.report_uploader.UploadError— 公共异常类型,仅供调用方愿意自己 raise 时使用;ReportUploader.upload本身永不抛它。PluginContext.make_report_uploader(run_id=...)— context 工厂,与make_llm_manager/make_tushare_client同款风格;plugin_id缺省自动兜底为__framework__。deeptrade.plugins_api直接 re-exportReportUploader / UploadError / UploadResult(非 lazy,因为框架基础能力没有可选依赖)。AppConfig.report_upload_enabled(默认 False) /report_upload_url(默认https://deeptrade.tiey.ai/api/reports/upload) /report_upload_timeout(默认 30.0),对应配置键report.upload.{enabled,url,timeout};report.upload.token加入_STATIC_SECRET_KEYS路由到secret_store。三个非 secret 字段均支持DEEPTRADE_REPORT_UPLOAD_*env 覆盖。- 核心迁移
20260601_001_report_uploads.sql建审计表report_uploads(字段:upload_id / plugin_id / run_id / trade_date / plugin_name / file_path / file_bytes / status / http_status / public_url / public_path / duration_ms / error_class / error / created_at);按plugin_id+run_id对齐llm_calls/tushare_calls的审计风格。 tests/core/test_report_uploader.py— 覆盖 gate / 成功 / HTTP4xx / URLError / timeout / 非 JSON / 非 dict JSON / 空 token 无 Authorization / PluginContext 工厂 / token 不入审计 /UploadResult不可变等 15 个用例。
Changed
- README 配置章节补一行
deeptrade config set report.upload.enabled true+ 报告上传特性说明;版本徽标升至 0.11.0。 pyproject.toml::version与deeptrade/__init__.py::__version__同步至 0.11.0。
Migration notes
- 现有插件零改动即可继续工作;
limit-up-board等仍自带 uploader 的插件可在后续版本里平移到框架 API(迁移说明随插件发布给出,不在本仓追踪)。 - 旧的 plugin-local
summary_upload_*配置仍由各插件解读;框架不会越界搬运它们——迁移由插件自身的升级路径负责。 - 新 audit 表
report_uploads在首次打开 DB 时自动创建(apply_core_migrations),无需手动deeptrade db upgrade。 - 跨进程并发:
report_uploads写入复用Database既有 RLock,不引入新的锁文件。
[v0.10.0] — 2026-05-24 — 代码评审整改:可观测性 / 失败回滚 / 插件 API 边界
按《DeepTrade 代码评审报告》(2026-05-23)的三阶段路线整改高 / 中 / 低优先级问题。运行行为变化集中在 CLI 启动日志、Tushare / LLM 审计的可见性、插件 upgrade 失败语义;插件契约新增稳定 facade 方法但完全向后兼容。
Added
- H1
deeptrade/cli.py::_bootstrap_logging:CLI callback 启动时按app.log_level配置调用setup_logging(),~/.deeptrade/logs/deeptrade.log轮转日志默认生效。DB 不可用时回落到INFO。 - H2
LLMManager.get_client在run_id非空、reports_dir未传时自动派生paths.reports_dir() / run_id,llm_calls.jsonl全量审计成为框架契约的一部分(插件不再需要手工接线paths.reports_dir())。 - H3
tushare_calls新增status / error_type / error_message / attempt_no / run_id / cache_action列(迁移20260524_001_extend_tushare_calls.sql),覆盖 429 / 5xx / transport / unauthorized 等失败路径与 fallback 命中;TushareClient(..., run_id=...)把 run_id 写入审计。 - H4
PluginContext.make_llm_manager()/PluginContext.make_tushare_client(...)—— api_version=2 插件不需要再from deeptrade.core.* import ...,直接通过 context 工厂拿到LLMManager/TushareClient。 - M4
deeptrade/core/process_lock.py:基于fcntl.flock/msvcrt.locking的进程级锁;PluginManager.install / upgrade / uninstall全程持锁,防止并发安装互踩。 - M5
tests/test_version_consistency.py:把pyproject.toml/__init__.py/ README badge 三处版本号对齐做成 CI 守门。 - M6 dep snapshot 新增
post-install阶段 +diff_dep_snapshots(pre, post):插件安装结束后同步记录后置快照,日志输出 added / changed / removed 计数,per-plugin venv 上线前的最小可观测兜底。 - 低优
RegistryEntry.sha256可选字段;fetch_tarball(..., expected_sha256=...)下载后、解压前校验摘要,不匹配抛TarballChecksumError。 - 低优
LLMClient._retry_hint_for(error, schema):Pydantic 重试提示按schema.model_fields动态枚举必填字段名,不再硬编码ts_code / score / strength_level等策略私有字段。
Changed
- M1
PluginManager.upgrade失败回滚现在快照 + 恢复完整三件套:plugins行(含type)、plugin_tables行、plugin_schema_migrations行。此前只回滚主行,会留下「旧版本 metadata + 新版本迁移登记」的不一致状态。前向 DDL 仍刻意保留(无 rollback DDL 模型)。 - M2
SecretStore.get / set / delete包裹 keyring 运行期异常:读失败 → 记 ERROR + 返回 None;写失败 → 降级到 plaintext-in-DuckDB 并 WARN;delete 失败 → WARN 后仍删 DB 行。此前 backend 存在但 unlock 失败的环境会让 raw 异常冒到 CLI。 - M3
pyproject.toml::devextras 加build>=1.2,让 AGENTS.md 里写明的uv run python -m build可复现。 - 低优
RegistryClient._write_cache改成 tmp 文件 +os.replace原子写,避免 Ctrl-C 留半截 JSON。
Migration notes
- 现有 api_version=1/2 插件零改动。新加的
PluginContext.make_*是可选 helper,老代码继续走LLMManager(ctx.db, ctx.config)/ 手搓TushareClient完全可用。 tushare_calls是 ADD COLUMN,旧行被回填为status='ok', attempt_no=1, cache_action='fresh';任何 SELECT 既有列的下游查询不变。- 进程锁实例化在
PluginManager.install/upgrade/uninstall入口;如果你的环境同时跑两个 CLI(CI 矩阵 / 多 shell),第二个会等最多 30 s,然后抛ProcessLockTimeoutError。锁文件位置~/.deeptrade/writers.lock。 - README badge 同步升到
0.10.0,与pyproject.toml/__init__.py一致;版本一致性测试已加。
[v0.9.1] — 2026-05-16 — set-llm 默认值表新增 OpenAI 官方端点
交互式 deeptrade config set-llm 在用户敲入 provider 名称后会用首段(split-by-dash 第一段)去 _DEFAULT_BASE_URLS 查一个默认 base URL 预填到下一步表单。此前该表只覆盖 deepseek / qwen / kimi / doubao 四家国内供应商,命名 openai 时默认值为空字符串,用户需要自己记忆 / 粘贴 https://api.openai.com/v1 才能继续。
底层 OpenAIOfficialTransport 早已存在并通过 _TRANSPORT_BY_BASE_URL 上的 api.openai.com substring 命中(v0.6 起),是唯一会把 StageProfile.reasoning_effort 真正写到 wire 的 transport(o1 / o3 reasoning 系列依赖)。这一版纯粹是补齐 wizard 的可发现性,运行时无任何行为差异。
Changed
deeptrade/cli_config.py::_DEFAULT_BASE_URLS新增"openai": "https://api.openai.com/v1"。命名为openai/openai-*(如openai-o3)的 provider 会自动预填该 base URL。_set_llm_new的 provider 名称提示串补上openai作为示例。
Migration notes
- 已有的 OpenAI provider 配置(如果用户在更早版本手动建过)不受影响——
_DEFAULT_BASE_URLS仅在新增时作为表单默认值使用,存量记录不会被回写。 - transport 路由、
reasoning_effort转发逻辑、llm_calls审计字段全部不变。
[v0.9.0] — 2026-05-16 — LLM transport 改流式,规避网关 idle-timeout
打板等长生成场景下,kimi-k2.6 等 thinking 模型的非流式调用 100% 触发 LLMTransportError: Request timed out.,单次失败耗时 ~27 分钟(外层 tenacity 3 次 × openai SDK 3 次 × 180 s)。Moonshot 官方文档明确:思考模型在 server 端先思考再生成,任何中间网关(包括 Moonshot 自家网关)只要看到长时间无 header 返回就会把 TCP 当僵尸连接切掉,这是非流式的设计性缺陷,与 base_url / DNS / TLS 均无关。
实证:同一 prompt + stream=True 下 TTFC 1.3 s、总耗时 42 s、content_len=240、JSON 合法、usage 完整。
Changed
deeptrade/core/llm_client.py::OpenAICompatTransport.chat()改为流式:stream=True+stream_options={"include_usage": True},逐 chunk 累积delta.content拼成完整文本,从末 chunk 取 usage。create()与迭代两阶段的APITimeoutError/APIError都包成LLMTransportError,原 tenacity 重试路径不变。- 保留 v0.6 / v0.8.1 的两段逻辑:
_adjust_temperature()钩子(Moonshot 强制 1.0)与supports_reasoning_effort门控(仅OpenAIOfficialTransport翻 True)继续生效。 - 子类(
GenericOpenAITransport/DashScopeTransport/MoonshotTransport/OpenAIOfficialTransport)零修改,继承新基类实现。RecordedTransport、LLMClient、LLMResponse、llm_calls表结构、reports/<run>/llm_calls.jsonl格式全部不变。
Why streaming-only, no fallback
内测期约定:不留 stream=False 开关、不加配置项、不做降级。理由:
- timeout 语义自然从「整次调用墙钟」变为「单 chunk 间最大不活跃间隔」,更宽容、更符合 LLM 长生成的实际形态;
provider.timeout=180字面值无需调整。 - 流式中途断 → 截断 JSON → 走
LLMClient._with_retry已有的LLMValidationError1 次 repair-retry,不必单独走非流式 fallback。 - include_usage 是 OpenAI 协议 2024-07 起的官方约定,in-scope 的全部 provider(OpenAI / Moonshot / DeepSeek / DashScope / Doubao / GLM / Yi / OpenRouter / SiliconFlow)均已支持;万一某末 chunk 缺 usage,审计记 0 不抛。
Migration notes
- 插件零改动。
LLMClient.complete_json()接口、异常类型、返回类型、重试语义、审计格式全部不变。 app_config.llm_providers[*].timeout字段保留,语义如上;用户原本配的数值无需调整。- 行为差异详见
docs/fix/2026-05-16-llm-streaming-transport.md§4.3。
[v0.8.1] — 2026-05-16 — Moonshot reasoning 模型 temperature 兼容性
limit-up-board 等插件接入 Kimi K2.6(base_url = https://api.moonshot.cn/v1)后,所有 LLM 调用 100% 命中 HTTP 400 invalid temperature: only 1 is allowed for this model。根因:Kimi K2 系列的 thinking / reasoning 变体(与 OpenAI o1/o3、Anthropic Sonnet thinking 同侧设计)在服务端硬约束 temperature——仅接受模型专属的固定值,而插件 StageProfile 出于复现性给的是 0.0 ~ 0.2。
修复职责完全在框架:插件不应感知具体 provider/model 的服务端约束,框架的契约是「插件给一个温度意图,框架在真正发出请求前 sanitize 到目标 provider/model 能接受的取值」。
Changed
deeptrade/core/llm_client.py::OpenAICompatTransport:新增_adjust_temperature(model, temperature) -> float钩子,默认 identity;chat()在写 kwargs 前调用钩子,并在改写时打一行logger.info便于排查。非 Moonshot 路径完全无感。- 新增
MoonshotTransport(OpenAICompatTransport):_FORCED_TEMPERATUREprefix 表强制kimi-k2-thinking/kimi-k2.5/kimi-k2.6到1.0、kimi-for-coding到0.6;fallthrough 走min(temperature, 1.0)兼顾非 reasoning 模型(moonshot-v1-*/kimi-k2-instruct-*)的[0, 1]上限——Pydantic 字段允许到 2.0,超界一样 400。 _TRANSPORT_BY_BASE_URL新增("api.moonshot.cn", MoonshotTransport)。substring 匹配自动覆盖api.moonshot.cn/api.moonshot.cn/v1所有形式;国际站api.moonshot.ai暂未支持,若后续需要追加一行即可。
Why prefix match, not exact / regex
Moonshot 命名空间 <major>.<minor>[-<dated-revision>] 的天然分界就在 prefix。exact 会让 kimi-k2.6-1106 / kimi-k2-thinking-128k 这类 dated revision 漏网,触发 0day 失败;regex 转义复杂度抬高 review 成本,收益不抵。
Migration notes
- 插件零改动。
limit_up_board/ 其他第三方插件的profiles.py不需要感知该约束。 - 用户原本在 Kimi reasoning 模型上设的
temperature=0.0在改写后会被强制为1.0——这本来就是服务端唯一允许的取值,不改写就是 100% 失败。 app.profile/llm.providers配置无变动。
[v0.8.0] — 2026-05-16 — 插件 install / upgrade 走 CDN,零 GitHub API 调用
deeptrade plugin install 与 deeptrade plugin upgrade 此前在解析"最新版本"与下载 tarball 时各打一次 api.github.com,未认证用户共享 60/h 的 IP 级配额。一旦插件用户数上来,或者用户与浏览器 / gh CLI / git clone 公共仓库共用同一公网 IP,HTTP 403: rate limit exceeded 就会把 install / upgrade 直接打死。共享 token 会违反 GitHub ToS,且配额仍会在那个 token 上聚合——不是解。
本版改造将插件分发热路径全部搬到 CDN 端点:
- Tarball 下载改走
codeload.github.com/<owner>/<repo>/tar.gz/<ref>。codeload 是 CDN-backed 静态端点,不计入 REST API 限流。 - **"最新版本"**改从注册表
index.json的新字段latest_version读取(raw.githubusercontent.com同样不限流,且已有 ETag 缓存)。 - URL 形式安装(
deeptrade plugin install https://github.com/...)无--ref时默认main分支;其他默认分支需显式--ref。
净效果:未认证用户走 install / upgrade 全流程零 API 调用,60/h 限制对默认路径不再存在。GITHUB_TOKEN 不再被框架读取,文档不再推荐设置。
Changed
deeptrade/core/github_fetch.py:删除latest_release_tag/NoMatchingReleaseError;fetch_tarball改写为 codeload 单端点(移除GITHUB_TOKEN/X-GitHub-Api-Versionheader)。deeptrade/core/registry.py:RegistryEntry新增可选字段latest_version: str | None;解析器引入_OPTIONAL_FIELDS集合,含字段时填入,缺省None。schema_version 不变(仍为 1,向后兼容旧注册表文件)。deeptrade/core/plugin_source.py:_resolve_short_name用entry.latest_version替代latest_release_tag;缺字段且无--ref时抛SourceResolveError提示需补--ref。_resolve_url无--ref默认main;codeload 失败时错误信息提示用户切换--ref。deeptrade/cli_plugin.py:移除NoMatchingReleaseError引用。
Removed
latest_release_tag公共函数与NoMatchingReleaseError异常类。- 对
GITHUB_TOKEN环境变量的读取(github_fetch.py 中)。
Migration notes
- 注册表维护者:在
DeepTradePluginOfficial/registry/index.json每个 plugin entry 中加latest_version字段,每次插件发版后同步该字段。可由 plugin 仓库 release workflow 自动 PR 到注册表仓库。 - 存量旧版框架用户:旧版(v0.7 及以下)仍会调
api.github.com。latest_version字段是可选的、旧框架读取时会被忽略,不破坏现有用户;旧用户升级到本版后限流问题自动消失。 - URL 形式高级用户:若你 host 的仓库默认分支不是
main,deeptrade plugin install https://github.com/<your>/<repo>需显式--ref <branch>。
Why not "use a shared token"
GitHub ToS 明确禁止 token 跨用户共享(被发现 token 会被吊销);即便允许,5000/h 也会被规模化使用快速吃掉,且 token 泄露需要维护者承担权限滥用风险。CDN 改造是唯一既不需要每用户认证、又能稳定服务的方案。未来若需要支持私有仓库或企业内部分发,再走 OAuth Device Flow(gh auth login 同款流程)让每个用户认证自己的 token。
[v0.7.0] — 2026-05-15 — 依赖隔离稳健化 + timezone IANA 校验
本版按 2026-05-15 评审研判文档 §5 v0.7 路线落地 2 项主线,至此 v0.5 § 5 全部"采纳项"清零:
- H6 依赖隔离稳健化:
- dry-run 预检:
run_install之前先跑uv pip install --dry-run抓 uv 的 change plan;解析~(update) /-(remove) 行,与framework_core_canonicals()求交集。若插件依赖会顺带升级 / 降级 / 移除任何框架核心 dep,默认拒绝,要求显式--allow-core-bump才放行。uv不在 PATH 时降级为不做检查(pip没有对等结构化 dry-run),保持 v0.4 装机不动手的承诺。 - dep snapshot:
run_install之前把pip list --format=freeze输出落到~/.deeptrade/dep_snapshots/<plugin_id>/pre-install-<UTC>.txt。失败时错误信息把这个目录甩到用户面前,便于diff回滚。 - 仍不做:venv-per-plugin / subprocess 隔离(重型方案,违反"框架轻");框架 deps 的 pin lockfile(会污染用户项目的解析)。
- dry-run 预检:
- L1 timezone IANA 校验:
AppConfig.app_timezone改用zoneinfo.available_timezones()做硬校验,把Asia/Shangai这种近似拼写在写时拦住,而不是延迟到第一次ZoneInfo(...)调用爆ZoneInfoNotFoundError。base_urlHTTPS 强制按方案 §4 评估仍不做(本地 LLM 网关 / Ollama 常用 http)。
Added
deeptrade/core/dep_installer.py:framework_core_canonicals()——读取deeptrade-quantdistribution 的Requires,过滤掉extramarker,输出框架核心 dep 的 canonical 名字集合。从源码运行(未pip install -e)时返回空集合,让预检自动降级为 no-op。preflight_dry_run(reqs, *, watched)——best-effort 跑uv pip install --dry-run;解析 stderr / stdout 中的~与-行;返回与watched相交的 canonical 名字集合。uv缺席、resolution 失败、超时——一律返回空集合,让真正的run_install路径报底层错误,预检不抢话语权。_parse_dry_run_changes(text, watched)——dry-run 输出解析的纯函数版本,便于单测覆盖。write_dep_snapshot(plugin_id, dir_root)——优先uv pip list,回退python -m pip list(uv 管理的 venv 通常不带 pip);写<dir_root>/<plugin_id>/pre-install-<UTC>.txt,返回Path或失败时None。DRY_RUN_TIMEOUT_SECONDS短超时常量,避免被卡住的 uv 阻塞真正的 install。
deeptrade/core/paths.py::dep_snapshots_dir()——~/.deeptrade/dep_snapshots/;纳入ensure_layout()自动创建。deeptrade/core/plugin_manager.py::PluginManager.install/upgrade接allow_core_bump: bool = False;_handle_dependencies在run_install之前先跑 preflight 与 snapshot,并在 install error 信息里附上 snapshot 目录 hint。deeptrade/cli_plugin.py:plugin install与plugin upgrade加--allow-core-bumpflag(中文 help 文案)。deeptrade/core/config.py::AppConfig._validate_app_timezone——基于zoneinfo.available_timezones()的@field_validator;非法值抛 PydanticValidationError并指出如何打印当前主机支持的全部 zones。- 测试:
tests/core/test_config.py5 个新用例(默认值通过、典型 IANA 名通过、typo / 空串 / 任意字符串拒绝)。tests/core/test_plugin_dependencies.py10 个新用例(framework_core_canonicals列出 must-have、_parse_dry_run_changes识别 update / remove、忽略 install 行、uv 缺席返回空集、空 requirements 返回空集、watched 为空跳过 subprocess、_handle_dependencies默认拒绝 core bump 与--allow-core-bump放行、write_dep_snapshot写文件、pip 缺失返回None、CLI 双向)。
Changed
deeptrade/core/plugin_manager.py::_handle_dependencies:新增allow_core_bumpkwarg;run_install调用包了try/except DepInstallError,把异常包裹一层附上 snapshot 目录提示,便于排障。deeptrade/core/dep_installer.py::write_dep_snapshot:拆出_snapshot_argv()帮助选择uv pip listvspython -m pip list,与detect_installer()的优先级一致。
Migration notes
- 用户视角零强制动作:默认行为是更严的(核心 dep 改动需要显式确认),但常见插件不会动框架核心 dep——绝大多数
deeptrade plugin install <短名>命令仍然零摩擦。 - 遇到核心依赖冲突如何处理:v0.7 之后若看到
plugin install would change framework core dep(s) [...]的拒绝信息,先看 dry-run 报告的具体包:- 如果是
duckdb/pydantic/click/typer等加载链路上的运行时关键件——大概率是插件 spec 写得太死,反馈给插件作者放宽;强行--allow-core-bump可能让框架进程立刻 import 失败。 - 如果只是 minor 版本提升且已读过 release note——加
--allow-core-bump显式确认放行。
- 如果是
- uv 缺席的主机:
preflight_dry_run自动跳过,行为与 v0.4 一致。write_dep_snapshot也能用python -m pip list兜底(虽然 uv-only venv 上 pip 通常缺席,此时 snapshot 静默 no-op)。 - dep_snapshots 目录的清理:框架本身不做轮转。如果该目录变大,可以放心
rm老的pre-install-*.txt文件——它们只用于回溯,不影响运行时。 - timezone:升级到 v0.7 之后第一次
Database()打开会触发自动迁移,迁移本身不动app.timezone;但如果你之前不小心存了 typo 进app_config,下一次ConfigService.get_app_config()会抛ValidationError。修法:deeptrade config set app.timezone Asia/Shanghai(或你本地的正确 IANA 名)。
[v0.6.0] — 2026-05-15 — 插件运行时 API + LLM 方言 + 杂项收尾
本版按 2026-05-15 评审研判文档 DeepTrade-review-assessment-and-fix-plan-2026-05-15.md §5 中 v0.6 路线落地 6 项主线:
- H4 插件运行时 API:
api_version="2"的插件改用dispatch(ctx, argv)接收PluginContext,无需再import deeptrade.core.*私有路径取 db / config。api_version="1"永久向后兼容,运行时按 metadata 决定调用形态。plugins_api/__init__.py把LLMManager/TushareClient提升为公共 API。 - H5 LLM 方言收口:
OpenAICompatTransport加 class 属性supports_reasoning_effort默认 False;新增OpenAIOfficialTransport(base_url 含api.openai.com)改 True。其余 OpenAI-compat provider 不再裸送reasoning_effort,消除国内非推理模型常见的 400 与忽略两种失败模式。 - H1 hard error:
table_prefix派生不匹配从 v0.5 的DeprecationWarning转ValueError,与显式声明路径合流,让"插件表必须落在派生命名空间"成为可执行约束。 - M6:
env_var_for(key)把 provider 名中的-归一化为_,避免DEEPTRADE_LLM_QWEN-PLUS_API_KEY这种 bash/sh 非法标识符;set_llm_provider在归一化后冲突时硬拒绝,避免qwen-plus/qwen_plus共用同一个 env var 时的静默 shadowing。 - M8:
_try_load_keyring改用keyring.get_keyring()的非写入式 backend 探测,只在backends.fail.*/backends.null.*类时返回 None;结果缓存到模块级变量,配_invalidate_keyring_cache给测试用。彻底消除SecretStore.__init__每次都向 OS 凭据存储写一个__probe__的副作用。 - M9:
TushareClient首次遇未知 API 改打 INFO 日志(进程级 + 实例级双 dedup,不再每次默认就刷屏);PluginMetadata.permissions.tushare_apis.cache_overrides: dict[str, str]让插件作者声明非默认缓存策略,TushareClient(cache_overrides=...)在分类决策中优先采纳。
Added
deeptrade/plugins_api/base.py:docstring 标注api_version与dispatch签名的对应关系;Plugin.dispatch改写为变参 Protocol 占位(runtime_checkable只校验属性存在,签名由框架按 metadata 决定)。deeptrade/core/plugin_manager.py::SUPPORTED_API_VERSIONS(frozenset({"1", "2"}));install / upgrade 路径用集合包含判断替换原先的!= CURRENT_API_VERSION,对未来扩"3"留接口。deeptrade/cli.py::_dispatch:按rec.api_version选择plugin.dispatch(argv)或plugin.dispatch(ctx, argv);v2 路径由框架开Database+ 构PluginContext+ 收尾关库,插件不再操心生命周期。deeptrade/plugins_api/__init__.py:重导出LLMManager与TushareClient,正式纳入公共 surface 的__all__。deeptrade/plugins_api/metadata.py::TushareApiPermissions.cache_overrides: dict[str, str],配_cache_overrides_values_validmodel_validator 强制值落在{static, trade_day_immutable, trade_day_mutable, hot_or_anns};非法值在 yaml 解析阶段拒绝,避免运行时缓存行为静默退化。deeptrade/core/tushare_client.py:TushareClient.__init__新增cache_overrideskwarg;新增_resolve_cache_class把"插件 overrides → 框架表 → 默认 + 一次性 INFO 日志"三级优先级集中表达;模块级_UNKNOWN_API_LOGGED做进程级 dedup。deeptrade/core/llm_client.py:OpenAICompatTransport.supports_reasoning_effort: bool = False类标志位;OpenAIOfficialTransport子类(api.openai.combase_url 触发)将其覆盖为 True;chat()仅在supports_reasoning_effort and reasoning_effort时把字段写入kwargs。_TRANSPORT_BY_BASE_URL新增("api.openai.com", OpenAIOfficialTransport)路由。deeptrade/core/secrets.py:模块级_keyring_cache/_keyring_probed缓存;_invalidate_keyring_cache测试钩子;_try_load_keyring重写为按 backend FQCN 子串识别 fail/null 类。deeptrade/core/config.py::ConfigService.set_llm_provider:归一化后冲突检测;env_var_for文档说明-→_规则的动机。- 测试新增:
tests/plugins_api/test_api_version_2.py(6 用例,覆盖双 api_version 安装、unknown api_version 拒绝、v1/v2 dispatch arity、公开类导出)、tests/core/test_secrets.pyv0.6 M8 节 3 用例(fail backend 拒绝、real backend 不写探针、cache 可重置)、tests/core/test_config.py3 用例(env 归一化、归一化冲突拒绝、同名更新放行)、tests/core/test_plugin_security.py3 用例(cache_overrides 默认空 / 合法值 / 非法值)、tests/core/test_tushare_client.py3 用例(overrides 优先、未知 API override 路径、INFO 日志一次性)、tests/core/test_llm_client.py4 用例(supports_reasoning_effort 默认 False、OpenAI-official 路由、Generic transport 不发送、empty 字符串不发送)。
Changed
deeptrade/plugins_api/metadata.py::_tables_match_prefix:table_prefix省略且不匹配派生时从warnings.warn(DeprecationWarning)转raise ValueError;删除现已无用的import warnings。模块顶部 docstring 把 v0.5 advisory / v0.6 enforcement 的边界明确标注。deeptrade/plugins_api/metadata.py:TushareApiPermissions加cache_overrides字段;定义_CACHE_CLASS_VALUES内部常量便于校验。deeptrade/core/tushare_client.py:cache_class = API_CACHE_CLASS.get(api_name, "trade_day_immutable")改为cache_class = self._resolve_cache_class(api_name),集中所有分类决策。deeptrade/cli.py:v2 路径在 dispatch 周边内联ConfigService/PluginContext导入,避免顶层冷启动负担;其余跨 dispatch 路径行为不变。tests/core/test_plugin_security.py:test_plugin_table_prefix_warns_when_omitted_and_mismatched改名test_plugin_table_prefix_hard_fails_when_omitted_and_mismatched,正向断言ValidationError;test_plugin_table_prefix_no_warning_when_tables_match_derived改名test_plugin_table_prefix_passes_when_tables_match_derived,保留simplefilter("error", DeprecationWarning)作为反向断言。tests/core/test_plugin_install.py::_make_minimal_plugin_dir:table_name默认值从硬编码test_table改为按plugin_id派生(<pkg>_t),满足 v0.6 派生前缀约束;table_ddl默认也从默认值同步派生。tests/core/test_plugin_dependencies.py::_minimal_meta_dict:表名x_t改为minimal_x_t以匹配plugin_id="minimal-x"的派生前缀。tests/core/test_llm_client.py::test_select_transport_class_defaults_to_generic:api.openai.com从"回退到 Generic"断言改为"回退用 Generic / OpenAI-official 走专属路由"的双行声明;docstring 标注 v0.6 行为变化。
Migration notes
- 插件作者侧(推荐路径):迁移到
api_version="2"——deeptrade_plugin.yaml::api_version改为"2"。def dispatch(self, argv):改写为def dispatch(self, ctx, argv):。- 把
Database(paths.db_path())/ConfigService(...)等手动构造替换为ctx.db/ctx.config;之前从deeptrade.core.*import 的LLMManager/TushareClient改为from deeptrade.plugins_api import LLMManager, TushareClient。
- 插件作者侧(最低改动路径):不动
api_version="1",所有既有插件零代码改动继续工作;唯一硬约束是metadata.tables必须落在<plugin_id 派生前缀>_*命名空间内(或显式声明table_prefix)。 - 多 LLM provider 命名:使用
qwen-plus/qwen-max-2024这类带连字符的 provider 名时,DEEPTRADE_LLM_QWEN_PLUS_API_KEY形式的 env var 现在可以正常生效;不要再同时注册归一化后冲突的名字(qwen-plus与qwen_plus)。 - 官方 OpenAI 推理模型:
reasoning_effort现在只在base_url含api.openai.com时上行。其他 OpenAI-compat 网关如需该字段(少见),临时方案是 fork 当前 transport 类并覆写supports_reasoning_effort = True;后续版本会按需要在路由表里追加。 - Tushare 缓存策略覆盖:插件作者可在
deeptrade_plugin.yaml写
把框架表里没列出的 API 显式钉到正确的缓存类,避免依赖默认permissions: tushare_apis: cache_overrides: moneyflow_industry_ths: trade_day_mutabletrade_day_immutable。运行时由插件自己把这份 dict 作为cache_overrides=...传给TushareClient。
[v0.5.0] — 2026-05-15 — 插件信任边界 + CLI 中文化
本版主线落地 2026-05-15 评审研判文档 DeepTrade-review-assessment-and-fix-plan-2026-05-15.md 中的 v0.5 路线:把"插件可以声明并清掉框架核心表"这条被忽视的信任边界明确收口,让框架的 SQL 迁移有边界、插件加载有 sys.modules 守卫、升级路径不会被静默改动的历史 migration 蒙混过关;同时把命令行用户向文案统一为中文,建立首道防回退的回归测试。整体不引入新框架概念(拒绝了报告里的 RuntimeContext 大对象、独立 venv、SQL DSL 等重型方案),保持"框架轻、插件重"的原则。
实现按 5 个 PR 切片推进;既有插件不需要任何源码改动,但 v0.6 会把 v0.5 用 DeprecationWarning 提示的 table_prefix 缺省派生策略升级为硬失败——本版起插件作者应开始把声明的表名收敛到 <plugin_id 派生前缀>_* 命名空间。
Added
deeptrade/plugins_api/metadata.py:新增公开常量RESERVED_TABLE_NAMES(10 个框架自有表)与RESERVED_TOP_PACKAGES(15 个框架 / 公共依赖的顶层包名)。PluginMetadata追加 4 个model_validator:_tables_not_reserved:拒绝声明任何RESERVED_TABLE_NAMES中的表(H1)。_tables_match_prefix:table_prefix显式声明则全表强制匹配;省略时按plugin_id.replace('-','_') + '_'派生并对不匹配的表发DeprecationWarning。v0.6 转硬失败。_entrypoint_top_pkg_matches_plugin_id:entrypoint 顶层包名必须等于plugin_id.replace('-','_'),且不在RESERVED_TOP_PACKAGES中(H3 / T04)。- 新增
table_prefix: str | None(pattern^[a-z][a-z0-9_]{0,15}_$)字段。
deeptrade/core/migrations/core/20260515_002_affected_tables.sql:给plugin_schema_migrations加affected_tables TEXT DEFAULT NULL列,由框架在每条 migration 应用后写入实际新增的表名(JSON 数组)。purge 路径优先按此列反查归属,plugin_tables仅作为兼容老库的兜底(H2 / T03)。deeptrade/core/plugin_manager.py::_apply_migrations:重写为按每条 migration 做 schema-diff(information_schema.tables前后快照)+ 3 道边界检查:- 新增表必须在
meta.tables声明集合内(创建未声明表 →PluginInstallError); - 删除表不可命中
RESERVED_TABLE_NAMES(DROP 核心表 →PluginInstallError); - 删除表必须当前归属本插件(
plugin_tables反查)。
- 新增表必须在
deeptrade/core/plugin_manager.py::_load_entrypoint:新增sys.modules["deeptrade"]守卫(T07)—— import 前后比较框架模块的对象身份与__file__,被替换则立即还原并PluginInstallError;新增isinstance(instance, Plugin)校验(T08),缺dispatch/validate_static/metadata任一属性即拒绝。deeptrade/core/plugin_manager.py::upgrade:在跑任何迁移前先比对(version, checksum)——若 metadata 中某条 migration 的 version 已在 DB 中但 checksum 不一致,直接PluginInstallError("checksum changed ... historical migrations are immutable")(T09 / M4)。deeptrade/core/plugin_manager.py::_purge_plugin_tables:从uninstall(purge=True)中拆出独立方法。affected_tables全部非空则按其与plugin_tables(purge=True)取交集;任一行为 NULL(v0.4 老库)则退化到全plugin_tables;无论走哪条路径都在 DROP 前再次拦截RESERVED_TABLE_NAMES并logger.error留痕(H1 / T06)。deeptrade/cli_plugin.py::cmd_enable:补PluginInstallError捕获分支(T11),把 manager 给出的 "install_path missing, reinstall before enabling" 提示透传,Exit(2)。deeptrade/cli_config.py::cmd_show:表格之后追加黄色⚠ N 个 secret 以明文存储提示(T13)。deeptrade/core/config.py::ConfigService.count_plaintext_secrets():上述 CLI 警告的支撑方法,封装SecretStore.list_records()过滤,避免 CLI 触私有属性。tests/core/test_plugin_security.py:v0.5 trust-boundary 全套负向回归,38 个用例覆盖 RESERVED 集合、table_prefixwarn/hard、entrypoint 顶层包约束(参数化跑 14 个保留包名)、schema-diff 拒创建未声明表、拒 DROP 核心表、purge 即便 plugin_tables 被污染仍跳过核心表、Plugin Protocol 缺 dispatch 拒装、upgrade 拒收已篡改的历史 migration。tests/cli/test_user_facing_strings_are_chinese.py:T16 中文化回归锁,11 个用例覆盖--help / config show / config show --help / plugin list / plugin install --help的中文关键字断言 + 6 条参数化反向断言(钉死被替换掉的英文短语)。- 新增覆盖:
tests/cli/test_plugin_cmd.py::test_enable_missing_install_path_message(T24)、tests/cli/test_config_cmd.py::test_config_show_warns_plaintext_secrets与_no_warning_when_no_secrets_set(T26)。
Changed
deeptrade/core/plugin_manager.py:删除自实现的_iter_statementsSQL 多语句分割器,改为self._db.execute(sql)直接喂入整段——DuckDB 原生支持;分隔多语句执行(T10)。deeptrade/cli.py/cli_config.py/cli_plugin.py/cli_data.py+plugin_manager.summarize_for_install:用户向文案全面中文化(T15)。范围限定为typer.echo/ Rich Table 标题与列名 /questionary.*prompt /typer.Option/Argument的help/@app.commanddocstring 首行 /✘ / ✔ / ⚠句子;明确保留英文:raise XxxError(...)消息、logger.*、源码注释 + docstring 第二行及之后、命令名本身——保证异常 / 日志 / stack trace 在英文社区仍可搜可贴。deeptrade/cli_plugin.py::cmd_uninstall:docstring +--purgeoption help 重写为中文,准确描述实际两段语义(默认 = 删文件 + disable,可重装恢复;--purge= 同时 DROP 表与所有 metadata 行,不可恢复)。实际行为不动。pyproject.toml::[tool.hatch.build.targets.sdist].include:删除不存在的/DESIGN.md、/PLAN.md(v0.2.0 删除的 docs 残留),改为收录/CHANGELOG.md(T14)。deeptrade/{cli_data, core/github_fetch, core/dep_installer, core/plugin_source, core/registry}.py五处模块 docstring 中的docs/*_design.md失效引用全部改指CHANGELOG.md对应版本段(项目 docs 目录已在 v0.2.0 删除,不予恢复)。README.md保留 IDs 段落补一句:db长度未达 plugin_id 正则下限(^[a-z][a-z0-9-]{2,31}$,至少 3 字符),由正则隐式拒绝;其余四个由RESERVED_PLUGIN_IDS在安装时显式拦截。
Migration notes
- 插件作者侧零强制动作:本版只对边界违例发
DeprecationWarning(仅table_prefix派生不匹配这一项)。v0.6 会把它转硬失败,因此官方插件 + 第三方插件建议在下个版本前把表名收敛到<plugin_id 派生前缀>_*命名空间(如limit-up-board→ 派生limit_up_board_,所有表以此开头),或在deeptrade_plugin.yaml顶层显式声明table_prefix: <prefix>_。 - v0.4 已装插件平滑升级:
plugin_schema_migrations.affected_tables是新列,老插件历史行将以 NULL 存在;uninstall --purge检测到 NULL 自动退化到 v0.4 的plugin_tables路径,行为不变。重新跑一次 install/upgrade 会让新写入的 migration 行带上affected_tables,自动进入更严格的路径。 - DB 自动迁移:v0.4.2 起
Database()已默认auto_migrate=True,本版新增的20260515_002_affected_tables.sql在用户下次执行任意deeptrade <...>命令时静默应用,无需手动db upgrade。设置DEEPTRADE_SKIP_AUTO_MIGRATE=1仍是逃生通道。 - CLI 输出语言切换:异常 / 日志 /
--help中 Typer 自动生成的Usage:/Options:头仍是英文(Typer 行为,不打算改),其余命令输出全部中文化。监控 / 自动化脚本如有按英文短语 grep 输出的,需要按tests/cli/test_user_facing_strings_are_chinese.py中的中文关键字相应更新。
[v0.4.2] — 2026-05-12 — Database 构造自动迁移 + DEEPTRADE_DEBUG=1 显示堆栈
定位到一个用户报错:升级到 0.4.1 后第一次跑 deeptrade limit-up-board lgb train ... 直接报 ✘ TypeError: list indices must be integers or slices, not str。链路追到 TushareClient._restore_cached_frame —— 0.4.1 改用 {"version","schema","data"} 包裹格式读取,但旧裸数组的缓存行没被清掉。根因不是缓存代码本身,而是迁移触发链路:apply_core_migrations 只在 deeptrade db init / db upgrade 显式调用,包升级后用户不手动跑就一直拿不到 20260512_001_drop_legacy_tushare_cache.sql 的清空动作。同时报错只有一行无堆栈,定位也很慢——这是双层 swallow(插件 + 框架都没渲染 traceback)。
Changed
deeptrade/core/db.py::Database.__init__新增auto_migrate: bool = Truekwarg;构造时若未关闭(且DEEPTRADE_SKIP_AUTO_MIGRATE未置位)就自动调一次apply_core_migrations(self)。任何打开 DB 的代码路径(CLI 插件分发、PluginManager、未来的 SDK 调用)都因此跟进 schema,包升级后无需手动db upgrade即可避开 0.4.1 那种"新读路径碰旧数据"的尴尬。deeptrade/cli.py的init/db init/db upgrade显式传auto_migrate=False,自己调apply_core_migrations收集"本次新跑了哪些版本"以打印✔ Schema applied: <version>行;其他命令走默认 auto-migrate 路径。deeptrade/cli.py::_dispatch给插件dispatch()包了一层兜底:捕获BaseException(放过SystemExit/KeyboardInterrupt)后用render_exception渲染、按typer.Exit(1)传播。已经自己 catch 的插件不受影响;不 catch 的插件也能在框架层看到一致的输出格式。
Added
deeptrade/plugins_api/errors.py:render_exception(exc, *, header_glyph="✘")与debug_enabled()。约定环境变量DEEPTRADE_DEBUG=1(接受1/true/yes/on,大小写不敏感)——开启时输出完整traceback.format_exception文本(自动包含__cause__链与 exception group),否则维持✘ {ExcType}: {msg}一行。两者均不带尾部换行,调用方自加。从plugins_api/__init__.py重导出。- 逃生通道
DEEPTRADE_SKIP_AUTO_MIGRATE=1:当一次失败的迁移把所有 CLI 命令都堵住时,置位该变量绕过 auto-migrate 还原现场。文档位于 README troubleshooting 段(非主流程)。 tests/core/test_db.py:5 个新用例覆盖 auto-migrate 行为——新建 DB 自动跑迁移、auto_migrate=False跳过、DEEPTRADE_SKIP_AUTO_MIGRATE=1跳过、迁移失败硬上抛、预置旧裸数组tushare_cache_blob行 → 再开 DB 必须被 0.4.1 的 drop 迁移清掉(本次故事的回归锁)。tests/plugins_api/test_errors.py:默认模式一行、DEEPTRADE_DEBUG=1含Traceback+ 链式__cause__、自定义 glyph、0视为关闭、debug_enabledenv 反映。
Migration notes
- 用户侧零动作:从 0.4.1 升上来的用户下一次跑任何
deeptrade <...>命令时,Database()都会自动应用20260512_001_drop_legacy_tushare_cache.sql,TypeError 自愈。仍想留旧缓存(不推荐)的用户可用DEEPTRADE_SKIP_AUTO_MIGRATE=1 deeptrade ...临时绕过。 - 插件作者:仍可保留自己的
except Exception兜底;推荐改用from deeptrade.plugins_api import render_exception,写成sys.stderr.write(render_exception(e) + "\n")即可让DEEPTRADE_DEBUG=1透传栈信息。api_version不动("1");旧插件不调新工具也照常工作。 - 测试夹具变更:
tests/core/test_db.py::fresh_db改用auto_migrate=False以保留"未迁移"语义;其余apply_core_migrations(fresh_db)用例无须改动(幂等)。
[v0.4.1] — 2026-05-12 — Tushare 缓存 dtype 还原 + 消除 pandas FutureWarning
插件在缓存命中路径上反馈 pandas 2.2+ 抛出 FutureWarning: The behavior of 'to_datetime' with 'unit' when parsing strings is deprecated。根因在框架侧 TushareClient._read_cached:pd.read_json(..., orient="records") 默认 convert_dates=True,按列名启发式(含 date / _at / _time / timestamp / modified)调 pd.to_datetime(values, errors='ignore', unit='ms'),tushare 列名几乎全部命中,每次缓存读取都会触发警告;更危险的是 pandas 未来版本会静默改变 string+unit 的语义。
Changed
deeptrade/core/tushare_client.py::TushareClient._write_cached/_read_cached改用{"version":1, "schema":{col:dtype_str}, "data":[...records...]}的包裹格式存取缓存;读取走json.loads + DataFrame.from_records + _restore_cached_frame,按 schema 显式还原 dtype(datetime64[*]→pd.to_datetime;非object数值/布尔列 →astype;object跳过)。彻底绕开pd.read_json的列名启发式,根因消除。- 框架不引入任何 tushare 业务字段名清单——
schema字典从df.dtypes.astype(str).to_dict()自动派生,保持"框架不持有业务知识"的原则。 - 依赖瘦身:删除
import io(旧实现唯一用途)。
Added
deeptrade/core/migrations/core/20260512_001_drop_legacy_tushare_cache.sql:升级时一次性DROP TABLE IF EXISTS tushare_cache_blob;旧裸数组格式的缓存条目与新读取路径不兼容,惰性建表_ensure_cache_table会在下次写入时重建。tests/core/test_tushare_client.py::test_cache_payload_round_trip_preserves_dtypes:以trade_date(YYYYMMDD字符串)+ts_code(object)+close(float64)+vol(int64)+is_st(bool)+ann_dt(datetime64[ns])全字段 round-trip,warnings.simplefilter("error", FutureWarning)把 warning 当错误抓,锁住回归。
Migration notes
- 缓存清空一次:升级到 0.4.1 后第一次
apply_core_migrations会执行DROP TABLE IF EXISTS tushare_cache_blob;下一次按trade_date拉取的 API 会走一次远程,之后正常命中新格式缓存。tushare_sync_state不受影响(仍记录 status=ok),但_cache_hit在缓存表不存在时已会判为 miss,行为安全。 - 插件无需改动:
TushareClient.call(...)返回的 DataFrame dtype 与首次远程拉取时一致——之前依赖.dt访问器或显式 dtype 的插件代码无须调整。
[v0.4.0] — 2026-05-12 — 插件级依赖管理 + 框架依赖瘦身
两项主线变更合并发布:
- 插件可以声明并由框架自动安装自己的 Python 依赖。
deeptrade_plugin.yaml::dependencies接受 PEP 508 specifier;plugin install / upgrade期间走uv pip install→python -m pip install两级回退,已装且满足跳过,不满足硬拒绝并归因到具体冲突方。设计文档:docs/DeepTrade/plugin_dependency_management_design.md(仓库外)。 - 框架主依赖瘦身:
pandas/tushare不再是 framework 的Requires-Dist,仅作为optional-dependencies.plugin-runtime/devextras 存在。deeptrade.core.tushare_client.TushareClient这一专为插件准备的 wrapper 行为不变,但其运行时依赖现在由插件自己声明。受影响的插件清单与改造指引见docs/DeepTrade/plugin_required_dependencies.md(仓库外)。
Added
PluginMetadata.dependencies: list[str](PEP 508 specifier;extra="forbid"通过默认空列表向后兼容旧 yaml);Pydantic 校验拒绝非法 spec、VCS/URL 形式、重名包(大小写无关)。deeptrade/core/dep_installer.py:parse_specs / plan_install / detect_installer / run_install;installer 探测优先uv(带--python <sys.executable>强制装入框架解释器)→ 回退python -m pip;环境变量DEEPTRADE_DEP_INSTALL_TIMEOUT覆盖默认 300s 超时;marker 不匹配的 requirement 自动跳过。PluginManager._handle_dependencies/_build_dep_ownership:在install/upgrade的copytree之后、migrations 事务之前解析并安装 deps;冲突错误信息归因到"框架核心依赖"或具体plugin <id>(反查既装插件的metadata_yaml)。plugin install/plugin upgradeCLI 新增--no-deps、--reinstall-deps;summarize_for_install增dependencies行;--no-deps启用时摘要区会显式提示。pyproject.toml::optional-dependencies.plugin-runtimeextras:把tushare>=1.4/pandas>=2.2显式收集到这个组,便于本地通过uv sync --extra plugin-runtime调试 TushareClient。tests/core/test_plugin_dependencies.py:29 个用例覆盖元数据校验、planning、installer 探测、PluginManager 集成(安装 / 跳过 / 冲突 / 失败清理 /--no-deps/--reinstall-deps/ 归因到其他插件)、upgrade 行为、CLI 摘要 + flag 透传。
Changed
pyproject.toml::dependencies:移除tushare>=1.4与pandas>=2.2;其余 11 条框架直接使用的依赖(typer / questionary / rich / duckdb / openai / pydantic / tenacity / pyyaml / keyring / click / packaging)保持不变。pyproject.toml::optional-dependencies.dev:补入tushare>=1.4/pandas>=2.2,使uv sync --all-extras仍能跑通tests/core/test_tushare_client.py等触及 wrapper 的测试。deeptrade/__init__.py::__version__→0.4.0;pyproject.toml::project.version→0.4.0(两处同步更新,遵 CLAUDE.md 发版流程)。
Migration notes
- 框架瘦身的兼容性边界:升级到 0.4.0 框架但沿用旧版本插件的用户,如果之前是用
pip install deeptrade-quant一并装下了 pandas / tushare,环境里这两个库仍在,旧插件依然能跑。但重新部署 / 新建虚拟环境时,如果插件 yaml 没声明pandas/tushare,运行期会以ModuleNotFoundError暴露——这就是新的"插件应该自己声明依赖"的预期行为。 - 官方插件改造:
DeepTradePluginOfficial仓中的limit_up_board/volume_anomaly等策略需要把pandas>=2.2/tushare>=1.4写进自家deeptrade_plugin.yaml::dependencies。具体指引见plugin_required_dependencies.md。 - 离线 / 私有源用户:本版本不支持自定义
index_url;如需禁用网络安装,用deeptrade plugin install <source> --no-deps跳过 dep 安装步骤,自行pip install准备好环境。 - 回滚不卸 deps:dep 安装成功但后续 migrations /
validate_static失败时,已装的依赖不会被反向卸载——共享依赖风险下"装哪些卸哪些"会误伤其他插件。设计文档 §4.5 有详细说明。
Out of scope (recorded for follow-up)
deeptrade.core.tushare_client仍位于框架代码树;下一步可考虑把它整体迁出框架(独立 PyPI 包或新的type=service插件类型),届时框架 wheel 完全不带 tushare 相关代码。- 不引入
requirements.lock风格的依赖锁定;当前每次 install 都按 specifier 实时解析。 - 不引入企业级
index_url/ 私有 PyPI 源;v1 直接走 PyPI 默认源。
[v0.3.1] — 2026-05-11 — Tushare 传输层韧性修复
TushareSDKTransport.call 用字符串关键字判断异常类型,长跑训练下 httpx 的 RemoteProtocolError("Response ended prematurely") 等传输层瞬断错误被错归为不可重试的 TushareError,绕过了 tenacity 重试白名单和 5xx → 缓存兜底链路,导致打板策略 lightgbm 训练(单轮 3000+ 次 Tushare 调用)频繁因网络抖动终止。本版本重写分类器并扩展重试策略。设计文档见 docs/DeepTrade/tushare_transport_resilience_plan.md(仓库外)。
Added
TushareTransportError(TushareServerError的子类):传输层瞬态错误的专门异常类型;自动复用现有重试白名单与 5xx 缓存兜底路径,调用方零改动。_classify_tushare_exception/_is_transient_transport_error/_extract_http_status三个模块级辅助函数,实现"按异常类型 → HTTP 状态码 → 字符串关键字"的三级分类,类型识别覆盖 httpx / requests / urllib3 / stdlib 三栈。app_config.tushare_max_retries(默认 7,范围 1-20,dotted keytushare.max_retries):tenacity stop_after_attempt 的最大尝试次数;从原来硬编码 5 提到 7。TushareClient.__init__新增可选max_retries: int = 7参数。tests/core/test_tushare_classifier.py:分类器全套单测(37 个用例),含 "Response ended prematurely" 回归保护。tests/core/test_tushare_retry_r1.py:硬约束 R1 回归测试——每次 tenacity 重试都必须重新经过_TokenBucket.acquire();任何把bucket.acquire()移出_do_fetch的重构都会被这个测试拦下。
Changed
TushareSDKTransport.call:原except Exception中的字符串匹配块全部替换为对_classify_tushare_exception的调用;不再有"5" in msg[:3]这种 in 检查的 bug;未识别的异常默认归类为TushareTransportError(可重试),而非历史的TushareError(终态)——这是核心设计反转,意图是远程网络服务的"未知错"绝大多数是瞬态。TushareClient._fetch_with_retries:从@retry装饰器形式改为显式Retrying(...)( _do_fetch, ...)调用,便于通过__init__注入max_retries;函数体拆为_do_fetch,self._bucket.acquire()仍是_do_fetch的第一行——见__init__中的 R1 注释。- 重试退避策略:
wait_exponential(multiplier=1, min=1, max=15)→wait_exponential_jitter(initial=1, max=30, jitter=2),加 jitter 散开并发重试的"羊群效应";stop_after_attempt(5)→stop_after_attempt(max_retries)(默认 7),最坏等待预算从 ~30s 抬到 ~70s。 - Tushare 限流文案识别扩展:除 "频率"/"限流"/"rate"/"429" 外,新增对 "每分钟…次"(如 "抱歉,您每分钟最多访问该接口500次")的匹配。
tests/core/test_tushare_client.py中 4 处monkeypatch.setattr(TushareClient._fetch_with_retries.retry, "sleep", ...)改为monkeypatch.setattr(cli._retrying, "sleep", ...),配合Retrying实例化下放到__init__。deeptrade/__init__.py:版本 bump 至0.3.1。
Out of scope (recorded for follow-up)
_TokenBucket只 decay 不 recover:撞过几次 429 后 rps 单调下降到 0.1,TushareClient实例生命周期内不自愈。lub 训练每轮新建 client 所以单轮内不至于雪崩,但跨长跑场景需在后续版本治理。- plugin 侧
collect_training_window缺 per-day try/except、缺中间检查点——属插件韧性,不在框架范围。
[v0.3.0] — 2026-05-11 — 移除 channel 插件类型 + 内置 notifier
本版本移除 channel 插件类型与框架内置的 notifier 链。IM 推送在实测中需要登录、轮询、发送多步流程,插件一次性 dispatch(argv) 的生命周期与之不匹配,整体能力将以框架级 ChatGateway 模块的形式重做(本版本不含 ChatGateway 实现,仅完成清理)。
Removed
deeptrade/plugins_api/channel.py(ChannelPluginProtocol)deeptrade/plugins_api/notify.py(NotificationPayload/NotificationSection/NotificationItem)deeptrade/core/notifier.py(NoopNotifier/MultiplexNotifier/AsyncDispatchNotifier/build_notifier/notify/notification_session)tests/core/test_notifier.py/tests/plugins_api/test_notify.py整文件tests/plugins_api/test_protocol.py中ChannelPlugin相关 casedeeptrade.notify/deeptrade.notification_session顶层导出
Changed
PluginMetadata.type收窄为Literal["strategy"](字段保留,便于未来扩展新 plugin 类型)。apply_core_migrations新增 v0.3.0 数据迁移migrate_purge_non_strategy_plugins:启动时清理plugins.type != 'strategy'的历史记录、plugin_tables/plugin_schema_migrations关联行,并删除对应 install 目录。该迁移必须先于PluginManager.list_all()执行,否则旧channel行会触发 Pydantic 校验失败。deeptrade/__init__.py:版本 bump 至0.3.0,移除 notify 顶层 re-export。- README / CLAUDE.md:删除 channel / notify 段落与架构图相关条目;CLAUDE.md 新增"IM / notifications"短段说明能力暂缺、ChatGateway 是计划替代物。
- 官方 strategy 插件
limit_up_board/volume_anomaly的runtime.py:删除未被调用的notify()/is_notify_enabled()方法与NotificationPayloadimport(这些方法在实际业务流中无任何调用点)。
Migration notes
- 从 0.2.x 升级:
pipx upgrade deeptrade-quant。下次任何deeptrade ...命令首次落地时,apply_core_migrations会自动清理stdout-channel等type=channel的历史插件记录及其 install 目录(行为等价于deeptrade plugin uninstall <id> --purge),并在日志输出对应警告。 - 官方注册表(
DeepTradePluginOfficial)的stdout-channel条目将在配套发版中下线,请勿再尝试安装。 - 业务流仍需要 IM 推送的用户:临时方案是在策略代码内直接
import httpx自行调用 webhook;统一的 ChatGateway 能力将在后续版本提供。
[v0.2.0] — 2026-05-09 — 框架瘦身(builtin 插件物理移除)· PR-8 cutover
本版本完成了框架与插件的物理解耦——deeptrade-quant wheel 不再携带任何插件代码。所有官方插件(limit-up-board / volume-anomaly / stdout-channel)必须通过 deeptrade plugin install <短名> 从注册表安装。
Removed
deeptrade/strategies_builtin/整目录(含limit_up_board/volume_anomaly)deeptrade/channels_builtin/整目录(含stdout)tests/strategies_builtin/整目录(140 个测试随插件代码迁移到DeepTradePluginOfficial)mypy.ini中针对 builtin 的ignore_errors(已无需要忽略的目标)
Changed
- README.md / docs/quick-start.md / docs/plugin-development.md /
guide/plugin/strategy/volume-anomaly.md:命令示例从本地 builtin 路径切换为短名(
deeptrade plugin install limit-up-board等)。 deeptrade/core/notifier.pydocstring:移除channels_builtin/ mirrors strategies_builtin/的过期引用。
Migration notes
- 从 0.1.x 升级:
pipx upgrade deeptrade-quant。已通过本地路径安装的 builtin 插件不会被升级删除(plugin 数据/代码独立于框架 wheel),可继续使用,也可执行deeptrade plugin upgrade <短名>切换到注册表版本。 - 历史快照可回溯:
git checkout archive/with-builtin-plugins-v0.1.0-preview取回含 builtin 的最后状态。 - 回滚发版:用户可
pipx install deeptrade-quant==0.1.0锁定上一稳定版本。
Wheel size impact
| 指标 | v0.1.0 | v0.2.0 |
|---|---|---|
| wheel 文件数 | 74 | 37 |
| Python 源文件(mypy 扫描) | 65 | 31 |
| 内置 .sql migration | 4 | 1(仅框架核心) |
v0.1.0 — 2026-05-09 — 框架与插件解耦 · PyPI 首个稳定发布(PR-1 ~ PR-6)
PyPI distribution name: deeptrade-quant(CLI 命令仍是 deeptrade)。
pipx install deeptrade-quant # or: uv tool install deeptrade-quant
deeptrade plugin search # browse the official registry
deeptrade plugin install limit-up-board
参考 docs/distribution-and-plugin-install-design.md。本版本是 §10 中
PR-1~PR-6 的合集;builtin 插件目录仍随 wheel 一起发布(兼容旧的本地路径
安装),下一个版本(PR-8 cutover)将完成瘦身。
Added
- 从 PyPI 分发:项目元数据、LICENSE、GitHub Actions(
ci.yml/release.yml)就位;tag v*触发 OIDC Trusted Publisher 直发 PyPI。 - 官方插件注册表客户端(
deeptrade.core.registry):从raw.githubusercontent.com/ty19880929/DeepTradePluginOfficial/main/registry/index.json拉取,ETag 缓存到~/.deeptrade/plugins/registry-cache.json;网络故障 时回退到本地缓存。 - GitHub tarball 拉取(
deeptrade.core.github_fetch):stdlib 实现 (urllib+tarfile),无第三方 HTTP 依赖;GITHUB_TOKEN环境变量 自动用于 rate-limit / 私有仓库(未来扩展);解压做 path-traversal 防护。 - 来源解析层(
deeptrade.core.plugin_source):SourceResolver把 用户输入(短名 / GitHub URL / 本地路径)统一解析为本地目录,强制min_framework_version校验。 - 新命令
deeptrade plugin search [keyword] [--no-cache]:列出注册 表中可用的插件;--no-cache旁路 ETag 缓存。 plugin info注册表 fallback:未安装但在注册表中时显示注册表条 目 + 安装命令提示。
Changed
deeptrade plugin install <SOURCE>现支持三种来源:短名(注册表)、 完整 git 仓库 URL、本地路径。判定顺序:本地目录存在 → git URL → 短 名。--ref <tag|branch|sha>可指定具体 ref(默认拉该插件最新 release tag)。deeptrade plugin upgrade <SOURCE>同上,且加入 SemVer 比较:- 待装 == 已装:
exit 0+ "已是最新版本 vX" - 待装 > 已装:执行升级
- 待装 < 已装:
exit 2+ 提示先uninstall --purge(禁止降级, 因为框架未建模 migration 回滚)
- 待装 == 已装:
PluginManager.upgrade()返回类型改为InstalledPlugin | UpgradeNoop。
Fixed
pyproject.toml删除了与packages = ["deeptrade"]冲突的[tool.hatch.build.targets.wheel.force-include]段(曾导致 wheel 内deeptrade/core/migrations/路径 duplicate name,让 PyPI 上传失败)。deeptrade.core.notifier:channel 实例 cast 到ChannelPlugin(mypy narrowing 修复)。- 一批预存在的 ruff lint(UP045 / F401 / I001 / W292 / B023)清理。
Notes
- builtin 插件(
limit-up-board/volume-anomaly/stdout-channel) 代码已迁移到DeepTradePluginOfficial独立仓库,并发布了首个 release tag(limit-up-board/v0.4.0等)。 本版本框架仍然包含旧 builtin 子树作为兼容期保留;下一版本(瘦身后) 将物理移除。 - archive tag
archive/with-builtin-plugins-v0.1.0-preview永久保留含 builtin 子树的最后状态,可随时git checkout取回。
[volume-anomaly v0.6.0] — 2026-05-08 — 显式分维度评分(PR-6)
仅升级 volume-anomaly 插件版本(0.5.0 → 0.6.0);框架版本不变。 升级方式:
deeptrade plugin upgrade <plugin-source>,会自动应用新增的 migration(20260601_002_dimension_scores.sql)。
⚠ Schema 变更:
VATrendCandidate.dimension_scores现为必填字段; 旧 LLM 响应不再可解析(但持久化到va_stage_results.raw_response_json不受影响——升级前的历史数据完整保留可读,仅新写入要求新 schema)。
参考 docs/volume_anomaly_wave2_design.md § 2。
Added (volume-anomaly plugin)
- 新子模型
VADimensionScores:6 个维度(washout/pattern/capital/sector/historical/risk)每个 0–100 整数评分, 其中risk为反向极性(高分 = 高风险),其余为正向极性。 VATrendCandidate.dimension_scores必填(F8):每只候选都必须输出 6 维评分,由 LLM 自洽与launch_score大致一致(F3 / F14 软约束, 不强制公式)。- Prompt 评分尺度锚定:
VA_TREND_SYSTEM加入【dimension_scores 评分尺度】 段(0-30 / 30-60 / 60-80 / 80-100 四档语义),每个判断维度(A–F)末尾 显式标注对应dimension_scores.<name>。 - Few-Shot 示例同步:
prompts_examples.pyA/B 示例补dimension_scores对象,与示例评分尺度一致。 - 拆 6 列持久化(G6):
va_stage_results新增dim_washout/dim_pattern/dim_capital/dim_sector/dim_historical/dim_risk6 个DOUBLE列。runner._write_stage_results写入 对应列;旧行dim_*全部为NULL,stats SQL 自动过滤。 原始 JSON 仍写入raw_response_json作为审计备份。 - stats
--by dimension_scores:新支持子选项,输出 6 个维度评分与ret_t3的 Pearson 相关系数(DuckDBCORR(...)内置聚合)。 - 报告紧凑表达:
write_analyze_report在 imminent_launch / watching 表中新增"W/P/C/S/H/R"列,紧凑展示 6 维分数(如80/75/70/75/60/25)。
Implementation notes
- 拆列而非单 JSON 列的理由(G6):
stats --by dimension_scores要求 6 维与ret_t3做 Pearson,AVG/CORR在拆列模式下纯 SQL 表达;JSON_EXTRACT在 SQL 层昂贵且 DuckDB 跨版本兼容性差。 - 输出 token 预算上调:
DEFAULT_AVG_OUTPUT_TOKENS_PER_CANDIDATE900 → 1100(吸纳 6 维评分 + alpha 字段),plan_batches按新预算切批。 - F13 决策:6 维(W/P/C/S/H/R),不加 liquidity。
Breaking changes
- 旧 LLM 响应(无
dimension_scores)会在 Pydantic 解析阶段抛ValidationError。已落地的批次再 retry 时需更新 prompt(已自动包含)。 - 回滚路径:revert PR-6 不需要 drop 列(
NULL列不影响旧逻辑),但 已经按新 schema 持久化的行将无法被旧版本读取——因此降级时保留raw_response_json而忽略dimension_scores_json。
[volume-anomaly v0.5.0] — 2026-05-08 — RPS / 大盘相对 alpha(PR-5)
仅升级 volume-anomaly 插件版本(0.4.0 → 0.5.0);框架版本不变。 升级方式:
deeptrade plugin upgrade <plugin-source>。本版本不新增 migration。
参考 docs/volume_anomaly_wave2_design.md § 1。
Added (volume-anomaly plugin)
- 沪深 300 alpha 字段(P1-1):候选行新增
alpha_5d_pct/alpha_20d_pct/alpha_60d_pct(个股相对沪深 300 同期累计收益差,单位 %)+baseline_index_coderel_strength_label ∈ {leading, in_line, lagging}(基于 alpha_20d_pct ±5% 分档)。让 LLM 在判断"放量异动"时区分跟随性反弹与抗跌强势。
- 新 tushare 权限
index_daily(optional):250d 拉一次沪深 300 基准 收盘价;G1—与个股 daily 长度对称、cache 友好。 - 维度 D 改名 / 扩写:
VA_TREND_SYSTEM中维度 D 由"板块强度"改为 "板块与市场相对强度",提示 LLM 同时考虑板块层 (sector_strength_*) 与市场层 (alpha_*_pct/rel_strength_label)。 - Few-Shot 示例补充 alpha 引用:
prompts_examples.py的 A/B 示例 各加一条alpha_20d_pct引用,确保 anchoring 与新维度一致。 - G8 显式降级提示:
index_daily权限缺失或 fetch 失败时,runner emit 一条EventLevel.WARNLOG 事件,明确提示用户"alpha 字段降级为 None; 如需启用 alpha,请确认 Tushare 账户已开通 index_daily 权限"。
Behavior change
- analyze 阶段每次 run 多 1 次
index_daily调用(cache 友好;首次冷拉 ~5s),无新数据消费 SLA 影响。 - LLM 输入每候选 +30–40 tokens(5d/20d/60d alpha + label); 在 200K 输入预算下完全可接受。
Implementation notes
- alpha 计算口径:
alpha_n = stock_pct_chg_n − baseline_pct_chg_n(简单收益)。 rel_strength_label仅基于alpha_20d_pct;alpha_20d 缺失时rel_strength_label = None,但baseline_index_code永远输出(元数据)。- F2 决策:行业相对 alpha 暂不做,留给波次 3 视效果再加。
[volume-anomaly v0.4.0] — 2026-05-08 — T+N 自动回测闭环(PR-4)
仅升级 volume-anomaly 插件版本(0.3.0 → 0.4.0);框架版本不变。 升级方式:
deeptrade plugin upgrade <plugin-source>,会自动应用新增的 migration(20260601_001_realized_returns.sql)。
参考 docs/volume_anomaly_wave2_design.md § 3。
Added (volume-anomaly plugin)
- 新表
va_realized_returns:每条 hit 一行(PK=anomaly_date+ts_code), 存 T+1/T+3/T+5/T+10 收盘价、对应收益、5d/10d 窗口最大涨幅与回撤。data_status ∈ {pending, partial, complete}(G5 严格三态:T+1 未到→pending; max_horizon 未到 OR 任一 horizon 数据缺失→partial;max_horizon 已到且全填→complete)。 - 新子命令
evaluate:
从deeptrade volume-anomaly evaluate [--lookback-days N] [--trade-date YYYYMMDD] [--backfill-all] [--force-recompute]va_anomaly_history全集(G3)里取 anomaly_date 在 lookback 内的 hits, 按 trade-day horizon 解析 T+1/T+3/T+5/T+10 实际 trade_date,复用_fetch_daily_history_by_date的 cache 拉取收盘价,UPSERT 进va_realized_returns。complete行默认跳过;--force-recompute强制重算。 - 新子命令
stats:
纯只读 SQL 聚合deeptrade volume-anomaly stats [--from] [--to] [--by prediction|pattern|launch_score_bin]va_stage_results JOIN va_realized_returns,输出每桶的 样本数 / T+3 平均收益 / T+3 胜率 / T+5 最大涨幅均。launch_score_bin默认分箱0-40 / 40-60 / 60-80 / 80-100(G4)。 evaluate写 va_runs / va_events(G10):mode='evaluate'与 screen / analyze / prune 同级,可在history子命令中查看。
Implementation notes
- 所有 horizon 时间换算用
TradeCalendar.next_open跳过周末/节假日。 va_realized_returns.t_close冗余存(G9)——让 stats 在 va_anomaly_history 被异常清空时仍可读取。- 不新增 tushare API 权限(仍只用
daily)。 - 不引入新插件级 config 表(F6);horizon 列表写死在 module-level
EVALUATE_HORIZONS = (1, 3, 5, 10)。
[volume-anomaly v0.3.0] — 2026-05-08 — 波次 1(PR-1 + PR-2 + PR-3)
仅升级 volume-anomaly 插件版本(0.2.0 → 0.3.0);框架版本不变。 升级方式:
deeptrade plugin upgrade <plugin-source>。本版本不新增 migration。
参考 docs/volume_anomaly_wave1_design.md(§ 1–5 共五项 P0 优化)。
Added (volume-anomaly plugin)
PR-1 — Screen 规则
- 上影线过滤(P0-1):T-day 规则后、换手率前新增一道过滤,
upper_shadow_ratio = (high − max(open, close)) / range > upper_shadow_ratio_max的候选直接淘汰,避免"避雷针"型的纯上影 K 线进入候选。诊断字段n_after_upper_shadow暴露在漏斗中;hit 行新增upper_shadow_ratio字段。 - 按流通市值分桶的换手率阈值(P0-2):用
daily_basic.circ_mv把候选按 流通市值(亿元)切 4 档,每档使用独立的[turnover_min, turnover_max]:≤50亿: [5, 15]/50–200亿: [3.5, 12]/200–1000亿: [2.5, 9]/>1000亿: [1.5, 6]。边界值归"较小桶"(E4)。circ_mv缺失时退化到 全局turnover_min/max,并写入data_unavailable。诊断字段turnover_bucket_hits/n_missing_circ_mv/circ_mv_missing_codes暴露在 screen 报告中;hit 行新增circ_mv_yi/turnover_bucket字段。
PR-2 — Analyze 候选行特征
- VCP 三维收敛指标(P0-3):候选行新增
atr_10d_pct/atr_10d_quantile_in_60d(10 日 ATR 在近 60 日 ATR 序列中的分位数)/bbw_20d(20 日 Bollinger 带宽)/bbw_compression_ratio(当前 BBW / 近 60 日 BBW 均值,<1 表示在收敛)。零新数据,复用现有 60d 历史窗口(实际从 250d 切片)。 - 120/250 日阻力位距离(P0-4):
collect_analyze_bundle默认history_lookback由 60 → 250;_build_candidate_row内部切片处理。 候选行新增high_120d/high_250d/low_120d/dist_to_120d_high_pct/dist_to_250d_high_pct/is_above_120d_high/is_above_250d_high/pos_in_120d_range。 E3-A:仅 120d 区间位置,不补low_250d。E7-C:任一窗口数据不足则 对应字段降级为None,不阻塞主流程。 - 首次冷拉延迟:扩展 250d 窗口的首次 fetch 约多 30–45 秒 (Tushare RPS=6 默认下),后续走 cache 零增量。
PR-3 — LLM Prompt 对齐
- Few-Shot 锚定(P0-5):新增
prompts_examples.py::VA_TREND_FEWSHOT, 在VA_TREND_SYSTEM末尾拼接两个示例(VCP 突破 / 高位上影诱多), 引导 LLM 在不同 provider 间保持一致的launch_score尺度。 - 维度 A 提示扩展:在【判断维度】A 段中加入"整理期波动率收敛"指引,
显式提示 LLM 引用
atr_10d_quantile_in_60d/bbw_compression_ratio。 - 字段一致性单测:
test_prompt_consistency.py用正则提取 示例中的"field": "<X>",断言每个 X 都属于_build_candidate_row输出键集合 OR screen hit 行字段集合。 防止后续字段重命名时 prompt 失配。
Default behavior change (heads-up)
- 默认开启 上影线过滤:
ScreenRules.upper_shadow_ratio_max = 0.35。 如需保持旧行为,在screen_rules配置中显式传"upper_shadow_ratio_max": null。 - 默认开启 分桶换手率:
ScreenRules.turnover_buckets = DEFAULT_TURNOVER_BUCKETS。 如需保持旧行为,在screen_rules配置中显式传"turnover_buckets": null, 此时回退到[turnover_min, turnover_max]全局阈值。 - 默认 analyze 历史窗口由 60 → 250 个交易日(E2-A 单 fetch 复用)。 首次冷启动会多消耗 30–45s(RPS=6 默认),后续走 cache 零增量。
Implementation notes
turnover_bucketsJSON 配置中最后一档可用null表示无穷上界,from_dict内转math.inf。as_dict()反向把math.inf序列化为null, 使screen_stats.json仍是合规的标准 JSON(不依赖Infinity扩展)。- 不动 framework;不新增 tushare API 权限(
circ_mv已包含在daily_basic字段中;250d 长窗口仍走daily)。 - VCP 计算
_compute_atr_series用简单平均 TR(与 Wilder 公式相比差异 在 60d 窗口尺度内不显著);BBW 用 4σ / MA20 × 100 表达带宽占均价比例。
[limit-up-board v0.4.0] — 2026-05-07 — 候选股市值/股价过滤 + 持久化设置
仅升级 limit-up-board 插件版本(0.3.2 → 0.4.0);框架版本不变。 升级方式:
deeptrade plugin upgrade <plugin-source>,会自动应用新增的 migration(20260508_005_lub_config.sql)。
Added (limit-up-board plugin)
- Step 1 候选股漏斗增加两条筛选条件:在主板 + 涨停 join 之后、ST/停牌之前
按 流通市值 与 当前股价 上限再筛一层。null 在任一字段 → 过滤(保守语义)。
默认
流通市值 < 100亿+股价 < 15元。bundle.market_summary新增candidate_filter_summary字段(before / after / 阈值),LLM prompt 中可见。 - 新子命令
deeptrade limit-up-board settings:交互式编辑两个上限阈值, 回车保留当前值,写入新表lub_config。 - 新子命令
deeptrade limit-up-board settings show:表格展示当前生效设置, source ∈ {default,persisted}。 - 运行时打印当前配置:
run/sync在 Step 1 之前 emit 一条 LOG 事件 (运行配置: 流通市值 < ...亿、股价 < ...元),dashboard / 日志中可见。
Implementation notes
- 配置存储:插件自有
lub_config表(与lub_runs/lub_events同 Plan A 纯隔离层级),不复用框架级app_config—— 避免_DOT_TO_FIELD白名单 扩张,框架保持轻量。 - 默认值唯一来源:
limit_up_board.config:LubConfig的 dataclass default; SQL / CLI / 文档不重复声明。
[limit-up-board v0.3.0] — 2026-05-07 — Phase A + Phase B 因子补齐
仅升级 limit-up-board 插件版本(0.2.1 → 0.3.0);框架版本不变。 升级方式:
deeptrade plugin upgrade <plugin-source>,会自动应用本版本新增的两条 migration(20260508_001_lub_lhb_tables.sql/20260508_002_lub_cyq_perf.sql)。
参考 docs/limit-up-board-optimization-plan.md。对照 Gemini 给出的"游资思路"因子
清单,分两阶段补齐:A 阶段为派生因子(不增 tushare API),B 阶段接入龙虎榜与筹码。
Added (limit-up-board plugin)
A1 — 派生因子(候选股层,pure compute)
amplitude_pct:T 日振幅 = (high − low) / pre_close × 100。fd_amount_ratio:封单比 = fd_amount / amount × 100,>10% 为强势封板。ma5/ma10/ma20+ma_bull_aligned:基于 prev_daily 的简单移动平均;ma_bull_aligned = (close > ma5 > ma10 > ma20)。历史不足窗口期返回 null。up_count_30d:近 30 个交易日 pct_chg ≥ 9.8 的天数;不足 30 日返回 null。
A2 — 市场情绪三件套(market_summary 层)
limit_step_distribution_prev+limit_step_trend:T 与 T-1 的连板梯队对比, interpretation ∈ {spectrum_lifting,spectrum_collapsing,stable}。yesterday_failure_rate:T-1 全市场炸板率(z / (u + z)),interpretation ∈ {high≥25%,moderate,low≤10%}。yesterday_winners_today:T-1 涨停股在 T 日的连板率与平均涨幅, interpretation ∈ {strong_money_effect,neutral,weak_money_effect}。
B1 — 龙虎榜(top_list / top_inst → required)
- 候选股层字段:
lhb_net_buy_yi(龙虎榜净买入,亿元;正/负均如实给出)、lhb_inst_count(机构席位 unique 数)、lhb_famous_seats(命中游资白名单的 exalter 原文数组)。 FAMOUS_SEATS_HINTS:~15 条主流游资席位子串白名单(拉萨系、宁波系、章盟主、 赵老哥、厦门帮等)。匹配逻辑大小写不敏感;只给 exalter 原文,不暴露白名单标签。- 新表:
lub_top_list(key trade_date+ts_code)、lub_top_inst(key trade_date+ts_code+exalter+side)。 - "未上榜 ≠ 数据缺失":candidate 不在当日 top_list 中时
lhb_*为 null,但data_unavailable中不会出现 top_list/top_inst(接口本身可用)。
B2 — 筹码(cyq_perf → required)
- 候选股层字段:
cyq_winner_pct(获利盘比例 %)、cyq_top10_concentration(100 − (cost_95pct − cost_5pct) / weight_avg × 100,clip [0,100])、cyq_avg_cost_yuan(weight_avg)、cyq_close_to_avg_cost_pct((close − weight_avg) / weight_avg × 100)。 - 新表:
lub_cyq_perf(key trade_date+ts_code)。
Prompt 改动
- R1_SYSTEM【分析维度】新增形态、历史基因、市场情绪三类;evidence 新增"missing_data 字段不得引用"硬约束。
- R2_SYSTEM【判断重点】追加:亏钱效应下自动下调 confidence 一档;梯队拉升期允许 上调 continuation_score;筹码维度三阈值(70/60/-10)+ 龙虎榜维度(未上榜 ≠ 缺失; 负净买入不可作为正面 evidence;不可推断游资身份)。
- R1 不引入 chip / LHB 维度(控制 prompt 噪声,B 阶段仅 R2 使用)。
Changed (BREAKING)
Tushare 权限要求扩张(用户必须确保 tushare 账户已开通以下权限):
top_list/top_inst/cyq_perf由permissions.tushare_apis.optional上移 到required;任一缺失 → run failed。未引入新权限分类——"接口可用但 candidate 未上榜"用 null + 数据层 inner-join 自然表达。
默认值 / 内部行为变更
RunParams.daily_lookback/ CLI--daily-lookback默认值 10 → 30(满足 ma20 + up_count_30d)。collect_round1新增prev_trade_date: str | None入参;3 个 runner 调用点 通过_safe_prev_trade_date(cal, T)计算并传入。- daily 历史窗口的日历缓冲由
lookback + 5改为lookback × 2,确保 30 个交易 日 lookback 在过节窗口下仍能命中 ≥30 行真实交易日。
Migrations
20260508_001_lub_lhb_tables.sql— 创建lub_top_list/lub_top_inst。20260508_002_lub_cyq_perf.sql— 创建lub_cyq_perf。
Tests
-
tests/strategies_builtin/limit_up_board/test_phase_a_factors.py:33 个单元 测试,覆盖 A1/A2 全部派生函数 + 边界(不足窗口期、零分母、空 frame、阈值跳点)。 -
tests/strategies_builtin/limit_up_board/test_phase_b_factors.py:20 个单元 测试,覆盖白名单匹配(大小写、去重、非字符串)、LHB rollup(空帧 / None / 仅 top_list)、cyq 集中度(紧/宽/截断到 0)、close_to_avg_cost_pct 边界。
[v0.7.0] — 2026-05-01 — Stage 概念归插件 + 配置键改名
清理 v0.6 留下的 stage 硬编码技术债。Stage 名字、preset → stage tuning 表全
部移入插件;框架的 LLMClient.complete_json 不再认识 stage,由调用方直接传
入 StageProfile。配置键 deepseek.profile 同步重命名为 app.profile。
Breaking change(项目仍在 dev/iteration 期)。设计原文:DESIGN.md §10.1。
Changed (BREAKING)
框架瘦身 — stage 退出 LLMClient
- 删除
core.llm_client.KNOWN_STAGES/LLMUnknownStageError/_stage_profile()/ 全局_CURRENT_STAGE。 LLMClient.complete_json签名变化:- 删除
stage: str入参;framework 不再写llm_calls.stage列。 - 新增
profile: StageProfile(必填)— 调用方直接传入已解析的调参档。 LLMClient.__init__删除profiles=入参。LLMManager.get_client()不再绑 profile。
- 删除
RecordedTransport改为纯 FIFO:register(response)不再带 stage 标签。- 删除
core.config.DS_STAGES/DeepSeekProfileSet/PROFILES_DEFAULT/ConfigService.get_profile()。 StageProfile升格为公共契约,搬到deeptrade.plugins_api.llm,由from deeptrade.plugins_api import StageProfile公开导出。
配置键改名 — deepseek.profile → app.profile
AppConfig.deepseek_profile→AppConfig.app_profile;_DOT_TO_FIELD同步更新。preset 仍是Literal["fast","balanced","quality"],语义全局, 但键名 vendor-agnostic。- DB 行自动迁移:
config_migrations.migrate_legacy_deepseek_profile_key幂等地把deepseek.profile行改写为app.profile,并删除旧行。 - 环境变量直接断代:
DEEPTRADE_DEEPSEEK_PROFILE不再被识别。ConfigService.get_app_config()启动时若检测到旧 env 而新 env 未设, 抛RuntimeError退出 — 避免静默用错配置(默认会回落到 "balanced", 让用户以为生效但其实并没有)。请改为DEEPTRADE_APP_PROFILE。
DB schema — llm_calls.stage 列删除
- 新 SQL 迁移
20260501_002_drop_llm_calls_stage.sqlALTER TABLE llm_calls DROP COLUMN IF EXISTS stage(DuckDB 1.0+)。 core/migrations/core/20260427_001_init.sql同步去掉stage字段, 让 fresh DB 直接落到 v0.7 期望状态。- 历史 run 的 stage 信息仍可在
~/.deeptrade/reports/<run_id>/llm_calls.jsonl中查阅;v0.7 起新写入的 jsonl 行也不再含stage键。
插件改造(两个内建插件)
- 新增
<plugin>/profiles.py:本地维护 preset → stage tuning 表 +resolve_profile(preset, stage)。 runner.py读取cfg.app_profile(preset 字符串),传给 pipeline 函数; pipeline 内部调resolve_profile()得到StageProfile。volume-anomaly借此改造把语义错误的 stage 名continuation_prediction改回trend_analysis。
Added
deeptrade/plugins_api/llm.py— 公共StageProfile契约(4 字段: thinking / reasoning_effort / temperature / max_output_tokens)。config_migrations.migrate_legacy_deepseek_profile_key+ 4 条单测 (rename / 幂等 / fresh DB no-op / new-already-set 跳过)。tests/core/test_config.py新增两条 env 行为测试(旧 env 报错、新旧并存 以新为准)。
Engineering
- 136 pytest tests passing (无变更)。
- 偿还 v0.6 RV6-4 / RV6 §10.2 已知技术债。
[v0.6.0] — 2026-05-01 — LLM Manager 化(多 Provider)
The LLM client is no longer DeepSeek-specific — it is now a framework-level
service that lets a single plugin call multiple OpenAI-compatible LLMs in
the same run. Breaking change (project remains in dev/iteration phase).
Full rationale: DESIGN.md §0.7 + §10.
Changed (BREAKING)
Configuration model — deepseek.* → llm.*
llm.providers(JSON dict, app_config) —{name: {base_url, model, timeout}}. Multiple providers coexist; each plugin picks by name at call time.llm.<name>.api_key(secret_store) — one secret per provider. Theis_secret_key()predicate (replacing the oldSECRET_KEYSconstant) matchestushare.tokenplus this dynamic prefix.llm.audit_full_payload(bool, app_config) — replacesdeepseek.audit_full_payload.deepseek.profileis kept as the global stage-profile name (rename deferred to v0.7 per §10.1 note).- The four legacy keys
deepseek.base_url/deepseek.model/deepseek.timeout/deepseek.audit_full_payloadare removed fromAppConfig. There is nollm.default— callers must pass a name.
Auto-migration
- On first
apply_core_migrations()after upgrade, legacydeepseek.*rows are migrated intollm.providers["deepseek"]+ the renamed secretllm.deepseek.api_key+llm.audit_full_payload. Idempotent: re-runs on already-migrated DBs are no-ops. Code indeeptrade.core.config_migrations.migrate_legacy_deepseek_keys.
LLM client / new manager
- New
core/llm_manager.py::LLMManager— the only path plugins should use:list_providers(),get_provider_info(name),get_client(name, *, plugin_id, run_id, reports_dir=None). Caches clients per(name, plugin_id, run_id). Documented as not thread-safe. core/deepseek_client.py→core/llm_client.py;DeepSeekClient→LLMClient.OpenAIClientTransportkeeps its name (it really is the OpenAI-compatible transport).LLMNotConfiguredError(new) — raised by manager when a provider is missing or its api_key is unset.
CLI command surface
- Added:
config set-llm(interactive new / edit / delete one provider),config list-llm(show usable providers),config test-llm [name](per-provider connectivity check; tests all when omitted). - Removed:
config set-deepseek,config test. The init-time prompt ("Configure deepseek now?") is now "Configure an LLM provider now?". config showexpandsllm.providersso each provider'sapi_keyslot is its own masked row.
Plugin runtime collapse
volume-anomalyandlimit-up-boardruntimes lose their per-pluginbuild_llm_client(rt); both now declarellms: LLMManagerand callrt.llms.get_client(provider_name, plugin_id=, run_id=, reports_dir=). Provider selection helperpick_llm_provider(rt)ships in each plugin'sruntime.py(prefersdeepseek, falls back to first available); a per-plugindefault_llmconfig key is deferred to v0.7.
Added
LLMProviderConfigPydantic model (per-provider connection record).ConfigService.set_llm_provider(name, *, base_url, model, timeout, api_key=None)anddelete_llm_provider(name)— CRUD helpers used by the CLI.tests/core/test_llm_manager.py— list/info/get_client + cache + missing api_key/provider errors + multi-provider coexistence (11 cases).tests/core/test_config_migrations.py— idempotency + happy path + partial-legacy-state edge case (7 cases).
Engineering
- 136 pytest tests passing (was 130; +11 manager + 7 migration − 12 retired duplicates).
- ruff + mypy clean on touched files.
- DESIGN §0.7 + §7.1 + §7.3 + §10 rewritten; PLAN §11 adds the v0.6 work breakdown.
Known design debts (deferred)
KNOWN_STAGES(strong_target_analysis / continuation_prediction / final_ranking) is still hardcoded in the framework — leaks plugin semantics intocore/llm_client.py. v0.7 will let plugins declare their own stage names + per-stage profile overrides.deepseek.profilekey name retained for backward compat within the current dev cycle; rename tollm.profileplanned for v0.7.- No transport plugin type yet — Anthropic native / Gemini native cannot be
added without code changes. Will land as
type=llm-transportplugins when first user need surfaces.
[v0.5.0] — 2026-04-30 — Plugin CLI dispatch + pure data isolation
Breaking-change reshape per docs/plugin_cli_dispatch_evaluation.md. The
project is in dev/iteration phase; no backward compatibility.
Changed (BREAKING)
Framework command surface — closed
- Top-level CLI is now
init / config / plugin / dataONLY.strategyandchannelcommand groups removed. - Pure pass-through: any unknown first token is looked up as a
plugin_id; if installed + enabled, framework callsPlugin.dispatch(remaining_argv)and is otherwise dumb. deeptrade --helpno longer enumerates plugin subcommands.--helpinside a plugin is the plugin's own responsibility.deeptrade helloremoved. Interactive main menu removed.- Reserved plugin_ids:
init,config,plugin,data.
Plugin contract — minimal
StrategyPluginProtocol removed. New unifiedPluginProtocol:metadata+validate_static(ctx)+dispatch(argv) -> int.ChannelPluginextendsPluginand addspush(ctx, payload).StrategyContext/StrategyParams/StrategyRunner/ TUI dashboard removed. Each plugin owns its own runtime + run lifecycle internally.ChannelContextrenamed toPluginContext(still narrow: db + config + plugin_id).
Data isolation — Plan A (pure)
- Framework owns ONLY:
app_config,secret_store,schema_migrations,plugins,plugin_tables,plugin_schema_migrations,llm_calls,tushare_sync_state,tushare_calls. No business tables. - Tushare-derived shared market tables (
stock_basic,trade_cal,daily,daily_basic,moneyflow) removed from core. Each strategy plugin declares its own prefixed copies (e.g.lub_stock_basic,va_*). tushare_sync_statePK now(plugin_id, api_name, trade_date). Each plugin tracks its own sync state and cache; no cross-plugin sharing.tushare_callsandllm_callsaddplugin_idcolumn.TushareClient.__init__requiresplugin_id. Framework probes use the reservedFRAMEWORK_PLUGIN_ID = "__framework__"sentinel.
Notification API
core/notifier.pyexposes top-levelnotify(db, payload)andnotification_session(db). Re-exported asfrom deeptrade import notify, notification_session. NoopNotifier when no channel enabled.strategy_runs/strategy_eventstables removed. Each plugin defines its own<prefix>_runs/<prefix>_eventsif it wants run history.
Added
- Built-in plugins reshaped to v0.2.0:
limit-up-board: owncli.py+plugin.py+runner.py+runtime.py; new migration with 10lub_*tables.volume-anomaly: same pattern; 5va_*tables; subcommandsscreen/analyze/prune/history/report.stdout-channel: implements newPlugin+ChannelPlugincontracts; owndispatchfortest/log.
- Tests: framework routing tests (
tests/cli/test_routing.py); Plugin Protocol contract tests (tests/plugins_api/test_protocol.py); plugin install + migration isolation tests (tests/core/test_plugin_install.py).
Removed
cli_strategy.py,cli_channel.py,tui/package,core/strategy_runner.py,core/context.py, old pluginstrategy.pyfiles.textualdependency._interactive_main_menu,hellocommand.
v0.1.0 — 2026-04-28
First public release. Baseline implementation of DESIGN.md v0.3.1.
Added
Framework
deeptrade init— DuckDB layout + 11-table core schema migrations (idempotent).deeptrade config show / set / set-tushare / set-deepseek / test— layered config (env > db > default), keyring + plaintext fallback for secrets.deeptrade plugin install / list / info / disable / enable / upgrade / uninstall [--purge]— three-stage lifecycle (install never touches network; validate is connectivity-only; run does the strict checks).deeptrade strategy list / run / history / report <run_id>— Live EVA-themed dashboard (header / progress / events / analysis / footer);--no-dashboardfor non-tty.- Plugin api_version "1":
StrategyPluginProtocol,StrategyContext,StrategyEventenum, PydanticPluginMetadata(YAML).
Core services
Database— single-process, single-writer DuckDB; reentrant write lock; short transactions.SecretStore— keyring-first, plaintext fallback with explicit warning.TushareClient— token-bucket rate limit, tenacity retries, 4 cache classes (static / trade_day_immutable / trade_day_mutable / hot_or_anns),data_completeness(final / intraday) for intraday isolation.DeepSeekClient— JSON-mode + Pydantic double-validate; profile triple (fast/balanced/quality); stage-levelmax_output_tokens(R1/R2 default 32k, final_ranking 8k); never passestools/tool_choice/functions.StrategyRunner— status state machine (running → success / failed / partial_failed / cancelled); KeyboardInterrupt → cancelled; anyVALIDATION_FAILEDevent flips success → partial_failed.setup_logging()— stderr handler + rotating file under~/.deeptrade/logs/.
Built-in strategy: limit-up-board
- Step 0
resolve_trade_date()— most-recent-closed trade day;app.close_afterconfigurable threshold;--allow-intradayopt-in. - Step 1 data assembly — main board filter (Q2: SSE/SZSE only), ST/suspended exclusion, three-tier
sector_strengthfallback (limit_cpt_list→lu_desc_aggregation→industry_fallback), normalized prompt fields (亿/万 + 2dp) while DB keeps raw. - Step 2 R1 —
plan_r1_batches()with input + output token DUAL budget (F5);EvidenceItem.unitmandatory. - Step 4 R2 — single batch by default; auto multi-batch when input exceeds budget.
- Step 4.5
final_ranking— only triggered on multi-batch R2;select_finalists()keeps top + watchlist + boundary avoid samples. - Step 5 reports — 5-file dump under
~/.deeptrade/reports/<run_id>/; banner stack rules (red for partial_failed/failed/cancelled, yellow for INTRADAY MODE, both stack).
Fixed (v0.3 review round 2 → v0.3.1)
- F1
limit_stepwas duplicated in optional table → removed; required-only. - F2
limit_cpt_list✅mismatched optional status → unified tooptional+fallbackeverywhere;sector_strength_sourcelabel propagated to prompts. - F3 Fast profile R2
thinking: truecontradicted "all off" docs → flipped tofalse. - F4
--allow-intradaywould have polluted EOD caches → addeddata_completenesscolumn; daily-mode reader rejects intraday-cached rows; UI/report INTRADAY MODE banner. - F5 Default
max_output_tokens=8192would truncate R1 → moved to per-stage profile (R1/R2 32k, final_ranking 8k); R1 evidence cap 8 → 4; rationale length-capped via prompt. - S1 Migrations are now the sole DDL source;
tablesonly declares names + purge flag. - S2
app.close_afterconfigurable (default 18:00); install never touches tushare. - S3
strategy_runs.statusCHECK constraint removed (DuckDB ALTER limitation); validation moved to Pydantic layer. - S4
row_count=0is a legal outcome (extreme tape day); fallback predicate accepts it. - S5
final_rankingonly ranks finalists; non-finalists keepbatch_local_rank; both surface inround2_predictions.json.
Engineering
- 163 pytest tests passing; ruff + mypy clean.
- 1 real concurrency bug fixed during development:
Database.transaction()+execute()self-deadlock with non-reentrantthreading.Lock→ switched tothreading.RLock. - 1 pandas 2.x compat fix:
pd.read_json(json_str)deprecated → wrap inio.StringIO().
Known design debts (planned for v0.4)
- D1 Replace
configure(ctx) -> dictwith schema-drivenget_param_schema() -> type[BaseModel]; CLI auto-renders questionary forms; non-interactive mode supports--params-file. - D2 Per-API
probesin plugin metadata;validatebecomes two layers (validate_connectivity+validate_required_apis).