Commit Graph
148 Commits
Author SHA1 Message Date
GitHub 534ab0e0c8 Merge branch 'main' of https://github.com/handsomejustin/easy_tdx 2026-09-02 13:16:41 +08:00
Justin Gu 0a00ca3f70 release: v1.28.0 — 深度风险报告+移动止损+黄金测试(借鉴 akquant):25 项绩效 / α·β·IR·TE 基准对比 / trail_stop OCO 2026-09-02 11:53:53 +08:00
Justin Gu 4dde339f26 fix(mac): 异动 0x1237 补齐 0x15/0x16/0x1D/0x1E 解析并修正 0x13 试盘方向(#62)
- 0x16 盘中强势/弱势:09:25 样本 v2 与开盘涨幅 49/49 精确一致,v1 为带符号 ±1~3 级强弱等级
- 0x15 竞价/尾盘异动:09:25 与 15:00 双时刻触发,desc 按时刻区分竞价/尾盘前缀
- 0x1D/0x1E 急速拉升/急速下跌:阈值下限 ±0.6%
- 0x13 竞价试盘:v1=0x00 试买/0x01 试卖(552 条对照昨收 549 条一致),旧实现试卖方向描述错误
- 新增顶层常量 UNUSUAL_TYPE_NAMES(19 种类型码→名称)
- 协议探索结论与完整证据链文档化 docs/protocol-unknown-fields.md §3.4
- 测试 test_unusual.py 21 例(全部真实抓包字节 fixture)
2026-09-02 10:44:09 +08:00
GitHub 36ea8497ae perf(engine): 信号管线提速 ×12.6 — 向量化快速路径 + 日期查找表 + 去 strftime
基准先行(scripts/bench_engine.py):ma_cross 800 根全流程基线 93.6ms。
profile 打破预期:85% 墙钟在 OrderSimulator._find_bar_index(每信号全列
strftime),而非计划认为的逐 bar 信号循环。三处优化(行为逐位一致):

- Strategy 新增 entry_exit_masks() 显式钩子,19 个内置策略全实现;引擎按
  掩码 + 候选事件 bar 状态机一次产出信号,持仓估算逐行复刻
  _update_strategy_position(含买不足 1 手退化路径);约束检测
  _vectorize_eligibility 显式可测,signal_path=auto/vector/loop 可指定;
  不满足约束(无钩子/缠论注入/掩码形状错)自动回退逐 bar
- OrderSimulator._build_dt_lookup:O(信号×bar) 全列扫描 → 每 simulate 一次
  O(bar) 查找表(重复日期取首个、未命中 None、object 恒不匹配语义对齐)
- _datetime_to_int 去 strftime:year*10000+month*100+day 整数算术
  (NaT→NaN 行为一致),_bind_data 与查找表共用

实测:ma_cross 全流程 93.6→7.4ms(×12.6),信号层 ×1.39~1.72(四策略);
32 点网格寻优 ~3.0s→231ms。对拍 39 例:19 策略×参数变体×warmup/低资金/
费率/缓存,performance/trades/equity/positions 逐位一致。
顺带记录:wr_reversal 默认阈值与 MyTT WR 刻度不匹配(策略恒不交易,既有问题未改)
2026-09-01 23:47:06 +08:00
GitHub e58de789ab feat(ws): /ws/realtime 接通 RealtimeDataFeed — 按需轮询 hub + fan-out + 冒烟脚本
- web/realtime_hub.py:RealtimeStreamHub——订阅集合变化按需启停 RealtimeDataFeed,
  无人订阅完全停止轮询(对齐 QuoteStreamer 节能);EventBus → 每连接队列
  fan-out(丢最旧保最新);去重标的上限 80;lifespan 挂载/关闭
- routers/realtime.py 重写:连接即订阅、断开退订、subscribe/unsubscribe 控制帧、
  30s ping 心跳;单一写者泵模型(全部出站帧经队列串行,防并发 send 交错)
- 修复 RealtimeDataFeed stop-before-start 竞态(_stop_requested 标志),
  补 2 个回归用例;hub 单测 10 例(并发订阅/退订竞态、跨标的串扰、背压、
  TestClient 端到端)
- 前端接入选文档方案(api_reference.md 协议+重连/心跳骨架;README 撤「未联动」):
  SSE 已覆盖看板/自选实时刷新,WS 定位按需单标的 tick,双通道冗余无必要
- scripts/ws_smoke.py:手动冒烟(mock 模式实测 tick 帧与动态订阅确认可见)
2026-09-01 23:21:42 +08:00
GitHub bec231e8be test: 修复时段门控用例的时间相关 flaky(当地 23 点档必炸)
test_outside_session_no_fetch 此前用固定 23:00-23:59 模拟盘外时段,
但当测试跑在当地 23:00-23:59 时当前时刻恰好落在该时段内,断言必炸(本地
23 点后全量回归复现)。改为动态构造「当前时刻 +5~+6 分钟」的 1 分钟未来
时段,任何时刻运行都保证盘外。1263 全量通过。
2026-09-01 23:05:14 +08:00
GitHub 211052f0e3 feat(e2e): Playwright E2E 前端测试基建 — 后端合成数据 mock 模式 + 5 用例 + CI
- web/e2e_mock.py:EASY_TDX_E2E_MOCK=1 时 lifespan 替换 TDX/MAC 客户端为
  确定性合成数据(CRC32 播种随机游走,分页语义对齐真实 /bars);回测/WF/
  评估/自选/策略库仍走真实后端,SSE 由 QuoteStreamer 真轮询(mock 下 2s 一拍)
- web-ui:@playwright/test + playwright.config.ts(webServer 自动起 serve,
  EASY_TDX_CONFIG_DIR 指向每轮临时目录);e2e/ 5 用例覆盖看板五指数/SSE、
  自选增删、回测全流程、附加分析(WF 柱状图+一条龙评估卡)、策略库保存
- package.json 加 test:e2e;CI frontend job 追加 E2E;verify_ci.sh 补前端段
- tests/unit/test_e2e_mock.py(11 例)守护 mock 契约;本地 npx playwright test 全绿
2026-09-01 22:56:38 +08:00
GitHub 917295edaf release: v1.27.0 — 通达信公式解析器三通道 + 轮动组合引擎 + 回测页WF/评估开关 + Docker 部署
升级计划 P3 + P4(部分)。全量 1252 单测、ruff/mypy strict、前端 vue-tsc+vite build 全绿。

- 通达信公式解析器(formula.py):自建 tokenizer + 递归下降 AST + 30+ 函数白名单求值
  (不走 Python eval);命名布尔输出=信号列、数值输出=排名列;除零→NaN、预热期不出信号
- 公式三通道:CLI easy-tdx formula compute|screen|backtest;REST /formula/validate|compute|
  backtest|screen(run/async);Python API run_formula_backtest(买/卖列自动挑选)
- 轮动组合引擎(rotation.py):排名定期换仓(打分只用截至当日数据)、槽位等额、
  跌出排名自动补位、日/周/月刷新、槽内止盈止损;momentum_score/formula_score 打分;
  REST /backtest/rotation/run/async
- 回测页附加分析开关(Web UI):勾选后随回测并行跑 WF(逐窗红涨绿跌柱状图+汇总卡,
  窗口数 2~12)与一条龙评估(评分分项条/高适配徽标/买入持有对比/8 项适配检查);
  新增 WalkForwardPanel/EvaluatePanel 组件与 store runWalkforward/runEvaluate;
  WF 端点 ?n_windows= 透传;修复报告 numpy 标量 REST 400(源头清洗)
- Docker 部署(Dockerfile + docker-compose.yml,/data 卷 + 健康检查)与
  scripts/verify_ci.sh 一键门禁
- 升级计划文档 docs/upgrade-plan-2026H2.md(四阶段全部完成 + 诚实实测数据)
- 未做(独立排期):Playwright E2E、WebSocket 实时联动、引擎逐 bar 向量化
2026-09-01 22:17:57 +08:00
GitHub 9569b2653c release: v1.26.0 — DuckDB K线仓库 + provisional 状态机 + 增量同步 + 健康自检
升级计划 P2(P2-2 评级后端化已随 1.25.0 提前交付):把碎片化缓存升级为统一数据底座,
对齐 indicator-lab 自建 DuckDB 层的定位。

- warehouse/ 包:DuckDB 单文件(~/.easy_tdx/warehouse.duckdb,随 EASY_TDX_CONFIG_DIR),
  列存 + 主键 upsert 去重;duckdb 为可选依赖(pip install easy-tdx[warehouse])惰性导入
- provisional/completed 状态机:15:05 前当日 bar 逐行标记临时,查询/回测默认忽略,
  promote_provisional 过期转正,include_provisional 显式可见
- 增量同步器:首同步全量(默认 8000 根)此后只补尾部 15 根,批次进度回调、单标失败不中断
- 健康自检:疑似缺口/异常跳变(复用 QFQ 对拍跳空检测)/最新度/provisional 统计
- CLI easy-tdx warehouse sync|query|stats|check(sync 支持 @文件标的列表)
2026-09-01 22:17:28 +08:00
GitHub b2509a8d0e release: v1.25.0 — Walk-Forward/适配性/一条龙评估防过拟合链 + 评分评级后端化 + 寻优加速
升级计划 P1:补上两个下游项目都在自研的样本外验证空白。

- Walk-Forward 引擎(walkforward.py):7 窗样本外、每窗独立开仓(backtest-system v1.2.1 踩坑语义)、
  上下文预热不污染;CLI --wf、REST /backtest/wf/run/async
- 适配性评估(fitness.py):train/valid/test 三段 + 8 项可解释检查 + 高适配标记;
  evaluate_prefix 滚动过滤原语(无未来泄漏)
- 一条龙评估(benchmark.py evaluate_strategy):回测+WF+适配性+评分+评级+买入持有基准对比;
  CLI --evaluate、REST /backtest/evaluate/run/async
- 综合评分(scoring.py,收益50/夏普15/回撤10/Sortino5/WF20)+ 评级后端化(grading.py,
  前端 TS 忠实移植,REST 响应新增 grade/score 字段)
- 多 seed 验证 + 四项晋级门槛(validation.py);REST /backtest/multiseed/run/async
- 寻优两段式加速:IndicatorCache(36 点网格命中率 41.7%)+ workers 进程并行(实测约 2x);
  诚实注:指标缓存墙钟 ~1.01x,瓶颈在逐 bar 循环,后续向量化
- strategy.I() 指标缓存钩子 + 数据代理零拷贝(astype copy=False);
  types.to_json_native 统一 numpy 清洗
2026-09-01 22:17:08 +08:00
GitHub 1fc1d00c90 release: v1.24.0 — QFQ 对拍验证体系 + 回测任务持久化 + 品种感知费率
升级计划 P0(docs/upgrade-plan-2026H2.md)。源自 backtest-system / indicator-lab 两个下游项目的逆向调研。

- QFQ 对拍验证:公式法(NONE+XDXR)与跳空检测法(板块感知涨跌停阈值)双证据链互检,
  检出负价/残留跳空/方向反演/XDXR 缺记录四类问题,接入 MAC 同步/异步客户端(mac/qfq_check.py);
  含茅台式多重分红、浦发式送转方向合成案例回归(13 用例)
- 回测任务 SQLite 持久化:~/.easy_tdx/tasks.db 双写内存 LRU + 磁盘(保留 500 条),serve 重启不丢;
  重启恢复中断任务标记;GET /backtest/tasks/{id}/export?format=json|csv 导出端点
- 品种感知费率:ETF/可转债免印花税等法定差异(backtest/fees.py),CLI --auto-fees、
  REST auto_fees 字段、组合引擎逐标的解析(34 用例)
- 修正 avg_holding_days 过时注释(实现早已是 FIFO 真实口径)
- tests/conftest.py 默认 EASY_TDX_NO_TASK_DB=1 防止单测污染用户任务库
- 注:engine/cli/routers/schemas 为跨版本累积态,后续版本提交继续演进
2026-09-01 22:16:44 +08:00
GitHub ac4ac6b2aa feat(cli): serve 新增 --no-ui 纯 API 模式
- --no-ui:不托管 Web UI 前端(根路径 404),仅 /api/v1/* REST 接口,
  且不自动打开浏览器——给 AI Agent / 程序化调用省去前端资源
- create_app/_create_app 新增 enable_ui 参数(默认 True 行为不变)
- 测试锁定:no-ui 模式无 web-ui Mount 且 API 路由完整;四道门禁全绿
  (ruff / ruff format / mypy / 1079 tests),实测根路径 404 + API 正常
2026-09-01 18:33:49 +08:00
GitHub dffcd9f792 style: ruff format 三文件重排(quote_streamer/watchlist_store/test_ex_tick_chart_date)
本地预演 CI 全部四道门禁通过:ruff check / ruff format --check /
mypy (226 files) / 1078 tests
2026-09-01 17:58:12 +08:00
GitHub 6fe3f0f8c4 fix: 补齐 CI 质量门禁——mypy strict 类型标注 + ruff 行宽/导入排序
- watchlist_store/routers/watchlist: 裸 dict 补泛型参数 dict[str, object]
- routers/stream: event_gen 返回 AsyncGenerator[str, None] 注解
- app: _watch_symbols 返回类型;teardown 变量改名消除类型冲突
- ruff: 5 处超长行拆行、3 处导入排序自动修复

ruff / mypy (226 files) / 1078 tests 全绿
2026-09-01 17:52:36 +08:00
GitHub 33d44ba5d6 feat(webui)!: 行情终端重大升级——市场看板/自选/个股与板块弹窗 + SSE 实时推送
展示层对标专业看盘终端(tick-stock-panel 模式),数据全部来自通达信协议直连:

- 市场看板:五大指数实时条(迷你分时)、涨跌统计、四维情绪雷达、
  全市场涨跌分布直方图(约 5500 只 22 桶、鼠标跟随浮窗)、涨停雷达、
  行业/概念热冷榜(可下钻)、涨幅/跌幅/成交额/换手四联排行榜、异动雷达
- 自选行情:6 位代码即加(symbol-info 自动取名)、SSE 实时全表刷新、
  行内迷你分时;SQLite 持久化(watchlist.db,幂等加删)
- 个股弹窗:五档盘口 + 1/3/5 日分时 + 日K(MA/BOLL/EMA + MACD/KDJ/RSI
  前端本地计算)+ 一键加自选 + 一键全策略寻优
- 板块弹窗:板块分时/日K + 成分股涨跌榜直达个股
- SSE 架构:QuoteStreamer 单循环轮询 fan-out(独立队列 + 背压丢旧、
  无订阅休眠、盘中 8s/盘外 60s 降频);前端 pinia 单连接指数退避重连
- fix(codec): SH 000 系大盘指数与 881/885 板块指数报价按 2 位小数解析
  (曾缩小 10 倍:科创50 1647.53→164.753);880 统计指数保持 3 位
- fix: 前端批量五档路径 /security/quotes→/quotes、SSE 五档白名单
  bid1_vol→bid_vol1、MAC 排行 close 列归一化、日K v-if 撑满、
  寻优查看跳转 /→/backtest、FastAPI 0.141 _IncludedRouter 测试适配

新增 27 个单测(streamer/自选/小数位语义),全套 1078 passed
2026-09-01 17:45:07 +08:00
毛利哥andGitHub 12d308e1a4 fix!: 扩展日线槽位语义——第6槽为 float32 成交额,移除误导字段 hk_stock_amount(#57) (#61)
read_ex_daily_bars 原把第7槽(成交量)同时赋给 amount 与 vol(amount=vol),
真实成交额藏在第6槽 float32 重解释值里、以 hk_stock_amount 之名暴露。
实测对照:47#IF300 2023-09-11 第6槽 float32=186871758848(元)、第7槽
uint32=105358016(手),与标准市场 sh000300.day 同日 amount/vol 完全一致。

- _EX_DAILY_FMT: <IffffIIf → <IfffffIf,第6槽正名为 float32 成交额
- ExDailyBar.amount: int → float;移除语义错误的 hk_stock_amount 字段
- encode 端沿新 fmt 自动正确:大成交额按 float32 编码,不再溢出 uint32
- CLI offline ex-daily 输出新增 amount 列
- 新增真实样本回归测试(47#IF300 原始记录)+ 补齐缺失的 amount 往返断言

Co-authored-by: GitHub <action@github.com>
2026-08-31 20:43:35 +08:00
Harveyliu007 349f44f959 fix(ex): ex tick --date 接受 YYYYMMDD 整数,修复 AttributeError 2026-08-28 19:53:51 +08:00
GitHub fd53ec4c90 fix(mac): 板块列表涨速恒 0——值槽实为排序键列值,暴露 sort_column(issue #53)
抓包+对值锚定确认:0x1231 响应中 price 与 pre_close 之间的 float 是
"当前排序列的值"(板块与领涨股各一份),并非固定涨速;此前硬编码
sort_column=0(涨跌幅,仅排序键、值槽恒 0),故该列永远全 0。

排序列映射(实测):0=涨跌幅(值槽恒0) 1=涨速 2=3日 3=20日 4=60日
5=年初至今 6=5日 7=10日。

- 新增 BoardSortColumn 枚举并公开导出;get_board_list(sync/async)
  新增 sort_column 参数,取涨速传 SPEED,默认涨跌幅排序行为不变
- BoardInfo 字段更名 rise_speed→sort_value、symbol_rise_speed→
  symbol_sort_value(旧名语义错误且恒 0,属破坏性更名)
- Web /board-mac/list 新增 sort_column 参数;CLI board-list 新增 --sort
- 新增 9 个测试;README 示例更新
2026-08-26 15:21:35 +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 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 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
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
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
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 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
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 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
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
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 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 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
Justin Gu 239ff46ac8 release: v1.17.13 — 修复多策略组合回测最大回撤虚高(分母误用初始值而非峰值)
用户反馈:3 个策略各自最大回撤仅 45.53%/40.16%/16.89%,组合却显示 83.76%。
根因:_build_combined_equity 的 drawdown_pct 分母误用 initial(固定初始资金),
净值大涨后(总收益 545%,峰值≈6.45×初始)绝对回撤额除以小初始值被等比放大。
改为 drawdown / peak(相对当时峰值,0~1),与单标的 PortfolioTracker 一致。
连带修复卡玛比率(年化/最大回撤)。其余指标经核对均正确。加回归守卫。
2026-07-04 21:07:39 +08:00
Justin Gu ee628c1b34 release: v1.17.12 — 修复 CI 在新版 FastAPI 上路由注册失败
DELETE /api/v1/strategies/{id} 原用 status_code=204,较新 FastAPI/Starlette
在路由注册阶段(add_api_route)就抛 AssertionError: Status code 204 must
not have a response body,导致 CI ubuntu 矩阵 21 个 web 测试因 fixture
导入 router 连带 ERROR。改为返回 200 + {"deleted": id} 确认体。
2026-07-04 20:50:07 +08:00
Justin Gu 05dc9a74af release: v1.17.11 — Web UI 策略库(SQLite 持久化)+ 多策略资金分仓组合回测
新增两层能力:
1. 策略库:单标的/组合回测结果可保存到本地 SQLite 单文件
   (~/.easy_tdx/strategies.db),策略库页可载入回填、重跑、删除。
2. 多策略组合回测:勾选 N 个单标的策略,各拿 1/N 资金、各跑原标的,
   净值曲线按日期并集对齐求和,组合结果含 19 项完整绩效指标 + 持仓表。

后端:strategy_store.py(SQLite CRUD) + multi_strategy_engine.py(资金分仓引擎)
+ routers/strategies.py + /backtest/multi-strategy/run/async。
前端:StrategiesView.vue + 保存策略按钮 + 复用组合页图表组件。

895 单测全绿(+24 新增),ruff/mypy strict/前端 vue-tsc 全通过。
2026-07-04 20:40:57 +08:00
Justin Gu 57cad1f3db release: v1.17.9 — 修复回测交易统计离谱数值(前后端口径错配 + 持仓天数跨月放大)
- 交易盈亏指标改为收益率口径(avg_win/loss/max_win/loss = pnl/cost_basis)
- 平均持仓天数改用真实日历日相减(原 YYYYMMDD 整数差跨月放大)
- 盈亏比无亏损时记 999.0(原 0.0,与 100% 胜率自相矛盾)
- 新增 Trade.cost_basis 字段 + engine 填充
- 3 个回归守卫;870 单测全绿,ruff/mypy strict/前端 vue-tsc 通过
2026-07-04 03:55:05 +08:00
Justin Gu cd790c9404 release: v1.17.8 — 修复 windows 3.10 flaky 测试超时
test_task_runner_does_not_evict_running 用 release.wait(timeout=5) 钉住慢任务保持
running,但 CI 慢环境(windows 3.10)下整个测试执行超过 5s 后任务因超时自动完成、
状态变 done,掩盖了「running 任务被 LRU 错误淘汰」的回归断言。

改为 timeout=30 留足 CI 慢环境余量(release.set() 仍是确定性释放点)。该测试用于
守护 LRU 淘汰跳过 running 任务的并发正确性,与港股逐笔成交无关,是 v1.17.2 引入的
预先存在问题。

本地连跑 5 次稳定通过,867 单测全绿。
2026-07-04 02:24:48 +08:00
Justin Gu 56153eeb97 release: v1.17.7 — 修复 Windows CI fixture 读取编码
1.17.5 引入的 tests/fixtures/ex_history_transaction.json 含中文注释,Windows CI
默认用 cp1252 解码 UTF-8 文件触发 UnicodeDecodeError: 'charmap' codec can't
decode byte 0x8f,导致 3 个 Windows 矩阵的 test_parse_ex_history_transaction_hk
失败(Linux 矩阵因默认 utf-8 不受影响)。

修复:所有 fixture 读取显式指定 encoding='utf-8':
- test_hk_transaction.py 的 load_hex/load_json(直接触发点)
- test_commands_offline.py 的 load_hex(纯 ASCII hex,当前未触发但一并统一)
- test_decode_errors.py 的 _load_hex(同上)

867 单测全绿,ruff format/check / mypy strict 通过。
2026-07-04 02:11:27 +08:00