Skip to content

refactor: RatioSizer 经 _data_feeder 取价对齐组件边界 (#3877)#6431

Merged
Kaoruha merged 1 commit into
masterfrom
6434-refactor/ratio-sizer-data-feeder
Jun 28, 2026
Merged

refactor: RatioSizer 经 _data_feeder 取价对齐组件边界 (#3877)#6431
Kaoruha merged 1 commit into
masterfrom
6434-refactor/ratio-sizer-data-feeder

Conversation

@Kaoruha

@Kaoruha Kaoruha commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Summary

RatioSizer.cal LONG 分支直访 container.bar_service().get() 取历史价格,绕过注入的 _data_feeder 接口,违反组件边界单向流动Selector → Strategy → Sizer → Risk,Sizer 不应直访 data 层 Service)。对齐 FixedSizer 范式改走 _data_feeder.get_historical_data

根因

  • 表象:RatioSizer 直接 container.bar_service().get(code, start_date, end_date) 取价,而非用 SizerBase 提供的 self._data_feeder
  • 调用链RatioSizer.cal L110 → container.bar_service().get(...) → 直访 data 层 Service(绕过注入的 _data_feeder)。
  • 根因:RatioSizer 未对齐 FixedSizer 范式(self._data_feeder.get_historical_data(symbols, start_time, end_time))。
  • 影响:(1) 绕过 data_feeder 的回测/实盘数据源切换;(2) 无法被 bind_data_feeder 注入控制;(3) 实盘拿不到回测 feeder 的缓存价格。

修改

src/ginkgo/trading/sizers/ratio_sizer.py

  • LONG 分支:container.bar_service().get(...)self._data_feeder.get_historical_data(symbols=[code], start_time=..., end_time=...),去掉 result.success/result.data.to_dataframe(),直接用返回的 DataFrame。
  • if self._data_feeder is None: GLOG.ERROR(...); return None 守卫(对齐 FixedSizer L115)。
  • from ginkgo.data.containers import container(修复后无引用)。

Test plan

TDD RED→GREEN(tests/unit/trading/sizers/test_ratio_sizer.py,2 测试):

  • test_long_uses_data_feeder_not_container:双重锁——正向 feeder.get_historical_data 被调用 + 反向 container.bar_service 不被调用(patch 单例源对象属性,对任意 import 方式成立);端到端断言订单生成。
  • test_long_no_data_feeder_returns_none:未绑定 feeder 时 ERROR 返回 None 不崩。

三层冒烟全绿:

  • L1 py_compile (src+api) ✅
  • L2 启动路径 (user_crud_mock + containers + user_cli) 29 passed ✅
  • L3 sizers 回归 21 passed(ratio 2 + fixed/atr/commission 19)零回归 ✅

Closes #3877

🤖 Generated with Claude Code

@Kaoruha

Kaoruha commented Jun 24, 2026

Copy link
Copy Markdown
Owner Author

Code review

No issues found(5 agent 审查 + 竞争 PR 核查 + 接口契约验证,全部 clean)。核心重构正确:RatioSizer.cal LONG 分支取价从直访 container.bar_service().get() 改走注入的 self._data_feeder.get_historical_data逐字符对齐 FixedSizer L126-132 范式(None 守卫 + symbols=[code], start_time, end_time 参数 + past_price is None or past_price.shape[0]==0 判空 + iloc[-1]["close"] 取价)。真正修复 #3877 组件边界违规(Sizer 不直访 data 层 Service)。

核查要点(均通过)

  • 行为保持get_historical_data(backtest_feeder.py:196-202)内部亦走 result.data.to_dataframe(),与原 container.bar_service().get().data.to_dataframe() 同源同转换,列名 'close' 一致;最终都取 past_price.iloc[-1]["close"],无语义漂移
  • SHORT 分支无需改:SHORT 用 portfolio_info["positions"].get(code).volume 平多不取价([[arch_long_only_model]] 纯多头架构,SHORT=平多非开空),FixedSizer SHORT 同构;本 PR 只改 LONG(取价分支)范围完整
  • container 彻底拔除:master ratio_sizer.py 仅 L110 一处 container 直访(LONG),删 import + header Downstream 注释(container→_data_feeder)同步,PR 版 grep container 零残留
  • 注入链完整_data_feederSizerBase.__init__:50 初始化 + bind_data_feeder:52 注入,PortfolioBase.bind_data_feeder(portfolio_base.py:231-235)转发,引擎装配(base_engine.py:640 / event_engine.py:185)下发;sizer_validator.py:264 已列为必需属性。本 PR 复用既有注入接口,未改 Base 类
  • 历史方向一致_data_feeder 注入 sizers 由 commit f10feeb1(decouple trading from data layer)确立,FixedSizer 同次迁移;RatioSizer 是该次解耦遗漏的尾巴,本 PR 补齐
  • 调用方无影响sizer.cal(info, signal) 仅 2 调用点(portfolio_live.py:154、t1backtest.py:322),均不依赖返回结构或 container 副作用,Optional[Order] 契约未变
  • 无竞争 PR:open PR 中仅 refactor: RatioSizer 经 _data_feeder 取价对齐组件边界 (#3877) #6431 自身触及 sizers/;fix(#5493): Sizer 佣金改用 SimBroker 实际费率 max(rate×turnover, min) #6243(已合并)改 calculate_order_size 佣金估算,未碰取价行,无语义冲突
  • 测试非 tautologytest_long_uses_data_feeder_not_container 双重锁——正向 feeder.get_historical_data.assert_called_once() + 反向 patch.object(container_singleton, "bar_service").assert_not_called();修复回退则反向锁触发。patch.object 作用于单例源对象属性对任意 import 方式成立(修复后 ratio_sizer 不 import container,patch 仍有效)。端到端 volume≈4500>0 真实生成订单
  • 分层合规:改具体 RatioSizer 非 Base 类;测试位置 tests/unit/trading/sizers/ 合规(同 test_fixed_sizer/test_atr_sizer 先例)

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@Kaoruha

Kaoruha commented Jun 24, 2026

Copy link
Copy Markdown
Owner Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance. One out-of-scope same-root issue noted below (non-blocking, follow-up recommended).

Verified(#3877:RatioSizer LONG 分支取价从 container.bar_service().get() 改走注入的 self._data_feeder.get_historical_data(),对齐 FixedSizer 范式,消除"Sizer 直访 data 层 Service"边界违规):

  • 切片定位准确(无竞争 PR):亲验 gh pr list --state open 匹配 sizer|feeder|ratio 区域仅本 PR,无 diff 重叠。repo 历史无任何 PR 曾触碰 ratio_sizer.py(本 PR 是首个),无遗漏的前序迁移讨论。架构: RatioSizer绕过data_feeder直接访问data层 #3877 OPEN,本 PR 是唯一推进者
  • 【linchpin】FixedSizer 范式逐字符对齐:亲验 master fixed_sizer.py L126-141 与本 PR L108-119 五元素完全一致——①_data_feeder is None → ERROR + return None 守卫 ②get_historical_data(symbols=[code], start_time=last_month_day, end_time=yester_day) 签名 ③is None or shape[0]==0 空数据检查 ④CRITICAL 日志 ⑤iloc[-1]["close"] 取价。非创造性修改,是范式忠实复制
  • 【P0】返回类型链路闭环:亲验 backtest_feeder.get_historical_data(backtest_feeder.py:173)注解 -> pd.DataFrame,实现 L213 return pd.concat(dfs) if dfs else pd.DataFrame();docstring 明示"时间验证失败返 None(装饰器拦截)"。故 past_price is None 检查必要(覆盖装饰器未来数据泄露分支),与 FixedSizer L138 一致。接口层 interfaces.py:156 声明 Dict 是文档漂移,两 Sizer 都对齐实际 DataFrame 实现
  • 【P0】行为等价性:新旧都走 bar_service.get(code)→to_dataframe()→iloc[-1]["close"],新代码内部多一层 pd.concat([单code df])(symbols=[code] 只 1 元素,不重排行序只重置索引),iloc[-1] 语义不变。新代码空数据检查(is None or shape[0]==0)覆盖三态(None/空DF/非空DF),比旧 not result.success or len(result.data)==0 更严(新增未来数据泄露防护)
  • 【P0】feeder 装配链路真注入(排除 LONG 永远 return None 回归):亲验 portfolio_base.py:632-634(commit 91ccaefbind_sizer 时若 portfolio 已有 _data_feeder 则传播给 sizer;portfolio_base.py:231-235 bind_data_feeder 广播含 sizer。决定性证据:FixedSizer master 已用同款 _data_feeder.get_historical_data 运行,若装配不注入 feeder,FixedSizer 早就全坏。RatioSizer 走完全相同 _sizer 装配路径,等价风险等价修复
  • 测试真实性test_long_uses_data_feeder_not_container 双重锁——正向 mock_feeder.get_historical_data.assert_called_once() + 反向 patch.object(_container_singleton, "bar_service").assert_not_called()。container 是进程级单例,patch 单例源属性对任意 import 方式成立;fix 回退(直访 container)则 spy 被调用 → 断言失败,区分有效。端到端 order.volume > 0 可达(_make_signal 设全 _portfolio_id/_engine_id)。test_long_no_data_feeder_returns_none 覆盖 None 守卫
  • SHORT 分支无需改:master ratio_sizer SHORT 分支(L141-160)只读 portfolio_info["positions"] 不取价(本仓库纯多头架构 [[arch_long_only_model]],SHORT=平多),不需 feeder。FixedSizer SHORT 分支同款
  • CLAUDE.md 合规:组件边界违规根因消除(Sizer 不再穿透 API/CLI→Service→CRUD→DB 分层直访 data Service);分层架构不违规;未触碰 Base 类;非 sed 批量;测试放 tests/unit/trading/sizers/

⚠️ 显著附注(范围外同源 bug,非本 PR 阻塞项,建议 follow-up)

atr_sizer.py 存在与 ratio_sizer 旧代码完全同型的 container 直访违规——亲验 L17 from ginkgo.data.containers import container + L78 container.bar_service().get(code=code, start_date=start_date, end_date=end_date)。全 sizers 目录同源范围仅 atr_sizer(2 处)+ ratio_sizer(本 PR 已修),本 PR 修复后 atr_sizer 是唯一剩余的同源 Sizer。

https://github.com/Kaoruha/GinkGO/blob/master/src/ginkgo/trading/sizers/atr_sizer.py#L77-L80

#3877 issue body 与 CLAUDE.md 组件边界规则适用于全部 Sizer,本 PR 标题/body 明确限定 ratio_sizer,atr_sizer 是独立文件不构成本 PR 缺陷。但为彻底闭环"Sizer 不直访 data 层",建议:要么本 PR 扩展范围一并改 atr_sizer(同款 _data_feeder.get_historical_data 迁移),要么开 follow-up issue 追踪(避免 #3877 类边界违规在 atr_sizer 残留)。

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

LONG 分支不再直访 container.bar_service().get(),改用注入的
_data_feeder.get_historical_data(symbols, start_time, end_time),
对齐 FixedSizer 范式。删 container import,加 _data_feeder None 守卫。
违反组件边界单向流动(Sizer 不应直访 data 层)。

Co-Authored-By: Claude <noreply@anthropic.com>
@Kaoruha Kaoruha force-pushed the 6434-refactor/ratio-sizer-data-feeder branch from c9daa7a to ae52d0b Compare June 27, 2026 17:16
@Kaoruha Kaoruha merged commit cc00241 into master Jun 28, 2026
3 of 4 checks passed
@Kaoruha Kaoruha deleted the 6434-refactor/ratio-sizer-data-feeder branch June 28, 2026 02:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

架构: RatioSizer绕过data_feeder直接访问data层

1 participant