133 Commits
Author SHA1 Message Date
Simon Lin cf58477fb7 fix: 历史日期调用时跳过实时请求及快照写入,明确说明实时数据已略去,不输出由其生成的金额或方向信号。可选缓存先按分析日截断,… (#121) 2026-10-26 07:10:23 +08:00
Simon Lin e3981af1e4 chore: release v0.5.21 v0.5.21 2026-10-11 21:38:16 +08:00
Simon Lin df0b63bdcf fix: 批量入库只认新浪 2020-03-13 那一批;三表工具拒收空日期,重试也补空日期
- 别的日期多期共用同一入库时间(逾期公司一次补发几期)照旧等入库时间放行,不按截止日提前露出。
- curr_date 传空字符串时三张表工具直接要求重传(数据层收到空日期会整段不筛)。
- 缺数据重试对 curr_date 为 None / 空字符串的旧任务也按任务分析日补上。
2026-10-11 21:38:16 +08:00
Simon Lin 763e43f855 fix: 识别新浪 2020-03-13 批量入库时间;升级前的缺数据重试补上 curr_date
- 同一入库时间被 3 期及以上共用视为批量入库:仍可放行,但不算原始入库时间,这些期按法定
  截止日判断。原来茅台 2019 年前三季的报表要到 2020-03-14 才放行(复核发现)。
- 公告日不晚于报告期的当作没有(上市前数据有写成报告期当天的)。
- 重试升级前记下、没有 curr_date 的财报调用时按任务分析日补上,结果仍记到原任务。
2026-10-11 21:26:49 +08:00
Simon Lin 067300364e fix: 财报公布判断加入入库时间,逾期披露不再提前露出;三表工具 curr_date 必填
- 已公布 = 公告日早于分析日,或入库时间(北京时间)早于分析日当天;两个日期都晚于
  报告期一年以上(原始日期已被改写丢失)时才按法定截止日判断。原来「截止日或公告日」
  会让逾期披露的公司(300630 的 2023 年报 2024-07-05 才发)截止日一过就露出来。
- get_balance_sheet / get_cashflow / get_income_statement 的 curr_date 改为必填,提示词写明要传。
- 回看越早取的期数越多;按报告期从新到旧排序;同名科目不再互相覆盖;没有报告日列时不返回未筛的数据。
- 更正 start_analysis 生产者数量的注释(4 个,历史记录只清空)。
2026-10-11 21:07:28 +08:00
Simon Lin fa3e25bb08 fix(dataflows): 财报按法定披露截止日或公告日判断是否已公布,防止回看时提前看到未公布财报
#117 修好新浪财报新结构后,按分析日筛选仍只比报告期:分析日 07-15 会拿到
8 月才公布的半年报。现在要求法定披露截止日已过(严格晚于),或新浪给的公告日
不晚于分析日。较早报告期的公告日是次年同期再刊出的日期,只作放行条件、
不作排除条件,也不放进返回表里。
2026-10-11 20:37:31 +08:00
Simon Lin 05209a17a0 Merge pull request #120 from ontheway06/fix/web-custom-model-id 2026-10-11 20:28:50 +08:00
Simon Lin a3d1ee7b29 Merge pull request #117 from ontheway06/fix/sina-financial-report-structure 2026-10-11 20:28:50 +08:00
Simon Lin ef1b180121 docs: README 表格标签列不换行,避免被挤成竖排 2026-10-10 21:33:04 +08:00
Simon Lin 30dd79e141 docs: 删除 README 求职段落 2026-10-10 20:44:52 +08:00
Simon Lin 7fc142e5a6 docs: 求职 → 看机会 · Open to Opportunities,中英双语统一作者表述 2026-10-07 14:40:40 +13:00
ontheway06 a4a69c4486 fix(web): 模型校验移到 start_analysis 的唯一消费点
上一个提交只在侧栏「开始分析」按钮里拦了「模型 ID 没填」,但 `start_analysis` 有
5 个生产者——侧栏「开始分析」、侧栏「未完成任务」续跑、历史记录、报告页「重新分析」、
错误页「继续未完成任务」。走其余任一条路径校验都被绕过,请求带着空模型发了出去:

    Error code: 400 - The supported API model names are deepseek-flash,
    deepseek-v4-pro, but you passed .

复现:深模型选「Custom model ID」且留空(`deep_model_idx: 3`),从「未完成任务」
续跑 → 请求里的 model 是空串。

改动:

- 校验移到 `web/app.py` 取出 `start_analysis` 之后、起线程之前——那是唯一的消费点,
  5 条路径都要过这里。
- `_validate_model_selection()` → `validate_model_selection(config=None)`:传入真正
  要发出去的 config 就校验最终值;不传则退回读 session_state,供侧栏按钮做即时反馈。
  两处调用同一个函数,没有重复逻辑。
- `_build_config()` 改为只算一次并复用(此前内联在 `run_analysis_in_thread(...)` 里),
  校验的就是即将发出去的那份配置。

测试:`tests/test_web_model_selection.py` 增至 14 项,新增「显式 config 校验」用例,
以及一条结构性回归——断言 app.py 在取出 start_analysis 之后、起线程之前调用校验,
防止以后有人把这道关卡挪回按钮里。

全量测试:525 passed, 14 skipped。
2026-10-05 17:03:04 +08:00
ontheway06 0f3341269a fix(web): 「Custom model ID」不再被当成模型名发给 API
`MODEL_OPTIONS` 里每个供应商都带一条哨兵项 `("Custom model ID", "custom")`,含义是
"自己填模型 ID"。CLI 会就此追问输入(`cli/utils.py::_select_model`),Web 侧栏却把
这个字符串直接当成模型名用了,于是用户拿到的是一句和界面对不上号的错误:

    Error code: 400 - The supported API model names are deepseek-flash,
    deepseek-v4-pro, but you passed custom.

复现路径:供应商选 DeepSeek → 深模型下拉停在 index 3(正是那条哨兵)→ 开始分析。

改动:

- `_resolve_model_value()`:选中哨兵就渲染文本输入框、用它填的 ID,补上 CLI 已有的
  那一步。输入框复用 `custom_quick_model` / `custom_deep_model`——这两个键本来就在
  配置持久化清单里(`_save_llm_config` / `_load_saved_llm_config`),所以填过的 ID
  会跟着一起记住。
- `_validate_model_selection()`:开始分析前拦住"模型 ID 没填",在侧栏直接给出可读
  提示,而不是让它变成服务端一句 400——模型配置在折叠面板里,用户看不到任何反馈。

测试:新增 `tests/test_web_model_selection.py`(9 项),含一条遍历全部供应商、断言
任何哨兵值都不能原样流过的回归用例,以及事故现场(deepseek deep index 3)的定点用例。

全量测试:520 passed, 14 skipped。
2026-10-05 16:57:22 +08:00
ontheway06 4c186c56f9 fix(dataflows): 适配新浪财报接口的新返回结构
新浪已把财经报表结果从 result.data.<fzb|lrb|llb>(行列表)改为
result.data.report_list{报告期: {..., data: [{item_title, item_value}]}}。

旧解析只认前者,遇到新结构时返回空 DataFrame,三个取数函数于是返回
"No ... data found" 这样的正常字符串而非异常——上游 Agent 无法区分
"真的没有财报"和"解析失败",会把"该股没有财报"当成事实写进报告。

- 同时兼容新旧两种结构,上游再改版也不会退化成空表
- 新增 tests/test_sina_financial_report_shape.py 覆盖新结构、旧结构、
  三个报表函数,以及未知结构的降级行为

实测 600519(curr_date=2026-09-30):资产负债表 48 → 9927 字符,
利润表 51 → 6251 字符,现金流量表 44 → 6339 字符。
全量测试:515 passed, 14 skipped。

Refs #116
2026-10-05 16:12:28 +08:00
Simon Lin b0594a4f2b docs: add concise open-to-work notice 2026-09-25 17:50:18 +08:00
Simon Lin d393981690 fix: bound external calls for v0.5.20 v0.5.20 2026-09-21 08:29:51 +08:00
Simon Lin 7747b6ddf8 fix: harden LLM resilience for v0.5.19 v0.5.19 2026-09-21 06:49:48 +08:00
Simon Lin dd35ce2cdb Merge pull request #100 from k176060444-lgtm/fix/llm-resilience
fix(llm): prevent hangs via timeout retry and streaming keep-alive
2026-09-20 21:05:01 +08:00
Simon Lin 38f5178aa9 Merge pull request #112 from FelixWang119/feat/glm-catalog-5.3
feat(glm): 模型表补充 GLM-5.3 / 5.3-Flash / 5.2
2026-09-20 20:56:23 +08:00
Simon Lin 926de6e630 chore: release v0.5.18 — 缺失数据追踪 / 端点统一 / 辩论去重(4 PR + 1 issue)
合并 #107(缺失数据追踪与重试,langgraph 抬到 >=1.0)、#111(同花顺 pandas 3.x
取空、百度风控 403 被谎报成「无概念板块」)、#109(辩论历史去重与滚动压缩)、
#103(DeepSeek V4 输出预算挂 capabilities),并修 #113(CLI 与客户端兜底连到
不同站点,glm / qwen 统一到国内站)。

