Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 14 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,28 @@

<p align="center">
<img src="https://img.shields.io/badge/status-v0.1.4-blue"/>
<img src="https://img.shields.io/badge/hqdata-%3E%3D0.1.21-blue"/>
<img src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue"/>
<img src="https://img.shields.io/badge/hqdata-%3E%3D0.1.22-blue"/>
<img src="https://img.shields.io/badge/python-%3E%3D3.10-blue"/>
</p>

`hqbacktest` 是 HonestQuant 量化系统的**策略回测与交易模拟层**,面向 A 股日线策略。它给量化研究者一个**确定性的、可复现的、与实盘严格隔离**的回测沙盒:策略只通过受控的 `Context` / `DataView` 读写数据、提交订单和查询组合,不接触数据源实现或内部账本;引擎负责时钟、撮合、规则与成本、指标和可审计的结果导出。

## 定位

- **对下:** 只读 `hqdata` CLI 已落盘的 CSV 快照(默认 `~/.hqdata/{source}/`),不导入 `hqdata`、不调用任何数据源 SDK、也不在回测运行时访问网络。
- **对下:** 通过 `hqdata.api` 的 `csv` source 读取 `hqdata` CLI 已落盘的 CSV 快照;不调用任何数据源 SDK、也不在回测运行时访问网络。
- **对中:** 提供严格的交易日事件时钟、数据可见性控制、订单生命周期、虚拟经纪商、持仓账本和交易规则。
- **对上:** 让策略只通过 `Context` / `DataView` 读取数据、提交订单和查询组合,不接触数据源实现或修改内部账本。
- **对外:** 输出可复现的净值、订单、成交、持仓、费用和绩效指标,用于研究和模拟,不连接真实券商。

## 已实现的能力

| 功能 | 目标接口 / 产物 | 首版语义 |
| --- | --- | --- |
| 日频事件时钟 | `BacktestEngine` | 五阶段固定顺序 `SESSION_START → BEFORE_TRADING_START → OPEN_MATCH → BAR_CLOSE → AFTER_TRADING_END`;盘前 D-1、收盘 D 的可见性切换;事件日志记录日期与阶段 |
| 交易日与历史股票池 | `MarketDataPortal` | 按回测日获取交易日和股票池,避免以今日股票列表产生幸存者偏差;`.BJ` 默认过滤,`include_bj=True` 保留 |
| 日线数据可见性 | `DataView.history()` | 盘前最多看到前一交易日;当天收盘后才可读取当天日线;首日盘前哨兵 `visible_through="00000000"` 不抛异常 |
| 缺行 / 停牌 / 估值口径 | `get_bars` / `DataView.current_price` / 日终估值 | `get_bars` 允许逐日间隙;停牌持仓按 20 日回看最近收盘估值并写 `DATA_WARNING`;整日快照缺失 → `SnapshotFileMissingError`;`Bar.volume` 单位「手」 |
| 策略生命周期 | `BaseStrategy`、`Context` | `initialize` / `before_trading_start` / `on_bar` / `after_trading_end` 四回调;只读 `Context` 暴露 `cash` / `positions` / `universe` / `pending_orders` / `history` / `current_price` / `total_equity`;下单意图 `order` / `order_value` / `order_target` / `order_target_value` / `order_target_percent` / `cancel_order` |
| 下单与撤单 | `Context.order_*()` | 首版只支持市价委托,仅盘前与收盘回调可下单;订单创建 / 撤销写入事件日志 |
| 虚拟撮合与账本 | `SimulatedBroker`、`Portfolio` | 盘前订单按当日开盘价撮合;收盘订单最早次日开盘成交;同批 SELL 先于 BUY;回测结束时未成交订单 `BACKTEST_ENDED` 撤销 |
| A 股基础规则 | `TradingRuleSet`、`CostModel` | 买入整手(卖出允许零股)、T+1、停牌 / 无价拒绝、现货多头、显式费率(佣金 0.025% + 5 元保底、印花税 0.1% 卖出) |
| 公司行为扩展 | `CorporateActionProvider`、`AdjustmentPolicy` | v0.1 仅 `adjustment_policy="none"`;`CorporateActionProvider` 是设计草案;因子诊断接口存在但默认不启用自动诊断 |
| 端到端示例 | `examples/buy_and_hold.py`、`examples/moving_average.py` | 仅用公共 API + 7 天 `InMemoryDataPortal` 确定性数据;`tests/examples/` 覆盖买-持、均线、T+1、费用、净值与指标 |
| 结果与分析 | `BacktestResult` | 净值曲线、订单 / 成交 / 持仓 / 费用 CSV + `summary.json` + `events.jsonl`;`PerformanceMetrics` 含累计 / 年化 / 波动 / 夏普 / 最大回撤 / 换手 / 胜率 |
| 配置与命令行 | `hqbacktest run` | TOML 配置 + 校验 + 策略导入 + 独立输出目录;`run_metadata.json` 不含凭证 / 完整环境;本地绝对路径脱敏为相对 cwd 路径 |

