Commit Graph
307 Commits
Author SHA1 Message Date
GitHub 8c35a864e8 release: v1.20.9 — 历史资金流修复(#52):当日主力净额盘中可取 + main_net_inflow 列 2026-08-26 15:01:50 +08:00
GitHub 574ffdd2a4 fix(client): 历史资金流当日行全零 + 主力净额列缺失(issue #52)
三个根因(全部实测核实):
1. Category 22 直连接口为虚构协议——52 台已知服务器中 46 台可达的
   全部仅回 2 字节空包,从未成功过;移除死代码与臆造解析格式。
2. 历史逐笔接口当日数据要收盘清算后才有,日 K 盘中已含当日 bar,
   故 start=0 的最新一行恒为全 0——当日 bar 改走当日实时逐笔接口。
3. main_net_inflow 此前仅为 dataclass property,asdict 静默丢弃,
   返回 DataFrame 无主力净额列——新增 _fund_flow_df_with_net 物化
   (history 紧随 date 列、当日快照放首列)。

sync + async 双客户端同步修改;更新示例与三份文档;重写/新增回归
测试(当日实时逐笔路径、主力净额列断言)。
2026-08-26 15:00:53 +08:00
GitHub 7c9e19de93 release: v1.20.8 — 信号雷达:一键扫描全部已保存策略买卖信号 2026-08-21 16:57:16 +08:00
GitHub 9336273f17 feat(web): 信号雷达——一键扫描全部已保存策略的最近买卖信号
新增导航页 /signals:把策略库 single/portfolio/multi 策略统一展开成
"策略×标的"子任务,按标的去重取最近 800 根 K 线,用与回测引擎同口径的
逐 bar 信号流程(含仓位跟踪)判断最近 N 根(窗口 1/3/5/10 可选,默认 5)
的买/卖信号,汇总卡片 + 筛选 tab + 明细表展示;上次结果缓存 localStorage。

- 后端 signal_scan.py(展开/去重取数/信号评估/汇总)+ POST /backtest/signal-scan/run/async
- 只扫信号不重跑回测,不改写策略库业绩快照;单行失败(未知策略/停牌/参数非法)不中断整批
- normalize_symbol 按代码段纠正历史错标市场前缀(与前端 detectMarket 同规则)
- 新增 20 个单测(含与回测引擎成交序列一致性对照),全套 1030 个单测通过
2026-08-21 16:57:12 +08:00
GitHub 67a5e1d08a release: v1.20.7 — /bars MIN_1 时间修复(#49)+ 寻优参数语义约束(#39)+ 依赖安全升级 2026-08-19 19:06:59 +08:00
毛利哥 8eaafb93ee Merge pull request #51 from handsomejustin/fix/issue-39-param-order-constraints
fix(backtest): 策略参数加跨语义约束,杜绝寻优选出快慢倒挂组合(issue #39)
2026-08-19 19:05:42 +08:00
GitHub 1d3d45828d style: ruff format 调整新增测试换行 2026-08-19 19:00:42 +08:00
GitHub 3180ab3002 fix(backtest): 策略参数加跨语义约束,杜绝寻优选出快慢倒挂组合(issue #39)
寻优器网格探索不区分语义:ma_cross 的 fast∈[5..60] × slow∈[10..250]
笛卡尔积包含 {"fast":30,"slow":20} 这类倒挂组合,倒挂的双均线交叉
本质是反向策略,回测成绩可能反而突出,从而被选为"最优参数"展示
(即 issue #39 截图中 {"fast":30,"slow":20} 的来源)。presets.py 的
注释"快线<慢线才有意义;去重无效组合"早已写下意图但从未实现。

根因是参数校验只有单参数 min/max,缺跨参数语义约束:

- ParametrizedStrategy 新增 param_constraints 类属性 [(a,b), ...]
  表示要求 a<b,在 __init__ 解析后统一校验,报错带中文标签;
  语义约束不受 skip_bounds 影响(寻优跳过的只是数值边界)
- 7 个策略声明约束:ma_cross/ema_cross(fast<slow)、macd
  (short<long)、triple_ma(short<mid<long)、rsi_reversal/cci/
  wr_reversal(oversold<overbought)
- 寻优器 build 阶段的 ValueError 降为 info 级跳过(无堆栈噪音),
  回测异常仍走 warning;预设网格 36 组合 → 有效 25 个
- 新增 10 个回归测试(旧代码全失败、新代码全通过)
2026-08-19 18:55:48 +08:00
毛利哥 cc17ab650d Merge pull request #50 from handsomejustin/fix/issue-49-bars-min1-daily-plus
fix(web): /bars MIN_1 被误判为日线,datetime 归一化为 date 00:00:00(issue #49)
2026-08-19 18:26:49 +08:00
GitHub fc63873717 style: 拆行修复 ruff E501(test_web_api.py 超长行) 2026-08-19 18:22:49 +08:00
GitHub bcaee1404d fix(web): /bars MIN_1 被误判为日线,datetime 归一化为 date 00:00:00(issue #49)
KlineCategory 枚举值不按周期长短排序(MIN_1=7、MIN_3=8 均大于 DAY=4),
/bars 的 MAC 路径用 int(cat) >= int(KlineCategory.DAY) 判定"日线及以上",
把 1 分钟线误判为日线,_normalize_mac_df 因此将 datetime 截断为 00:00:00
并把列名改为 date。回退 TdxClient 路径与 MAC symbol_bar 均为显式判定,
仅此一处用整数比较,故只有 MAC 路径复现。

改为 _is_daily_plus() 查表判定(复用 _df._CATEGORY_MINUTES,与回退路径
同一判定源),保证两条路径 date/datetime 语义一致。新增表驱动单测 +
端点级回归测试(假 MAC 客户端,在未修复代码上失败、修复后通过)。
2026-08-19 18:19:07 +08:00
毛利哥 e533b705d4 Merge pull request #48 from handsomejustin/fix/dependabot-postcss-nanoid
fix(web): 升级 postcss/nanoid 修复 Dependabot 安全告警
2026-08-14 17:35:29 +08:00
GitHub 01a9823ed4 fix(web): 升级 postcss/nanoid 修复 Dependabot 安全告警(alerts #2 #3 #5)
- postcss 8.5.16 -> 8.5.26:修复路径遍历读取任意 .map 文件(GHSA 高危)及后续不完整修复
- nanoid 3.3.15 -> 3.3.18:修复非安全生成器负数 size 死循环(高危)

均为 vite / @vue/compiler-sfc 的 semver 兼容间接依赖,仅更新 lockfile;
npm run build 验证通过。
2026-08-14 17:30:13 +08:00
毛利哥 513ee15c83 Merge pull request #46 from handsomejustin/feat/issue-43-bars-mac-adjust
feat(web): /bars 迁移到 MacClient + 支持复权(issue #43)
2026-08-05 16:44:00 +08:00
GitHub 5a3ad15477 feat(web): /bars 迁移到 MacClient + 支持复权(issue #43)
Web 获取 K 线此前用 AsyncTdxClient.get_security_bars(标准协议不支持复权),
导致 REST API 无法取前复权/后复权数据。将 /bars(个股 K 线)迁移到
AsyncMacClient.get_stock_kline(MAC 协议,支持 NONE/QFQ/HFQ + QFQ 负价兜底),
保持旧输出契约不变(日线 date 列、分钟线 datetime 列、OHLC 顺序、无 float_shares),
新增 adjust 参数(默认 QFQ),MAC 主机不可用时自动回退标准 TdxClient。

⚠️ 半破坏性变更:/bars 默认复权从"不复权"改为 QFQ(前复权)。老调用方若需
不复权请显式传 ?adjust=NONE。输出 DataFrame 列名/顺序/字段与旧版完全一致
(_normalize_mac_df 规整),仅价格数值因复权变化。

改动:
- bars.py:/bars 改走 mac_client.get_stock_kline(adjust=...),MAC 不可用回退
  get_security_bars(无复权 + warning);新增 _normalize_mac_df 规整输出契约;
  新增 adjust 查询参数(默认 QFQ)。
- convert.py:period_times_from_category(KlineCategory→Period 映射,显式处理
  YEAR 9→YEARLY 11、SEASON→QUARTERLY)+ adjust_from_str。
- deps.py:get_mac_client_optional(未连接返回 None,供回退判断)。
- schemas.py:AdjustEnum(OpenAPI 文档用)。

测试:新增 7 个(period_times 映射全表 + 不可映射值、adjust 转换、_normalize_mac_df
日线/分钟线/空 df)。全套 996 passed;ruff format/check + mypy 改动文件零错误。

不在本次范围:/bars/index(指数 K 线,MAC 另一套接口)、/minute、/transaction*
(MacClient tick_chart 语义不同,暂不迁移)。
2026-08-05 16:37:10 +08:00
毛利哥 e5e8f75516 Merge pull request #45 from handsomejustin/fix/issue-41-fund-flow-failover
fix(client): get_history_fund_flow 加空数据故障转移(issue #41)
2026-08-05 16:02:53 +08:00
GitHub 60bd0586f9 style(tests): ruff format 修复 patch.object 参数换行(CI ruff format --check 失败) 2026-08-05 15:58:00 +08:00
GitHub 0490f67a16 fix(client): get_history_fund_flow 加空数据故障转移(issue #41)
用户反馈 get_history_fund_flow(SH, "600519") 返回空 DataFrame,日志显示
"K线响应为空(声称 800 条但首条即解析失败...)"。排查定位:当前 host 对
常见标的也返回 ret_count 撒谎的空 body,但资金流兼容回退路径(直连空 →
拉 K 线 + 历史逐笔重算)未接入 v1.20.4 的空数据故障转移,"服务器回包正常
但内容是假的空" 既非 TdxConnectionError 也不触发换台,用户卡在坏服务器上。

修复:get_history_fund_flow(sync+async)当前 host 直连与 K 线回退均空时,
按延迟顺序逐台实测找首台返回有效数据的服务器(与 get_security_bars/
get_index_bars 同源逻辑)。因资金流获取涉及多命令,无法用单 cmd 复用泛化版
_find_host_returning_data,故内联 _fund_flow_failover:每台候选上跑完整
_fetch_fund_flow_records(原直连+K线回退逻辑抽出成独立方法),返回首台非空
结果。全空返回空 DataFrame(不 raise,区分"真无历史数据"与"服务器缺数据")。

测试:新增 6 个(4 sync + 2 async)。全套 989 passed;ruff/mypy 改动文件零错误。
2026-08-05 15:53:49 +08:00
Justin Gu 3b7ce97f2a chore(release): v1.20.4 服务器健康分引擎 + K线空数据故障转移
版本包含(PR #37):
- 新增 _health.py 服务器健康分引擎(降权/冷却/恢复)
- get_index_bars/get_security_bars 空数据自动逐台换台
- 8 个 client _execute 统一健康分联动
- select_best_host_*/find_working_host_* 应用 rank_by_health
- security_bars 截断日志区分空响应与末尾截断
- 26 个新增测试,CI 8/8 通过
2026-07-13 16:36:11 +08:00
毛利哥 0be4277d03 Merge pull request #37 from handsomejustin/feat/health-score-and-bars-failover
feat(reconnect): 服务器健康分引擎 + K线空数据故障转移
2026-07-13 16:21:55 +08:00
Justin Gu 1f040afabf feat(reconnect): 引入服务器健康分引擎 + K线空数据故障转移
彻底解决通达信服务器"跳来跳去"问题:

1. 新增 _health.py 健康分引擎:失败乘性降权(×0.5)、连续失败3次进
   120s 冷却、成功加性恢复(+0.2)。rank_by_health 按 latency/score
   (有效延迟)重排,冷却中的剔除。全健康时恒等映射,对既有测试零影响。

2. get_index_bars/get_security_bars 空数据时自动逐台换台(此前直接
   返回空 DataFrame,是日志"指数K线响应在第1/800条处被截断"后用户拿
   不到数据的根因)。复用泛化后的 _find_host_returning_data。

3. select_best_host_*/find_working_host_* 应用 rank_by_health 重排;
   空数据验证失败/异常时调 record_failure,命中调 record_success。

4. 8 个 _execute(A股/MAC/EX/MAC-EX × sync/async)统一注入健康分记录:
   成功 record_success、连接失败 record_failure。

5. security_bars 截断日志区分"首条即空(服务器无数据)"与"末尾截断"。

测试:26 个新增(15 health + 7 failover + 4 ex-client 健康分追踪),
全量 reconnect/failover/decode 回归通过,ruff/mypy 通过。
2026-07-13 15:59:28 +08:00
毛利哥 5dd1b96818 Merge pull request #36 from handsomejustin/fix/issue-34-realtime-data-feed
feat(realtime): 新增 RealtimeDataFeed 轮询数据源(issue #34)
2026-07-13 03:58:36 +08:00
Justin Gu afcc100e35 feat(realtime): 新增 RealtimeDataFeed 轮询数据源(issue #34)
EventBus 原本是纯发布/订阅管道,库内没有任何代码调用 publish(),导致
RealtimeStrategy 订阅后无数据可收(issue #34 根因)。

新增 RealtimeDataFeed:轮询 get_stock_quotes 快照 → 封装 MarketEvent →
bus.publish,让 RealtimeStrategy 拿来就能跑。

- feed.py: async(run_async) + sync(run_sync, executor 线程避免卡 loop)
  支持 interval/dedup(去重)/sessions(交易时段过滤,默认 9:15-11:30/13:00-15:00)
  symbol key 自动拼 f"{market}{code}" 匹配 EventBus.publish
- test_realtime_feed.py: 20 个单测覆盖 publish 路径/symbol key 匹配/
  去重开关/空 df/客户端异常不崩溃/盘外不拉取/stop 退出
- engine.py + __init__.py docstring: 「API 骨架待实现」→「需配合 feed 使用」
- README: 新增实时行情轮询 Python API 段落(含 issue #34 三个坑说明),
  WebSocket 段落标注「当前未联动数据源」现状
- examples/11_realtime/: 可直接运行的异步示例

通达信协议无服务端推送,本模块是约 3 秒延迟的快照轮询近似,非逐笔 tick。

验证:ruff + mypy(strict) + pytest 全量 956 passed(+20 新增),无回归。
2026-07-12 16:56:41 +08:00
毛利哥 9e7d30f33e Merge pull request #35 from handsomejustin/fix/index-bars-empty-failover
fix(client): get_index_bars/get_security_bars 加空数据故障转移
2026-07-10 19:58:35 +08:00
Justin Gu 6b98715a14 fix(client): get_index_bars/get_security_bars 加空数据故障转移
指数/板块指数(880xxx 等)并非所有服务器都提供,延迟最低的不一定返回数据。
实测约 6/47 台可达主机对所有指数返回空 body 且不报错,from_best_host() 按纯
延迟选主会选中这种"快但空"的主机,导致 get_index_bars 返回空 DataFrame。

get_market_stat 早有 _find_host_returning_quotes 兜底,但 K 线查询缺失同类逻辑。
本提交补齐对称设计:

- 新增 _find_host_returning_bars(sync + async):照搬 _find_host_returning_quotes
  模式,复用 find_working_host_sync/_async(最多试 5 台、带持久化、异常吞掉、
  全失败回退原 host)
- get_security_bars / get_index_bars(sync + async 共 4 处)接入:空数据且
  auto_reconnect 开启时触发逐台实测,找首台返回数据的 host
- 新增 TestIndexBarsEmptyFailover(5 个测试):覆盖空数据切 host、全候选空、
  非空不触发、auto_reconnect=False 不触发、个股 K 线同链路

验证:23 个 failover 测试全绿,941 个单元测试无回归。
2026-07-10 19:37:31 +08:00
GitHub a8c035010e style(tests): ruff format 修复尾随空行(CI ruff format --check 失败) 2026-07-10 15:17:00 +08:00
GitHub d84a08b7f2 fix(backtest): v1.20.3 修复回测绩效统计两个 bug(issues #30 #31)
#31: RebalanceEngine 缺失价格导致净值假崩塌
  已持仓标的当日缺 K 线(停牌/日历错位)时 prices.get(code,0)=0,
  持仓市值记 0 → 净值单日暴跌(159915 在 20210208 缺一天,持仓 ~93%,
  净值 1.1M→91,845,全期最大回撤 -92%)。
  修复:last_known_price forward-fill,缺失日沿用最近已知收盘价。
  附带:_compute_performance 最大回撤改正值口径(与 BacktestEngine 一致)。
  验证:真实 ETF 数据 max_drawdown 24.01%(backtrader 基准 27%),
  total_return 220.56% 不变。

#30: PortfolioTracker 交易静默漏单
  apply_trades 用 trade.datetime 作 dict key、df["datetime"].to_numpy()[i]
  查找;两端类型不一致(int vs datetime64)时永不命中,交易被静默丢弃,
  净值恒定(total_return=0 但 trades 表有 PnL)。
  修复:改为按"位置索引"匹配(归一化 datetime 后查位置),类型无关。
  验证:int-trade+datetime64-df 修复前净值=100000(恒定),修复后=100289。

测试:新增 4 回归测试(未修复代码上均失败,修复后通过);
全套 936 passed;mypy 改动文件零错误;ruff 全绿。
2026-07-10 15:12:47 +08:00
GitHub 06be298c92 ci(publish): 暂时关闭 attestations 规避 OIDC 503 故障
GitHub OIDC → sigstore 服务连续 503 overflow(v1.20.2 两次 publish 失败),
非配置问题(v1.20.1 能发成功证明 trusted publishing 配置正确)。
attestations 是可选签名,关闭后上传正常进行;待服务稳定后改回 true。
2026-07-09 22:12:20 +08:00
毛利哥 471c79d5e2 Merge pull request #29 from handsomejustin/fix/v1.20.2-mypy-ci
fix(types): v1.20.2 修复 v1.20.1 引入的 CI mypy 失败
2026-07-09 20:59:58 +08:00
GitHub bc0ea064ba chore: retrigger CI (Actions queue stuck 20min) 2026-07-09 20:55:16 +08:00
GitHub a67e5588d0 fix(types): v1.20.2 修复 v1.20.1 引入的 CI mypy 失败
v1.20.1 把 BacktestResult.performance 扩大为 dict[str, float | str]
(为塞 diagnostic_warning 字符串),破坏 6 处下游消费方类型契约,
CI mypy job 转红。重构为更干净的设计:

- 诊断信息走独立的 BacktestResult.diagnostic 字段(performance.py
  PerformanceAnalyzer.diagnostic 属性 → engine 透出 → to_dict 含该字段
  → CLI 显示),performance 字典回归 dict[str, float](保留别名键)。
- optimizer.py 补 npt.NDArray 标注,消除 3 个既有 ndarray type-arg 错误。

测试:932 passed;mypy backtest/+portfolio/ 零错误;ruff 全绿。
2026-07-09 20:34:23 +08:00
毛利哥 eb51770608 Merge pull request #27 from handsomejustin/fix/v1.20.1-backtest-bugs
fix(backtest): v1.20.1 修复回测引擎 3 个 bug(issues #22 #23 #25)
2026-07-09 20:17:00 +08:00
GitHub 00b8c6374a fix(backtest): v1.20.1 修复回测引擎 3 个 bug(issues #22 #23 #25)
排查发现用户反馈的"回测统计数据缺失/异常"并非服务器连接问题,
而是回测引擎与组合优化器自身的代码缺陷:

#23: 首根 bar 访问 close[-1] 崩溃
  _SeriesAccessor 负向越界改返回 NaN(不抛 IndexError);
  BacktestEngine 新增 warmup_bars 参数跳过指标预热期。

#25-A: FactorWeightedOptimizer 权重坍缩
  n_stocks=2 且得分接近时,减最小值把低分标的权重压到 ~6e-8,
  等于单股满仓、n_stocks 被无视,进而出现持仓1只/-99.98%回撤。
  新增 _apply_weight_floor 权重下限保证入选标的都有实质权重。

#25-B: RebalanceEngine total_trades 统计错误
  total_trades = len(equity_curve)(天数)改为 len(trades_df)(真实笔数)。

#22: 绩效别名键 + 数据异常诊断
  performance dict 新增 sharpe_ratio/start_cash/end_value 别名键;
  资金曲线异常时返回 diagnostic_warning 而非静默全 0,CLI 显示提示。

文档:README 加入回测手册导航;backtest_usage.md 补充 warmup 说明。
测试:新增 6 个回归测试,更新 3 个;932 passed。
2026-07-09 20:16:12 +08:00
毛利哥 e21c4d595c Merge pull request #26 from handsomejustin/feat/fsl-indicator-strategy
feat(backtest): v4.2 新增 FSL 分水岭指标策略(单标的/组合/寻优)
2026-07-09 20:10:42 +08:00
GitHub 052119e6ae feat(backtest): v4.2 新增 FSL 分水岭指标策略(单标的/组合/寻优)
- MyTT.py: 新增 FSL(CLOSE, VOL, CAPITAL) 分水岭指标
  - SWL = (EMA(C,5)*7 + EMA(C,10)*3) / 10
  - SWS = DMA(EMA(C,12), MAX(1, 100*SUM(VOL,5)/(3*CAPITAL)))
    MIN(A,1.0) 模拟通达信 DMA(X,A) 内部钳制 A<=1 的语义
- MyTT.pyi: 补 FSL 类型 stub(CI mypy 用 .pyi 校验)
- strategies/builtin.py: 注册 FslStrategy(SWL/SWS 金叉死叉)
  capital 作为 Param 暴露流通股本输入
- strategies/presets.py: fsl 寻优网格(4 档 capital)
- tests: 注册表断言加 fsl,阈值 18→19
2026-07-09 18:42:00 +08:00
毛利哥 8bf4ffb322 Merge pull request #20 from handsomejustin/feat/auto-failover
feat: v1.20.0 服务器失败自动 ping 切换(无需手动 easy-tdx ping)
2026-07-08 18:13:03 +08:00
GitHub d0d65d64b8 feat: v1.20.0 服务器失败自动 ping 切换(无需手动 easy-tdx ping)
服务器连不上或返回空数据时,自动测速切到延迟最低的可用服务器再试,
Python API / CLI / Web API 三入口全部自动生效。

核心改动:
- _reconnect.py 新增 select_best_host_sync/async(连接失败 failover)
  和 find_working_host_sync/async(空数据逐台实测)
- 8 个 client 的 _execute 注入跨主机故障转移(复用 auto_reconnect 开关)
- get_market_stat 空数据时按延迟顺序逐台实测找返回数据的服务器
- 新增 _reconnect/_areconnect helper 收敛重建连接副本
- MacClient failover 用 save_best_mac_host(延续 v1.19.4 不污染 best_host)
- 顺手修复 test_commands_offline 未使用 import(main CI failure 根因)

测试:925 passed(新增 18 个 failover 测试),ruff/mypy 零新增错误。
2026-07-08 18:07:37 +08:00
Justin Gu 14debdb446 feat(web-ui): v1.19.7 服务器设置页面(测速+切换通达信服务器)
新增 web UI 第六个页面「服务器设置」,让用户在浏览器上:
1. 列出全部 50+ 候选服务器
2. 一键并发测速(显示延迟和可达性)
3. 点选切换到最快或可用的服务器(热重连,无需重启)

解决不同地区/运营商对通达信各服务器连通性不同的问题。

新增:
- AsyncTdxClient.reconnect_to(host) 热切换方法(持锁,关旧建新)
- GET/POST /api/v1/server/hosts|test|switch 三个端点
- ServerSettingsView.vue 页面(列表+测速+切换)
- 导航栏第六个入口

设计: 先reconnect成功再save_best_host(避免污染config),
host校验只允许候选列表内IP,不自动测速避免首屏卡顿。
2026-07-07 23:08:32 +08:00
Justin Gu c2c3e7ffb4 fix(release): v1.19.6 release.yml 先构建前端再 pip install(修复EXE丢依赖)
v1.19.5 EXE 只有 11MB,双击报 ModuleNotFoundError: No module named 'pandas'。
根因:release.yml 先 pip install -e '.[web,packaging]' 再 Build frontend,
但 force-include 要求 web-ui/dist 在 pip install 时就存在——install 阶段
dist 不存在导致 editable install 静默降级,PyInstaller 收集不到第三方包。

修复:调换步骤顺序,Build frontend 在 Install Python deps 之前。
ci.yml 上一 commit 已同样修复。
2026-07-07 22:23:30 +08:00
Justin Gu e8b088bca1 fix(ci): ci.yml test/mypy job 先构建前端再 pip install
pyproject.toml 的 force-include 要求 web-ui/dist 存在,否则 pip install -e .
报 FileNotFoundError。ci.yml 的 test 和 mypy job 此前没有构建前端步骤,
导致全部 CI 失败。

修复:test(6 矩阵)和 mypy job 在 pip install 前加 setup-node + npm ci +
npm run build。frontend job 本来就有,不变。

同时清理 pyproject.toml:恢复 force-include(之前误改成 build-hook 但
hatchling 自定义 hook 需要插件注册,过于复杂)。
2026-07-07 22:14:03 +08:00
Justin Gu 9ab093a3c7 fix(build): hatchling artifacts 让 sdist 也含 web-ui/dist
python -m build 先构建 sdist 再从 sdist 构建 wheel。sdist 默认尊重 .gitignore
不含 web-ui/dist(构建产物),导致 wheel 阶段 force-include 找不到源报
FileNotFoundError。用 [tool.hatch.build] artifacts 强制 sdist 和 wheel
都包含 web-ui/dist。

本地验证:sdist 含 4 个 dist 文件,wheel 含 4 个 web/dist 文件。
2026-07-07 20:50:48 +08:00
Justin Gu d5309454bc fix(packaging): v1.19.5 PyPI wheel 含前端 dist(pip install 后不再 404)
根因:PyPI wheel 只有 Python 包,不含前端 dist。_resolve_web_dist_dir
三级探测(env/_MEIPASS/仓库根)在 PyPI 安装环境下全失败返回 None,
StaticFiles 不挂载,GET / 返回 404。

修复:
- pyproject.toml hatchling force-include 把 web-ui/dist 映射到包内
  easy_tdx/web/dist
- publish.yml 在 python -m build 前先 npm ci && npm run build
- _resolve_web_dist_dir 新增第 4 级探测:包内 easy_tdx/web/dist

验证:本地构建 wheel 含 4 个前端文件,模拟 PyPI 安装后第 4 级探测
成功找到 dist。907 测试全过。
2026-07-07 20:46:26 +08:00
Justin Gu 3bc15e620c fix(config): v1.19.4 MAC客户端不再污染标准best_host(取不到数据根因)
根因:mac/client.py 的 MacClient.from_best_host 和 AsyncMacClient.from_best_host
调用 save_best_host(best),把选中的 MAC 协议服务器(如 121.36.248.138)
写进了全局 best_host 字段——但这个字段是标准 TDX 协议用的。之后标准
AsyncTdxClient 用 get_best_host 读到这个 MAC host,用标准协议请求 MAC
服务器,返回空 body(偏移 2 剩余 0)。web app lifespan 启动 MAC 客户端
时触发污染。

修复:
- config.py 新增独立的 best_mac_host 字段 + get_best_mac_host / save_best_mac_host
- mac/client.py 4 处改用新字段(不再碰 save_best_host / get_best_host)
- get_best_host 加交叉污染校验:缓存 host 不在标准列表里自动重置
  (自动修复已被污染的 config.json,用户无需手动操作)

测试:新增 5 个回归测试(污染检测 + MAC host 隔离),907 全过。
2026-07-07 20:36:44 +08:00
Justin Gu 032f5a40c0 fix(web): v1.19.3 K线空body不再500 + SPA路由刷新404
两个独立问题(日志证据):

1. K线空body 500:SH600519 等正常股票偶发请求时,通达信返回
   ret_count>0 但 body 完全为空(偏移2 剩余0)。v1.18.3 的容错有
   'if bars:' 条件,bars 为空时 raise → 500。移除该条件,始终 return
   (空列表让前端分页重试比 500 友好)。GetIndexBarsCmd 同改。

2. SPA fallback 缺失:/optimize /portfolio 等前端路由刷新时 404。
   子类化 StaticFiles 为 SPAStaticFiles,404 时返回 index.html。
   API 路径不受影响。

测试:更新 test_security_bars_truncated_first_record(原断言 raise,
现断言返回空列表)+ 新增空 body 回归测试。902 全过。
2026-07-07 20:13:39 +08:00
Justin Gu 6b51487477 test: 修复 test_task_runner_lru_eviction 的竞态 flaky
根因:测试用 max_workers=1 串行执行 5 个任务,断言「前 2 个被淘汰」。
但 LRU 淘汰发生在 submit() 时,淘汰的是「当时最旧的非 running 任务」。
快机器上 t0 还在 running(被跳过),实际淘汰 t1/t2;慢机器(CI windows
3.12)上 t0 已完成,淘汰 t0/t1——取决于提交速度 vs 执行速度的竞态。

修复:不断言特定 task_id 被淘汰,改为验证不变量:
(1) 最后提交的任务一定存活(LRU 最近)
(2) 至少淘汰 2 个(5 - max_results 3)
(3) 存活任务数 ≤ max_results

本地跑 10 次全过,不再 flaky。
2026-07-07 15:47:13 +08:00
Justin Gu 27a8ab1c76 style: ruff format security_bars.py(修复 ci.yml format 检查)
合并 e3e8dd4 时采用远程版本,但其 _log.warning 字符串换行未跑
ruff format,导致 ci.yml 的 format 检查失败。纯格式调整,无逻辑变化。
2026-07-07 15:27:20 +08:00
Justin Gu ecd6e298f5 fix(packaging): v1.19.2 修复干净 Windows 上 EXE 页面纯黑(MIME 类型)
根因:干净 Windows(无开发环境)注册表里没有 .js 的 Content Type 映射,
mimetypes.guess_type('.js') 返回 None,StaticFiles 回退到 text/plain。
浏览器 <script type="module"> 启用严格 MIME 检查,拒绝执行 text/plain
的 JS → Vue 不挂载 → 页面纯黑(/docs 不受影响因为它是 API 路由)。

修复:mount 前用 mimetypes.add_type 强制注册 .js/.mjs/.css/.svg 的正确
MIME,无论机器装没装开发工具都生效。

同时改 easy_tdx.spec:datas 用 pathlib.rglob 逐文件展开,更可靠。
2026-07-07 15:18:53 +08:00
Justin Gu 957cb8eeb0 Merge commit 'e3e8dd4'
# Conflicts:
#	CHANGELOG.md
#	pyproject.toml
#	src/easy_tdx/commands/security_bars.py
2026-07-07 01:54:20 +08:00
Justin Gu 0b4ed9af62 feat(packaging): v1.19.1 支持 Windows 单 EXE 打包 + 系统托盘 + 自动发版
面向零基础老年用户,easy-tdx 可打包成单一 Windows EXE,双击即用。

新增:
- 后端同源托管前端 dist(app.py 三级探测:env → _MEIPASS → web-ui/dist)
- easy-tdx serve 默认 --open-browser,启动后自动开浏览器
- PyInstaller 打包入口(__main__.py)+ spec 配置(easy_tdx.spec)
- 系统托盘(tray.py):右下角图标,右键"打开浏览器/退出"
  解决老人不会用任务管理器关闭的问题
- GitHub Actions release.yml:打 v* tag 自动构建并发布 EXE 到 Releases
- docs/packaging.md 打包使用文档

修复:
- K 线残缺尾记录导致 500(security_bars.py):通达信服务器偶发
  ret_count 与 body 长度不匹配,改为 try/except 优雅降级丢弃残缺尾,
  返回已解析的完整记录。GetIndexBarsCmd 同改。加 4 个回归测试。
- PyInstaller frozen 模式三个坑:
  1. console=False 下 stdout/stderr 为 None → 重定向到日志文件
  2. multiprocessing spawn 子进程重新 import __main__ → freeze_support + 子进程检测
  3. 系统托盘需主线程消息泵 → uvicorn 挪到后台线程

文档:
- README/手册改为三档分流:EXE(零基础)/ Python(一条命令)/ 源码(打包)
- 删除 npm run dev / 5173 / 两个终端的过时说明
- 手册补虚拟环境配置 + EXE 打包附录 + EXE 排错 FAQ
2026-07-07 01:48:07 +08:00
GitHub e3e8dd492e fix: K线响应截断容错 + 一键寻优并发默认8进程
- security_bars: parse_response 遇末尾残缺记录时丢弃并返回前 N-1 条,
  避免 TDX 服务端截断响应导致整页 500(如 000408 count=800 日线)
- OptimizeView: 一键寻优并发默认 8 进程,用户选择持久化到 localStorage

bump version to 1.18.3
2026-07-06 13:23:37 +08:00