两处不向后兼容:langgraph 最低版本、glm/qwen 默认端点。

全量 422 passed / 14 skipped。
v0.5.18
2026-09-20 15:58:29 +08:00
k176060444-lgtm 9782a1f576 fix(llm): prevent hangs via timeout retry and streaming keep-alive
DeepSeek V4 thinking models emit long reasoning chains that can trip the
OpenCode Go gateway's ~3min idle timeout (TauricResearch/TradingAgents#1204),
leaving the process hung. Combined with transient 5xx / read-timeouts from
unstable LLM backends, a single node failure aborts the whole run.

- forward llm_timeout so a hung request can't stall the graph silently
- retry 5xx AND read-timeouts at the app layer with exponential backoff
  (previously only 5xx was retried; a read-timeout crashed the graph)
- raise llm_timeout 120->150 and llm_max_retries 2->3
- enable streaming on opencode-go to keep the socket alive past the idle timeout
- capture reasoning_content on the streaming path so the DeepSeek round-trip
  on the next turn doesn't fail
2026-09-20 15:28:13 +08:00
Simon Lin 922db59ae5 fix(llm): CLI 与客户端兜底连到不同站点(#113)
同一批 provider 的 base URL 写在两处,v0.2.4 引入时就没对齐:

  provider   CLI(cli/utils.py)              客户端兜底(openai_client.py)
  glm        open.bigmodel.cn(国内)        api.z.ai(海外)
  qwen       dashscope.aliyuncs.com(国内)  dashscope-intl(国际)

两站都能用同一个 key 且返回同一份模型列表,所以不报错——只是从 CLI 跑走国内站、
从 Web 跑走海外站(侧栏 Base URL 默认留空时用兜底值),报告里也看不出来。
其余 provider(xai / deepseek / openrouter / ollama)两处一直是一致的。

统一到国内站,判据:这是 A 股特化 fork,README 中英文都让用户去
open.bigmodel.cn 申请 key,用户主体在国内。海外用户不受影响——国内站同样可用,
需要指定端点可用 backend_url / 侧栏 Base URL 显式覆盖。

新增 tests/test_provider_endpoint_consistency.py 钉住两处一致,以及 glm/qwen
不得回到海外站——此前这两个值零测试覆盖,所以分裂了三个版本没人发现。
比对范围用显式的 _MUST_AGREE 集合并断言「恰好相等」:只断言交集非空的话,
某一侧漏写一个 provider 会让它悄悄退出比对而测试照样绿,那正是要防的失效模式。

issue 由 @FelixWang119 提出,含精确行号与两站实测对比。
2026-09-20 15:28:09 +08:00
Simon Lin ec78d700f4 Merge pull request #109 2026-09-20 15:23:38 +08:00
Simon Lin ace9efaff1 chore: 合并提交不带 CHANGELOG,撤回一处无关的注释转义
本仓的 PR 合并提交不碰 CHANGELOG / pyproject 的 version / CLAUDE.md,这三处
由发版时单独一个 `chore: release vX.Y.Z` 提交统一改,tests/test_version_consistency.py
钉住三者同步(#93 / #94 / #96 / #101 四个合并提交均如此)。

#107 把条目写进了已发布的 `## [0.5.16]` 段——0.5.16 和 0.5.17 都已发布,这批功能
会随下一版出去,写在旧版本段里会让人以为装 0.5.16 就有 langgraph>=1.0 的要求。
内容一字未改地存档,随 v0.5.18 进 CHANGELOG。

#103 顺带把注释里的 `^deepseek-v\d` 转义成了 `\\d`(Python 注释不做转义,读者会
看到两个字面反斜杠),与该 PR 主题无关,还原。

pyproject 的 langgraph>=1.0 是 #107 的真实依赖变更(wrap_tool_call 自 LangGraph 1.0
起提供,实测 0.6.11 没有),保留;version 仍为 0.5.17 未动。
2026-09-20 15:11:14 +08:00
Simon Lin e58b94f2a2 Merge pull request #111 2026-09-20 15:00:42 +08:00
Simon Lin fbb268df5c Merge pull request #107 2026-09-20 15:00:42 +08:00
Simon Lin dc086d22db Merge pull request #103 2026-09-20 15:00:41 +08:00
Simon Lin 19dee49168 docs: remove hiring and donation sections 2026-09-17 07:53:30 +08:00
FelixWang c51504667c feat(glm): 模型表补充 GLM-5.3 / 5.3-Flash / 5.2
智谱当前 /models 实际可用(实测 api.z.ai 与 open.bigmodel.cn 返回一致):

    glm-4.5  glm-4.5-air  glm-4.6  glm-4.7  glm-5  glm-5-turbo
    glm-5.1  glm-5.2  glm-5.3  glm-5.3-flash