> 「已实现」与「不做」的完整边界见 [`docs/design/mvp-contract.md`](docs/design/mvp-contract.md)。模块级细节见 [`docs/`](.) 下的专题文档。

## 支持的数据源

| 数据 | 来源 | 适用场景 |
| --- | --- | --- |
| 真实日线 | `hqdata` CLI 落盘的 CSV 快照(`tushare` / `ricequant`) | 任何需要真实行情的回测 |
| 真实日线 | `hqdata` CLI 落盘的 CSV 快照(`tushare` / `ricequant`)通过 [`HqDataCsvPortal`](src/hqbacktest/data/hqdata_portal.py) 读取 | 任何需要真实行情的回测 |
| 内存 fixture | `InMemoryDataPortal` | 单元测试、示例、`tests/examples/` 端到端 fixture |

`HqDataCsvPortal` 在构造时把 snapshot 路径传给 `hqdata.init_source("csv", root=...)`,所有 CSV 解析由 `hqdata.sources.csv_source.CsvSource` 负责(列名校验、文件存在性、整日缺失抛 `SnapshotFileMissingError`)。

`hqdata` 当前 `akshare` 适配器不稳定,按其官方说明**不**作为本项目首选数据源。需要日线请使用 `tushare` 或 `ricequant`;具体数据下载与落盘见 [`hqdata` README](https://github.com/HonestQuantTech/hqdata)。

## 首个可用版本的范围
Expand Down Expand Up @@ -75,25 +58,26 @@ cd hqbacktest
python -m venv .venv
source .venv/bin/activate

# 装数据层(按需选择数据源)
pip install -e "../hqdata[tushare]"
# 装数据层(hqbacktest 只依赖 hqdata 的 csv source;具体数据源 tushare/ricequant 由 hqdata CLI 异步下载落盘)
pip install -e "../hqdata"

# 可编辑安装本项目 + 开发依赖
pip install -e ".[dev]"
```

`pyproject.toml` 声明的 Python 目标版本为 3.10 / 3.11 / 3.12。
`pyproject.toml` 声明的 Python 下限为 `>=3.10`(与 `hqdata` 一致)。

## 配置数据源

`hqbacktest` 不接触任何数据源 token,回测配置通过 `data_root` + `source` 定位 `hqdata` 已落盘的 CSV。
`hqbacktest` 不接触任何数据源 token,也不在回测运行时联网。回测侧只声明 `source`(数据源名或绝对路径)与 `data_root`(父目录),`hqbacktest` 内部把它们解析成 hqdata 要求的 `(root, source_name)` 并交给 [`hqdata.init_source("csv", root=..., source_name=...)`](https://github.com/HonestQuantTech/hqdata)。

| 写法 | 含义 |
| --- | --- |
| `data_root="~/.hqdata"`, `source="tushare"` | 使用 `~/.hqdata/tushare` |
| `data_root="/mnt/market-data"`, `source="ricequant"` | 使用 `/mnt/market-data/ricequant` |
| `data_root="~/.hqdata"`, `source="tushare"` | 解析为 `(~/.hqdata, tushare)`,传给 hqdata 的 `root=~/.hqdata/tushare`、`source_name="tushare"` |
| `data_root="/mnt/market-data"`, `source="ricequant"` | 解析为 `(/mnt/market-data, ricequant)` |
| `source="~/.hqdata/tushare"`(绝对路径) | 直接拆分 `(parent_dir, basename)`,忽略 `data_root` |

`source` 接受名称或绝对路径,底层 CSV 布局由 `hqdata` CLI 在回测前写入;`hqbacktest` 既不下载数据,也不保存凭证。
`source` 接受**名称**(搭配 `data_root`)或**绝对路径**(拆分)。底层 CSV 布局由 `hqdata` CLI 在回测前写入;`hqbacktest` 既不下载数据,也不保存凭证。

## 使用

Expand Down
11 changes: 7 additions & 4 deletions docs/design/mvp-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
| --- | --- |
| 市场与频率 | A 股普通股票的**日线**回测;先支持沪深普通股票。北交所、ST、上市首日无涨跌幅限制等特殊证券延后。 |
| 账户 | 单账户、人民币现金、现货多头;不支持融资融券、做空、期货、期权、组合级保证金。 |
| 数据边界 | 每次运行只读取一个 hqdata 已落盘数据源的 CSV 快照;`data_root` 默认 `~/.hqdata`,`source` 选择其下的子目录。门户直接只读稳定的 CSV 布局,不导入 `hqdata`、不接触底层 SDK、不访问网络。日线首选 Tushare 或 RiceQuant 的本地 CSV。 |
| 数据边界 | 每次运行只读取一个 hqdata 已落盘数据源的 CSV 快照;`data_root` 默认 `~/.hqdata`,`source` 选择其下的子目录。门户通过 `hqdata.api.get_*` 调用统一的 `CsvSource` 读取稳定布局的 CSV —— CSV 列映射、文件存在性与列名校验由 hqdata 负责,回测侧只把 DataFrame 转 `Bar/Factor`。hqbacktest 不导入 `hqdata.sources` 或任一数据源 SDK、不在回测运行时访问网络。日线首选 Tushare 或 RiceQuant 的本地 CSV。 |
| 时间语义 | `before_trading_start(D)` 只能看到 D-1 及以前的数据,可提交在 D 开盘撮合的订单;`on_bar(D)` 在 D 收盘后看到 D 日线,订单最早在 D+1 开盘撮合。 |
| 初始订单 | 首版只支持市价委托;默认按符合交易条件的开盘价全额成交。限价单、分笔、成交量参与率、盘中撮合均属后续能力。 |
| 数据可见性 | 策略读取数据必须经过带 `visible_through` 截止日的 `DataView`;任何未来数据访问必须抛错。 |
Expand Down Expand Up @@ -134,9 +134,10 @@
- `strategy` 只能依赖 `engine/context` 暴露的 `Context`、`DataView` 与生命周期回调;不得导入 `hqdata.*`、不得持有 `MarketDataPortal` 的原始实现。
- `engine` 编排 `MarketDataPortal`、`DataView`、`broker`、`portfolio` 与策略生命周期;它通过 `MarketDataPortal` 读取交易日历,并向 `broker` 提供窄化的撮合行情接口。
- `broker/portfolio` 只能由 `engine` 驱动;不得反向调用策略或回写 `Context`。`broker` 不负责日历迭代或策略调度。
- `data portal` 只暴露协议化的 `MarketDataPortal`;`HqDataCsvPortal` 是默认实现,只读取 hqdata CLI 已落盘 CSV,禁止导入 `hqdata`、`hqdata.sources` 或任一数据源 SDK。
- `data portal` 通过 `data_root` 与 `source` 解析数据集根目录。v0.1 的固定布局为 `{root}/{source}/calendar.csv`,以及 `stock_list/{YYYYMMDD}.csv`、`stock_daily/{YYYYMMDD}.csv`、`stock_factor/{YYYYMMDD}.csv`;任何缺失、不可读或格式不符的文件必须报错,不得联网回补。
- `data portal` 只暴露协议化的 `MarketDataPortal`;`HqDataCsvPortal` 是默认实现,通过 `hqdata.api` 的 `csv` source 读取 hqdata CLI 已落盘的 CSV(详见 §3.1)。portal 不得导入 `hqdata.sources` / 任一数据源 SDK;不重新实现 CSV 列映射。CSV 列名校验、缺失文件异常、数据来源的「整日」边界均由 hqdata 端负责。
- `data portal` 通过 `data_root` 与 `source` 解析数据集根目录,**传给 `hqdata.init_source("csv", root=...)`**,由 hqdata 端解析布局(v0.1 固定为 `{root}/{source}/calendar.csv` + `stock_list|stock_daily|stock_factor/{YYYYMMDD}.csv`);任何缺失、不可读或格式不符的文件由 hqdata 抛出 `SnapshotFileMissingError` / `InvalidDataError`,portal 透传。
- hqdata CSV 快照是叶子数据边界;更新数据只能在回测运行前通过 hqdata CLI 完成。
- 回测侧通过 `hqdata.api` 读取 CSV;任何自定义源(替代 `CsvSource`)必须保持同等的列名契约与缺失语义,否则替换需要回到本节同步调整。

### 3.3 数据可见性与缺行语义

Expand Down Expand Up @@ -260,6 +261,7 @@
| 2026-08-17 | 修正盘前订单的同日开盘撮合语义;明确收盘估值、结束订单、股票池资格与异常分类;v0.1 仅支持 `AdjustmentPolicy=none` | hqbacktest 维护者 |
| 2026-08-23 | 公司行为扩展设计门槛落地——`adjustment_policy` 严格只接受 `"none"`;`CorporateActionProvider` 列为设计草案并锁定 10 个权威字段;`factor_diagnostics` 字段已就位;因子诊断接口存在但 v0.1 不启用 | hqbacktest 维护者 |
| 2026-08-23 | 修正回测运行时数据边界:`hqbacktest` 直接只读 hqdata CLI 落盘 CSV;`data_root` 默认 `~/.hqdata`,不调用 `hqdata.api` 或网络数据源 | hqbacktest 维护者 |
| 2026-08-26 | 改造数据层契约:hqbacktest 改为通过 `hqdata.api` 的 `csv` source 读取 snapshot(不再直读 CSV),DataFrame → Bar/Factor 转换与双层缓存在 hqbacktest 侧;新增 `hqdata.errors.SnapshotFileMissingError` 透传路径;Calendar 缺失返回空(对齐 tushare)。CSV 列名校验全部移交 `hqdata.sources.csv_source` | hqbacktest 维护者 |
| 2026-08-23 | 重构数据门户:`HqDataPortal` 替换为 `HqDataCsvPortal`,固定布局 `{data_root}/{source}/calendar.csv` + `stock_list|stock_daily|stock_factor/{YYYYMMDD}.csv`;`source` 名称或绝对路径均可,`CacheKey` 加入 `data_root` 防跨目录串扰 | hqbacktest 维护者 |
| 2026-08-24 | 数据层缺行/停牌/首日语义:钉死 `get_bars` 允许间隙、引入 `SnapshotFileMissingError` 区分整日文件缺失与个股缺行、`current_price` 回看 20 交易日最近有效收盘价、首日哨兵日期不抛异常、删除 `InMemoryDataPortal.get_universe` 向前回退、补双门户 parity 测试、缓存返回防御性拷贝、`.BJ` 股票默认过滤、`Bar.volume` 单位标注为「手」 | hqbacktest 维护者 |
| 2026-08-24 | 撮合与账本语义:同批撮合 SELL 先于 BUY(滚动现金)、SELL 不整手取整、`Fill.BUY` 携带非零 stamp_tax 报错、`Order.record_fill` 移除不可达 `ACCEPTED` 分支、`intents.target_quantity_for_value(0)` 按 docstring 返回 0、CLI `initial_cash` 拒绝 float 与引擎对齐、`realized_pnl` 不含费用;登记 §3.4 撮合口径表 | hqbacktest 维护者 |
Expand All @@ -269,4 +271,5 @@
| 2026-08-24 | CLI 易用性与文档真实性:console script 把 config dir + cwd 加入 sys.path;`initial_cash` 拒绝 nan/inf/float;空交易窗口、空输出目录、`--force` 覆盖;`order_value` 接受 int/str;`git_commit` 改为 hqbacktest 自身版本;README 错误码表与包布局对齐;登记 §3.8 | hqbacktest 维护者 |
| 2026-08-25 | `source` 绝对路径支持(拆为 `data_root` + 名称);`run_metadata.json` 中 `config_path` / `output_directory` / `config_output_directory` 写入相对路径(`os.path.relpath`);`validate_yyyymmdd` 用 `datetime.strptime` 拒绝假日期(保留 `"00000000"` 哨兵);性能夹具生成器改用 `datetime` 迭代;`test_console_script_runs_end_to_end` 改名 `test_python_m_runs_end_to_end` 并补一个真正测 console script 的同名测试;README「26 项 CLI 测试」改为「见 tests/cli/」;`pyproject.toml` 删除过时的「no runtime deps yet」注释 | hqbacktest 维护者 |
| 2026-08-25 | 波动率/夏普首日采样缺口修复:`metrics.compute_metrics` 不再从 `total_equity` 重新推导日收益(旧零种子会丢首日真实收益),改为直接读 `engine` 写好的 `EquityPoint.daily_return`;删除死代码 `_drawdown_series`;新增手算回归(2 日 -9% / +5.5%,`daily_volatility` ≈ 0.10253)。波动率 / Sharpe 与 `max_drawdown` 对首日盈亏的可见性现在一致 | hqbacktest 维护者 |
| 2026-08-25 | 数据层测试覆盖补齐与文档措辞澄清:6 项 `get_factor` 双门户逐值一致性断言;§3.3 删除不存在的 `get_bar(symbol, date)` 引用;`DataView.portal` 措辞改为准确表述(下划线是约定私有,不是 Python 语言级强制力);哨兵常量 `"00000000"` 收敛到 `data.validators.SENTINEL_NO_HISTORY` 一处;`test_version_matches_pyproject` 强化版本号形态校验 | hqbacktest 维护者 |
| 2026-08-25 | 数据层测试覆盖补齐与文档措辞澄清:6 项 `get_factor` 双门户逐值一致性断言;§3.3 删除不存在的 `get_bar(symbol, date)` 引用;`DataView.portal` 措辞改为准确表述(下划线是约定私有,不是 Python 语言级强制力);哨兵常量 `"00000000"` 收敛到 `data.validators.SENTINEL_NO_HISTORY` 一处;`test_version_matches_pyproject` 强化版本号形态校验 | hqbacktest 维护者 |
| 2026-08-26 | §5 边界规则表述同步至 v0.1.* 改造:`data portal` 现在通过 `hqdata.api` 的 `csv` source 读取 snapshot;上一条 8-26 修订的 CSV 列名校验移交 `hqdata.sources.csv_source` 在 §5 重述;并补 docs/strategy-api.md 中 `data_view.py` 路径修正 | hqbacktest 维护者 |
2 changes: 1 addition & 1 deletion docs/isolation.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_strategy_cannot_access_raw_portal_by_public_name():
view.portal
```

代码位置:`src/hqbacktest/data/view.py`、测试 `tests/engine/test_isolation.py`。
代码位置:`src/hqbacktest/data/data_view.py`、测试 `tests/engine/test_isolation.py`。

## 3. Universe 生效

Expand Down
Loading
Loading