多个时间区间
你可能希望以不同于底层数据源的时间框架来做出交易决策。例如,你可以选择在通过周线或月线确认趋势后,在日线上执行交易。PyBroker v2 支持将回测数据压缩为更长的时间区间,并让这些压缩后的 K 线可供你的策略使用。
区间类型
你可以使用以下三种格式中的任意一种来定义时间区间:
每 n 根 K 线 (大于
1的int):将每n根基础 K 线压缩为一根 K 线。在日线数据上使用5会每五个交易日生成一根 K 线。时长 (
str):以数字加单个单位字母(s、m、h或d)表示的固定时间跨度。传入"5m"会将 1 分钟 K 线压缩为 5 分钟 K 线。日历 (
str):使用以下选项之一,将压缩后的 K 线对齐到日历边界:
日历字符串 |
边界对齐 |
|---|---|
|
标准的每日边界。 |
|
从周一开始。 |
|
从每月 1 日开始。 |
|
从 1 月、4 月、7 月和 10 月开始。 |
|
从 1 月 1 日开始。 |
你所选择的时间区间必须始终长于被压缩的 K 线。例如,如果你从 YFinance 获取日线数据,那么 "weekly" 和 "monthly" 都是有效的时间区间。尝试使用 "daily" 或 "1h" 将引发 ValueError。
在策略中使用时间区间之前,让我们先通过直接压缩 K 线来建立一些直观认识。我们将从下载日线数据开始:
[1]:
import pybroker
from pybroker import Strategy, YFinance
pybroker.enable_data_source_cache("multiple_time_intervals")
yfinance = YFinance()
df = yfinance.query(
["AMD", "NVDA", "INTC"], start_date="1/1/2021", end_date="1/1/2026"
)
df.head()
Loading bar data...
[*********************100%***********************] 3 of 3 completed
Loaded bar data: 0:00:00
[1]:
| date | symbol | open | high | low | close | volume | adj_close | |
|---|---|---|---|---|---|---|---|---|
| 0 | 2021-01-04 | AMD | 92.110001 | 96.059998 | 90.919998 | 92.300003 | 51802600 | 92.300003 |
| 1 | 2021-01-04 | INTC | 49.889999 | 51.389999 | 49.400002 | 49.669998 | 46102500 | 44.902931 |
| 2 | 2021-01-04 | NVDA | 13.104250 | 13.652500 | 12.962500 | 13.113500 | 560640000 | 13.060796 |
| 3 | 2021-01-05 | AMD | 92.099998 | 93.209999 | 91.410004 | 92.769997 | 34208000 | 92.769997 |
| 4 | 2021-01-05 | INTC | 49.450001 | 50.830002 | 49.330002 | 50.610001 | 24866600 | 45.752716 |
压缩 K 线
compress_bars 函数可将 OHLCV 数据(可以是 Pandas DataFrame 或 BarData)转换为更长的时间区间,并以新的 BarData 对象形式返回结果。每根压缩后的 K 线都以其所包含的最后一根基础 K 线的日期作为时间戳。
在将基础 K 线分组为压缩 K 线时,数据将按以下方式聚合:
开盘价: 取自第一根基础 K 线。
最高价 / 最低价: 取最高价中的最高值和最低价中的最低值。
收盘价: 取自最后一根基础 K 线。
成交量: 各成交量之和。
VWAP: 成交量加权平均价。
自定义列: 取该周期内的最后一个值(例如 YFinance 的
adj_close)。
你还必须提供 base_timeframe 参数,以声明输入 K 线的间隔(例如,日线数据对应 "1d")。
让我们将 AMD 压缩为日历周,并使用 bars_to_df 将结果显示为 Pandas DataFrame:
[2]:
from pybroker import compress_bars
from pybroker.common import bars_to_df
amd_df = df[df["symbol"] == "AMD"]
bars_to_df(compress_bars(amd_df, "weekly", base_timeframe="1d")).head()
[2]:
| date | open | high | low | close | volume | adj_close | |
|---|---|---|---|---|---|---|---|
| 0 | 2021-01-08 | 92.110001 | 96.400002 | 89.459999 | 94.580002 | 220635900.0 | 94.580002 |
| 1 | 2021-01-15 | 94.029999 | 99.230003 | 87.860001 | 88.209999 | 279733900.0 | 88.209999 |
| 2 | 2021-01-22 | 89.559998 | 95.949997 | 87.239998 | 92.790001 | 205817500.0 | 92.790001 |
| 3 | 2021-01-29 | 94.139999 | 95.739998 | 85.019997 | 85.639999 | 291661400.0 | 85.639999 |
| 4 | 2021-02-05 | 86.830002 | 89.480003 | 84.660004 | 87.900002 | 169582500.0 | 87.900002 |
每 n 根 K 线压缩的方式相同。在此示例中,每 5 根日线合并为一根 K 线:
[3]:
bars_to_df(compress_bars(amd_df, 5, base_timeframe="1d")).head()
[3]:
| date | open | high | low | close | volume | adj_close | |
|---|---|---|---|---|---|---|---|
| 0 | 2021-01-08 | 92.110001 | 96.400002 | 89.459999 | 94.580002 | 220635900.0 | 94.580002 |
| 1 | 2021-01-15 | 94.029999 | 99.230003 | 87.860001 | 88.209999 | 279733900.0 | 88.209999 |
| 2 | 2021-01-25 | 89.559998 | 95.949997 | 87.239998 | 94.129997 | 260904400.0 | 94.129997 |
| 3 | 2021-02-01 | 94.910004 | 95.720001 | 84.660004 | 87.660004 | 278933800.0 | 87.660004 |
| 4 | 2021-02-08 | 88.489998 | 91.989998 | 86.879997 | 91.470001 | 174863100.0 | 91.470001 |
多时间框架策略
在构建回测时,你可以通过向 add_execution 传递 intervals 参数来使用更高的时间框架。随后,你的执行函数便可通过 ctx.interval 访问这些压缩后的 K 线,该方法会返回一个只读的 IntervalContext。
使用 intervals 参数仅提供压缩后的 K 线。除非像本文档后续内容那样显式绑定,否则指标和模型永远不会在这些区间上计算。
为防止前视偏差,ctx.interval 只会公开 已完成 的 K 线。例如,当前正在形成的这一周或这一月永远不可见,从而确保未来数据不会泄漏到你的每日交易决策中。
在下面的策略中,我们将在日线上执行交易,同时使用更长的时间区间来生成不同的交易信号:
月线(市场状态): 仅当最后一根已完成的月线收盘价高于三个月前的收盘价时才建仓。
周线(趋势): 仅当最后一根已完成的周线收盘价高于十周前的收盘价时才建仓,跌破时平仓。
日线(择时): 在首根日线收盘价高于最后一根已完成的周线收盘价时建仓。
[4]:
def buy_with_trend(ctx):
weekly = ctx.interval("weekly")
monthly = ctx.interval("monthly")
# Wait until enough completed weekly and monthly bars exist.
if len(weekly.close) < 10 or len(monthly.close) < 4:
return
regime_up = monthly.close[-1] > monthly.close[-4]
trend_up = weekly.close[-1] > weekly.close[-10]
pos = ctx.long_pos()
if not pos and regime_up and trend_up and ctx.close[-1] > weekly.close[-1]:
ctx.buy_shares = 100
elif pos and not trend_up:
ctx.sell_all_shares()
strategy = Strategy(yfinance, start_date="1/1/2021", end_date="1/1/2026")
strategy.add_execution(
buy_with_trend,
["AMD", "NVDA", "INTC"],
intervals=["weekly", "monthly"],
)
result = strategy.backtest(timeframe="1d")
result.metrics_df.head(20)
Backtesting: 2021-01-01 00:00:00 to 2026-01-01 00:00:00
Loaded cached bar data.
Test split: 2021-01-04 00:00:00 to 2025-12-31 00:00:00
100% (1255 of 1255) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[4]:
| name | value | |
|---|---|---|
| 0 | trade_count | 25 |
| 1 | initial_market_value | 100000.0 |
| 2 | end_market_value | 118433.0 |
| 3 | total_pnl | 18433.0 |
| 4 | unrealized_pnl | 0.0 |
| 5 | total_return_pct | 18.433 |
| 6 | total_profit | 28083.0 |
| 7 | total_loss | -9650.0 |
| 8 | total_fees | 0.0 |
| 9 | max_drawdown | -9555.0 |
| 10 | max_drawdown_pct | -7.797903 |
| 11 | max_drawdown_date | 2025-05-19 00:00:00 |
| 12 | win_rate | 60.0 |
| 13 | loss_rate | 40.0 |
| 14 | winning_trades | 15 |
| 15 | losing_trades | 10 |
| 16 | avg_pnl | 737.32 |
| 17 | avg_return_pct | 18.0996 |
| 18 | avg_trade_bars | 57.08 |
| 19 | avg_profit | 1872.2 |
将指标绑定到区间
要在压缩 K 线上计算指标,请使用 Indicator.intervals(…) 将其绑定到一个或多个区间。
下面的示例更新了周线趋势规则,将周线收盘价与基于周线计算的 10 根 K 线 SMA 进行比较:
[5]:
from pybroker.vect import sumv
sma_10 = pybroker.indicator("sma_10", lambda data: sumv(data.close, 10) / 10)
def buy_with_indicator(ctx):
weekly = ctx.interval("weekly")
monthly = ctx.interval("monthly")
# Wait until enough completed weekly and monthly bars exist.
if len(weekly.close) < 10 or len(monthly.close) < 4:
return
wk_sma = weekly.indicator("sma_10")
regime_up = monthly.close[-1] > monthly.close[-4]
trend_up = weekly.close[-1] > wk_sma[-1]
pos = ctx.long_pos()
if not pos and regime_up and trend_up and ctx.close[-1] > wk_sma[-1]:
ctx.buy_shares = 100
elif pos and not trend_up:
ctx.sell_all_shares()
strategy = Strategy(yfinance, start_date="1/1/2021", end_date="1/1/2026")
strategy.add_execution(
buy_with_indicator,
["AMD", "NVDA", "INTC"],
indicators=sma_10.intervals("weekly"),
intervals="monthly",
)
PyBroker 会自动将已绑定的区间与执行函数的 intervals 参数取并集,因此示例中的 "weekly" 区间可通过 ctx.interval(“weekly”) 访问。此时 intervals 参数只需指定 "monthly",用于提供原始的月线数据。
请注意,绑定会覆盖在数据源基础时间框架上计算该指标的行为。若要同时在数据源的基础时间框架上计算该指标,需向 Indicator.intervals() 传入 "base"。
[6]:
result = strategy.backtest(timeframe="1d")
result.metrics_df.head(20)
Backtesting: 2021-01-01 00:00:00 to 2026-01-01 00:00:00
Loaded cached bar data.
Computing indicators...
100% (3 of 3) |##########################| Elapsed Time: 0:00:00 Time: 0:00:00
Test split: 2021-01-04 00:00:00 to 2025-12-31 00:00:00
100% (1255 of 1255) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[6]:
| name | value | |
|---|---|---|
| 0 | trade_count | 36 |
| 1 | initial_market_value | 100000.0 |
| 2 | end_market_value | 120278.0 |
| 3 | total_pnl | 20361.0 |
| 4 | unrealized_pnl | -83.0 |
| 5 | total_return_pct | 20.361 |
| 6 | total_profit | 31960.0 |
| 7 | total_loss | -11599.0 |
| 8 | total_fees | 0.0 |
| 9 | max_drawdown | -9555.0 |
| 10 | max_drawdown_pct | -7.398317 |
| 11 | max_drawdown_date | 2025-11-21 00:00:00 |
| 12 | win_rate | 47.222222 |
| 13 | loss_rate | 52.777778 |
| 14 | winning_trades | 17 |
| 15 | losing_trades | 19 |
| 16 | avg_pnl | 565.583333 |
| 17 | avg_return_pct | 10.874444 |
| 18 | avg_trade_bars | 38.972222 |
| 19 | avg_profit | 1880.0 |
在区间上训练模型
你可以用相同的方式,通过 ModelSource.intervals(…) 绑定模型。PyBroker 接着会使用该区间的压缩 K 线以及任何已注册的指标,为每个区间训练模型。随后,你可以在该区间的上下文上调用 preds 方法来获取该区间的预测结果。
本示例训练一个 LinearRegression 模型,根据周线收盘价预测下一周的收益率:
[7]:
from sklearn.linear_model import LinearRegression
def train_weekly(symbol, train_data, test_data):
# Predict the next weekly return from the weekly close.
returns = train_data["close"].pct_change().shift(-1)
train_rows = train_data.assign(pred=returns).dropna()
model = LinearRegression()
model.fit(train_rows[["close"]], train_rows[["pred"]])
return model, ["close"]
model_weekly = pybroker.model("weekly_slr", train_weekly)
def hold_with_model(ctx):
preds = ctx.interval("weekly").preds("weekly_slr")
if len(preds) == 0:
return
if not ctx.long_pos():
if preds[-1] > 0:
ctx.buy_shares = 100
elif preds[-1] < 0:
ctx.sell_all_shares()
strategy = Strategy(yfinance, start_date="1/1/2021", end_date="1/1/2026")
strategy.add_execution(
hold_with_model,
["AMD", "NVDA", "INTC"],
models=model_weekly.intervals("weekly"),
)
在 Walkforward Analysis 过程中,区间模型训练数据与测试数据之间的 lookahead 将以压缩 K 线为单位强制执行,以防止未来数据泄漏。
[8]:
result = strategy.walkforward(windows=3, train_size=0.5, timeframe="1d")
result.metrics_df.head(20)
Backtesting: 2021-01-01 00:00:00 to 2026-01-01 00:00:00
Loaded cached bar data.
Train split: 2021-01-07 00:00:00 to 2022-04-04 00:00:00
Finished training models: 0:00:00
Test split: 2022-04-05 00:00:00 to 2023-07-05 00:00:00
100% (313 of 313) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Train split: 2022-04-05 00:00:00 to 2023-07-05 00:00:00
Finished training models: 0:00:00
Test split: 2023-07-06 00:00:00 to 2024-10-01 00:00:00
100% (313 of 313) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Train split: 2023-07-06 00:00:00 to 2024-10-01 00:00:00
Finished training models: 0:00:00
Test split: 2024-10-02 00:00:00 to 2025-12-31 00:00:00
100% (313 of 313) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[8]:
| name | value | |
|---|---|---|
| 0 | trade_count | 10 |
| 1 | initial_market_value | 100000.0 |
| 2 | end_market_value | 123627.0 |
| 3 | total_pnl | 23627.0 |
| 4 | unrealized_pnl | 0.0 |
| 5 | total_return_pct | 23.627 |
| 6 | total_profit | 25186.0 |
| 7 | total_loss | -1559.0 |
| 8 | total_fees | 0.0 |
| 9 | max_drawdown | -13776.0 |
| 10 | max_drawdown_pct | -12.158334 |
| 11 | max_drawdown_date | 2025-04-08 00:00:00 |
| 12 | win_rate | 90.0 |
| 13 | loss_rate | 10.0 |
| 14 | winning_trades | 9 |
| 15 | losing_trades | 1 |
| 16 | avg_pnl | 2362.7 |
| 17 | avg_return_pct | 44.464 |
| 18 | avg_trade_bars | 192.6 |
| 19 | avg_profit | 2798.444444 |