原表 quick 档到 glm-5 为止、deep 档到 glm-5.1 为止,5.2 / 5.3 已上线但未收录,
用户在 Web 侧栏和 CLI 里都选不到。

本次补齐:

    quick: glm-5.3-flash / glm-5.3 / glm-5 / glm-4.7
    deep:  glm-5.3 / glm-5.2 / glm-5.1 / glm-5

低成本项(glm-5 / glm-4.7)保留作为回退,`Custom model ID` 仍在末尾。
纯增补,不改任何既有选项的语义。
2026-09-16 11:20:49 +08:00
FelixWang a13f336e03 fix(a_stock): 概念板块把百度风控 403 谎报成「该股无概念板块」
百度 PAE `getrelatedblock` 被风控时返回 HTTP 403 +:

    {"ResultCode": 0(整数), "Result": {"code": 403, "isCaptchaEnabled": true,
                                       "msg": "hit risk"}}

外层 `ResultCode` 是**整数** 0,而代码里是 `str(d.get("ResultCode", -1)) != "0"`,
整数 0 正好通过检查,随后 `result.get(code, [])` 取空,落到
`return f"No concept/block data for {code}"`。

后果:一次「取数被拦」被当成「该股确实没有概念板块」这个事实喂给模型,
且报告里完全看不出异常。现在单独识别 `Result.code == 403` 并如实报出失败原因。

