分钟做 T¶
目标:区分“信号想卖”“账户有持仓”和“当前可以卖”,验证 T+0、T+1 与昨日底仓三种研究假设的实际成交。
完成后,你会得到四个情景的成交对照、可以手工核对的现金账本,以及 intraday-t-report/ 分钟报告。两日人工价格让每笔成交和费用都能直接手算。
1. 先运行现成实验¶
按安装指南安装后,下载intraday_t.py。在该文件所在目录运行:
python intraday_t.py
python intraday_t.py --frequency 5min
两种频率都用完整分钟网格,信号出现在第二日开头。1 分钟每日 240 根,5 分钟每日 48 根。示例 bar 全部为连续交易;接入真实行情时,以 auction 标记竞价区间。
四个模式的区别:
模式 |
市场/账户假设 |
用途 |
|---|---|---|
|
股票型 ETF 只在第二日用 |
与 T+1 对照,观察可卖延迟对交易的影响 |
|
股票型 ETF 的 T+1 预设,无昨日持仓 |
验证当天买入后无法卖出 |
|
T+1,首日先买 1000 份底仓 |
次日先卖后买,检查底仓是否耗尽 |
|
黄金 ETF 的原生 T+0 profile |
在对应市场分类假设下执行同日往返 |
DEMO 是人工标识。接入自己的证券时,选择对应的市场分类,行情提供每日价格边界,详见市场规则。
2. 理解时间顺序与底仓¶
示例策略读取刚完成 bar 的收盘价:低于 10 元时买入 1000 份,高于 10 元时尝试卖出 1000 份。订单最早在下一根 bar 开盘撮合,成交到该 bar 结束才可见。
def round_trips(ctx: Context) -> None:
"""已完成 bar 收盘价低于 10 买入,高于 10 时尝试卖出持仓。"""
if ctx.session != DAYS[1]:
return
close = ctx.history(SYMBOL, bars=1)[-1]
quantity = ctx.account.positions[SYMBOL].quantity
if close < 10 and quantity == 0:
ctx.order(SYMBOL, 1000)
elif close > 10 and quantity >= 1000:
ctx.order(SYMBOL, -1000)
没有昨日底仓时,第二天新买入的 1000 份要到下一交易日才可卖。ctx.account.positions["DEMO"].quantity 是持仓;应同时检查该持仓的 sellable。这里故意只判断持仓、再尝试卖出,用订单结果展示真实约束。
底仓版本首日先买入,次日的高价信号卖出昨日股份,低价信号再买回:
def inventory_t(ctx: Context) -> None:
"""先建立昨日底仓;次日卖高买低,买回股份仍受 T+1 约束。"""
if ctx.bar_index == 0:
ctx.order(SYMBOL, 1000)
if ctx.session != DAYS[1]:
return
close = ctx.history(SYMBOL, bars=1)[-1]
quantity = ctx.account.positions[SYMBOL].quantity
if close > 10 and quantity >= 1000:
ctx.order(SYMBOL, -1000)
elif close < 10 and quantity == 0:
ctx.order(SYMBOL, 1000)
买回的份额形成新的 T+1 批次:总持仓回到 1000,可卖量仍为 0,要到下一交易日才解锁。规则覆盖按买入成交日决定批次可卖日,不会追溯把旧批次改成当天可卖。
3. 自己运行、核对四个情景¶
把以下代码依次保存为 tutorial_intraday_t.py,与下载的 intraday_t.py 放在同一目录。这里使用它的公开示例函数生成固定行情;账户与分析仍通过 DoriBT 顶层 API 调用。
from pathlib import Path
import numpy as np
from intraday_t import inventory_t, round_trips, sample
from doribt import Backtest, BarExecution, Costs, FixedTicks, RunConfig
config = RunConfig(
initial_cash=100_000,
costs=Costs(commission=".0003", minimum_commission=1),
execution=BarExecution(participation=".1", slippage=FixedTicks(1)),
backend="python",
)
expected = {
"t0": ([1000, -1000, 1000, -1000], 100383.88),
"t1": ([1000], 99996.00),
"inventory": ([1000, -1000, 1000], 100187.94),
"native": ([1000, -1000, 1000, -1000], 100383.88),
}
results = {}
for mode, (quantities, final_equity) in expected.items():
strategy = inventory_t if mode == "inventory" else round_trips
result = Backtest(sample(mode, "1min"), config=config).run(strategy)
results[mode] = result
assert [fill.quantity for fill in result.fills] == quantities
assert int(result.equity_units[-1]) == round(final_equity * 10_000)
print(mode, quantities, "期末权益", f"{result.equity[-1]:.2f}")
print("期末持仓/可卖:", result.holdings[-1, 0], result.sellable[-1, 0])
配置中的一档滑点按当期规则价位计算。在这里 ETF 的一档为 0.001 元,买入价 10.001、卖出价 10.199。不要把股票常见的 0.01 元档位套过来。每根成交量 50000 份、参与率 10%,单根上限 5000 份,因此这里的 1000 份订单可以一次成交。
1 分钟 t0 的四笔成交依次在第二日 09:32、09:33、09:34、09:35;5 分钟模式相应为 09:40、09:45、09:50、09:55。T+1 底仓模式的第一笔买入在首日,不能把它当成第二日凭空出现的初始持仓。
手算 t0:每次往返毛价差为 1000 × (10.199 − 10.001) = 198 元,买卖佣金合计 6.06 元,两次净增 2 × (198 − 6.06) = 383.88 元。滑点已进入成交价,不要把滑点成本再扣一次。
4. 把未成交也当作研究结果¶
inventory_result = results["inventory"]
for order in inventory_result.orders:
if order.reason.value != "none":
print("未完成约束:", order.session, order.quantity, order.filled, order.reason.value)
assert any(order.reason.value == "insufficient_sellable" for order in inventory_result.orders)
assert inventory_result.holdings[-1, 0] == 1000
assert inventory_result.sellable[-1, 0] == 0
# 本例无公司行动:逐根权益应等于现金加原始收盘价下的持仓价值。
reconstructed = inventory_result.cash_units + np.sum(
inventory_result.holdings * inventory_result.close_units, axis=1
)
np.testing.assert_array_equal(reconstructed, inventory_result.equity_units)
for fill in inventory_result.fills:
print(fill.timestamp, fill.quantity, fill.reference_price, fill.price, fill.fees)
insufficient_sellable 是本例预期,而不是引擎漏卖。真实策略可以先检查 sellable 再提交订单,减少无效申请;审计时仍应保留原始信号和受约束的成交结果。
上面的简化权益恒等式只适用于这份没有公司行动的输入。有应收分红、待入账股份或税款时,还需把这些项目纳入对账,见结果字段和公司行动。
5. 日频汇总,保留分钟审计¶
daily_report = inventory_result.report(periods_per_year=252)
assert len(daily_report.daily) == 2
print("日数:", len(daily_report.daily))
print("总收益率:", daily_report.stats["total_return"])
output = inventory_result.export(Path("intraday-t-report"), daily=True)
print("报告目录:", output)
python tutorial_intraday_t.py
report.json 按日末采样,account.csv、positions.csv 和逐笔成交仍保留完整分钟记录。已定义的统计值直接展示;样本不足的指标返回 None。重新运行时要换一个尚不存在的报告目录名。
如果已经安装 doribt[numba],可以把配置改为 backend="numba";也可以用下载脚本的 --backend numba 参数核对同一人工案例。预热后重复运行可复用编译缓存。
6. 接真实分钟数据前的检查表¶
时间为带
+08:00的 bar 结束时点,frequency与输入一致;独立日历加完整网格用于发现漏分钟continuous/auction按实际时段标记;竞价与盘后撮合不在本例模型内OHLC 为未复权人民币价格,成交量为股/份;不能直接把供应商的“手”当“份”
信号只用当时已完成信息;不因回测提供了下一 bar 成交量而在前一 bar 决策时使用它
参与率、最低佣金、滑点与订单有效期明确。
next_bar只尝试下一根,day才延续到首个可执行 bar 所属交易日结束T+1 按交易日推进;真实证券按当期市场分类选择 T+0/T+1 规则
需要检查部分成交时,继续运行分钟教程中的 minute.py:它把单根量限降低到 100 份,演示同一订单分多次成交及累计最低佣金。完整时间、滑点与失败语义见分钟执行契约。