顺带把手工拼接的 query string 换成 `params=`(原写法把 code 直接插进 URL,
若 code 含需转义字符会拼出坏 URL)。

补充实测:该接口对 python-requests 的 TLS 指纹做风控——同一时刻 curl
(编码 / 不编码均可)与 curl_cffi `impersonate=chrome` 都返回 200,
而 requests 连续 6/6 返回 403。要稳定取数需要浏览器指纹伪装,
这属于是否新增 curl_cffi 直接依赖的决策,本 PR 不擅自引入。
2026-09-16 11:20:49 +08:00
FelixWang 2444646ac9 fix(a_stock): 同花顺一致预期在 pandas 3.x 下必然取空
`_ths_eps_forecast` 把 HTML 文本直接传给 `pd.read_html`。pandas 3.x 起
`read_html` 的 `io` 参数只接受路径 / URL / 文件对象,字符串会被当作**路径**,
于是必然抛 FileNotFoundError;异常消息里还带着整页 133KB HTML,把日志刷满。

实测 pandas 3.0.5:

    pd.read_html('<table>...</table>')                  -> FileNotFoundError
    pd.read_html(io.StringIO('<table>...</table>'))     -> OK

影响范围:`get_fundamentals` 的一致预期 EPS 段和 `get_profit_forecast` 双双失效。
用 io.StringIO 包装后实测可正常取到 002008 的 2026-2028 三年一致预期(14 家机构)。

pyproject 声明的是 `pandas>=2.3.0`,pandas 3.x 在允许区间内,属于必现回归。
2026-09-16 11:20:49 +08:00
sc 13a9a21af7 refactor(agents): dedupe last-argument injection in debate and risk nodes 2026-09-08 21:47:19 +08:00
sc f9b5946d22 feat(agents): add compact_history for rolling-window debate compaction 2026-09-08 21:47:18 +08:00
Codex ac4de230f8 feat(report): warn when exported data is incomplete 2026-09-08 08:46:00 +00:00
Codex 2dad8afffc feat(data): track and retry missing data tasks 2026-09-08 08:43:17 +00:00
Simon LinandClaude Fable 5.1 f1bd10f946 chore: release v0.5.17 — LLM 配置持久化落到 ~/.tradingagents 并修订阅覆盖的恢复(#96 补修)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
v0.5.17
2026-09-05 10:02:23 +08:00
WenhuaXia 7654e2304e feat(web): 持久化 LLM 模型配置,重开标签页不再回到默认 provider (#96)
Web UI 的 provider / quick·deep 模型 / base_url / subscription_scope / 自定义模型 ID 现在写入 .llm_config.json,页面加载时用 setdefault 恢复(不覆盖当次修改);写失败只记 warning 不打断分析。

Contributed by @WenhuaXia.
2026-09-05 09:56:04 +08:00
Simon Lin 119fa8da90 docs: update AI role focus 2026-09-04 16:32:31 +08:00
Simon LinandClaude Fable 5.1 4b9d16f10d chore: release v0.5.16
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
v0.5.16
2026-09-02 08:02:33 +08:00
Simon LinandClaude Fable 5.1 dc737a09fb fix: 只传部分 config 时 TradingAgentsGraph 直接 KeyError(#101)
README 快速开始的示例只给 4 个键,而 __init__ 里 `config or DEFAULT_CONFIG` 是整体替换,
紧接着 os.makedirs(config["data_cache_dir"]) 就抛 KeyError —— 照 README 粘贴即崩。

- 新增 merge_config():传入的 config 当覆盖项与 DEFAULT_CONFIG 浅合并,
  与 dataflows 层 set_config() 的合并语义对齐;传完整 config 的用户行为不变
- README 中英文说清 config 是传给 TradingAgentsGraph(config=...) 的字典、不是仓库里的文件,
  以及这段代码放在哪运行
- 根目录 main.py 从上游残留的 NVDA/yfinance 示例改成与 README 一致的 A 股可运行示例
- 新增 tests/test_partial_config.py

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 08:02:33 +08:00
Simon Lin 22f91015dc docs: add author career opportunities note 2026-09-01 14:21:57 +08:00
k176060444-lgtm 3563c10d99 fix(llm): bound DeepSeek V4 output via model capabilities
Capped only explicitly identified DeepSeek V4 thinking models at 8192
tokens (reasoning + content) via capabilities.py, per PR #100 maintainer
review. Follow-up to #100 (which removed the contested global
DEFAULT_CONFIG max_tokens=8192 and kept it at None / provider-native).
Upstream #1204: uncapped V4 reasoning chains can trip the OpenCode Go /
OpenAI-compatible gateway ~3 min idle timeout.

- Add default_max_tokens to ModelCapabilities and
  _DEEPSEEK_V4_DEFAULT_MAX_TOKENS = 8192 (single source; matches
  anthropic_client _THIRD_PARTY_DEFAULT_MAX_TOKENS = 8192 provenance,
  #91 report-truncated feedback). Only DEEPSEEK_THINKING carries it.
- OpenAIClient.get_llm fills max_tokens from capabilities only when the
  caller did not already set one (explicit TRADINGAGENTS_MAX_TOKENS /
  role_llms max_tokens wins). Matcher reuses the existing strict
  DeepSeek V4 pattern ^deepseek-v4(?:$|[.-]) — V3.x, Claude/OpenAI/
  Gemini/MiniMax/unknown models stay at None.
- Tests cover V4 vs non-V4, V3.x non-match, explicit-override wins,
  and provider-kwargs -> llm propagation plus capability regression.

No change to DEFAULT_CONFIG global None, timeout/retry/streaming, or
reasoning_content capture (those remain in #100).
2026-08-21 15:10:50 +08:00
Simon Lin ed778c51ee chore: release v0.5.15 v0.5.15 2026-08-19 15:50:46 +08:00
KKandk176060444-lgtm 8db46605b3 feat: support per-role api_key in role_llms for multi-gateway setups (#94)
* feat: support per-role api_key in role_llms for multi-gateway setups

* test: add per-role api_key precedence tests for openai_compatible provider

---------

Co-authored-by: k176060444-lgtm <k176060444-lgtm@users.noreply.github.com>
2026-08-19 15:43:12 +08:00
Simon Lin 0badc3340c fix: 连字符分隔符被当成构词 / 版本号三处不同步(codex 第九轮)
1. `最终评级:**Sell**- 退出` 里的 - 只是分隔符,却被当成"会延续成更长的词"判否,
   parse_rating 静默返回 Hold,记录下错误的决策。
   判据再补一层:连字符要看后面——跟字母/数字是构词(Sell-off / Buy-side 拒),
   跟空白或其它字符只是分隔符(Sell- 退出 收)。覆盖矩阵扩到 27 例。

2. 上一版改了 pyproject 和 CHANGELOG,漏了 CLAUDE.md 的「当前版本」行,后续 agent
   和发版流程会读到旧版本。新增 test_version_consistency.py,三处不一致直接失败,
   不再靠人记得。

测试:新增 4 例,369 passed / 13 skipped / 0 failed。→ v0.5.14
v0.5.14
2026-08-09 15:30:44 +12:00
Simon Lin 1edf4be27b fix: 加粗+连字符仍被判成评级(正则回溯,codex 第八轮)
v0.5.12 把 markdown 收尾星号写成 \*{0,2}(?!词字符)——先消耗再判断会回溯:
`建议:**Sell**-off risk remains elevated` 里 \*{0,2} 先吃掉 **,被 - 判否后退一步
只吃一个 *,剩下那个 * 恰好满足边界,于是又判成 Sell。正好把这条规则本来要挡的
连字符散文放了回来。

改成把星号放进前瞻内部((?!\*{0,2}词字符))——"后面不能是(0~2 个星号 + 词字符)"
没有可回溯的余地。`最终评级:**Sell**` 不受影响。

⚠️ 刻意不用占有量词 (?>...) / *+:那是 Python 3.11+ 特性,而本项目声明
requires-python = ">=3.10",3.10 用户会在导入时就 re.error——比逻辑 bug 更硬的破坏。
新增守卫用例直接检查编译后的 pattern。

覆盖矩阵扩到 25 例。测试:新增 4 例,365 passed / 13 skipped / 0 failed。→ v0.5.13
2026-08-09 15:23:53 +12:00
Simon Lin be38f469d8 refactor: 评级边界与 BESTIP 保护改用正确抽象(codex 第七轮)
这两块前后各被修了三轮"修了又漏",问题不在某个字符,在抽象选错了。

1. 评级边界:枚举标点 → 「不能延续成更长的词」
   第七轮指出 v0.5.11 收紧后又误伤 `最终评级:Buy(基于风险收益比)`——枚举的是
   收尾标点,没算开括号,合法评级被静默改写成 Hold。
   三轮轨迹:无边界→`Buyer interest`判Buy;(?![A-Za-z])→`Sell-off`判Sell;
   枚举收尾标点→`Buy(...)`反被判Hold。
   枚举允许的标点是填不完的坑。改成只问:紧跟其后的字符会不会让它变成另一个词
   ([A-Za-z0-9_-])?会就不算评级,中文/括号/标点/空白/行尾一律放行。
   新增 22 例参数化矩阵,并写明改这条规则必须整张过一遍。

2. BESTIP 保护:手动调还原 → 上下文管理器
   原来在两处手动调 _restore_bestip(),再加一条提前返回就会漏,而漏掉的后果是
   静默留下一台死服务器。改成 with _preserve_mootdx_bestip() as keep:选中时
   调 keep() 表示这次覆写是想要的,其余每条退出路径(含异常)自动还原。
   _get_mootdx_client 114 → 82 行。

端到端实测:真实探测 38 台全失败后,用户配置的 218.6.170.47 完好无损。
测试:新增 21 例,361 passed / 13 skipped / 0 failed。版本 → v0.5.12
2026-08-09 15:16:18 +12:00
Simon Lin f5cd0ef67f fix: 上一版那两个修复本身都没修对(codex 第六轮)
1. BESTIP 快照取到的是空默认值 → v0.5.10 的修复无效
   新进程里 mootdx 要等 BaseQuotes.__init__ 调 config.setup() 之后才把持久化的值
   读进来,此前 get("BESTIP") 是模块默认空值。快照到空值后,全部探测失败时的
   "还原"反而把用户真实配置抹成空——比不还原更糟。
   实测 0.11.7:setup 前 {'HQ': ''},setup 后 {'HQ': ['218.6.170.47', 7709]}。
   现在快照前先显式 setup();端到端验证真实探测 38 台全失败后用户配置完好。
   测试也改为复刻真实构造函数语义(旧假件不调 setup,抓不到这条)。

2. 评级边界仍放行连字符散文 → v0.5.10 的修复不完整
   (?![A-Za-z]) 只挡紧跟字母的情况,连字符和数字后缀照样过关:
   `建议:Sell-off risk remains elevated` → Sell、`最终评级:Buy2024` → Buy。
   改为要求评级词后是真正的结束(行尾/空白/中文/常见标点),并先吃掉 markdown
   收尾星号,`最终评级:**Sell**` 不受影响。

测试:新增 2 例,340 passed / 13 skipped / 0 failed。版本 → v0.5.11
2026-08-09 15:05:04 +12:00
Simon Lin cbabc02fc9 fix: 探测覆写 mootdx 配置 / 中英混排评级误判(codex 第五轮)
两处都是新增行为自己带出来的副作用。

1. 探测服务器会覆写用户配置的 mootdx 服务器
   StdQuotes.__init__ 里有 config.set('BESTIP', {'HQ': self.server})——每建一次
   带 server 的 client 都会持久化写进配置文件。v0.5.6 把候选表扩到 38 台后,逐台
   探测等于一路覆写用户原本配好的服务器,最后留下最后一台失败的:裸 factory 兜底
   (读 BESTIP)再也救不回来,同机上其它用 mootdx 的程序也被连累。
   现在探测前快照 BESTIP,兜底前与全失败时都还原。

2. 中文标签后跟英文散文被误判成评级
   _CN_LABEL_EN_RE 缺词边界:`最终评级:Buyer interest remains weak` → Buy,
   `建议:Selling pressure is high` → Sell。误判会写进记忆日志再污染绩效统计,
   报告里完全看不出来。已加 (?![A-Za-z]),正常中英混排不受影响。

测试:新增 4 例,338 passed / 13 skipped / 0 failed。版本 → v0.5.10
2026-08-09 14:55:30 +12:00
Simon Lin 3968973958 fix: 未完成任务索引写失败不再静默吞掉(承接 #93)
#93 的重试 + 降级思路是对的,但最后一层 `except OSError: pass` 完全静默——
索引一直写不进去时用户永远不会知道,「未完成任务」列表长期不更新也无从排查。

改为打一条 warning:仍然不往上抛(不中断分析,读取端容错、下次会重建),
但至少可见。本项目对静默吞错一贯从严。
2026-08-09 14:50:12 +12:00