March cohort is now open: How to secure your spot:

Easily cross-validate parameters to boost your trading strategy

Trading strategies often rely on parameters.

To enhance and effectively cross-validate these parameters can provide a competitive advantage in the market.

However, reliable cross-validation strategies can lead to look-ahead bias and other pitfalls that can lead to overestimating a strategy’s performance.

In today’s newsletter, we’ll use VectorBT PRO to easily implement a variety of sophisticated cross-validation methods with just a few lines of code.

Ready?

Easily cross-validate parameters to boost your trading strategy

VectorBT PRO offers several features that are highly beneficial for traders.

It allows for lightning-fast testing of trading strategies over historical data using a vectorized approach.

The framework supports extensive customization and optimization enabling traders to fine-tune strategies according to specific market conditions or personal trading styles.

VectorBT PRO is designed to handle large datasets and deal with complex analyses efficiently. It is tightly integrated with pandas which makes it easy to fit into existing data processing pipelines.

Let’s see how it works

Imports and set up

Let’s import VBT PRO and the few libraries relevant to our analysis.

import numpy as np
from pandas.tseries.frequencies import to_offset
import vectorbtpro as vbt
vbt.settings.set_theme("dark")

Grab the data for your favorite asset. We’ll use AAPL.

SYMBOL = "AAPL"
START = "2010"
END = "now"
TIMEFRAME = "day"

data = vbt.YFData.pull(
    SYMBOL,
    start=START,
    end=END,
    timeframe=TIMEFRAME
)

Cross validation schema

Next, we’ll set up a “splitter,” which divides a date range into smaller segments according to a chosen schema. For instance, lets allocate 12 months for training data and another 12 months for testing data, with this cycle repeating every 3 months.

TRAIN = 12
TEST = 12
EVERY = 3
OFFSET = "MS"

splitter = vbt.Splitter.from_ranges(
    data.index, 
    every=f"{EVERY}{OFFSET}", 
    lookback_period=f"{TRAIN + TEST}{OFFSET}",
    split=(
        vbt.RepFunc(lambda index: index < index[0] + TRAIN * to_offset(OFFSET)),
        vbt.RepFunc(lambda index: index >= index[0] + TRAIN * to_offset(OFFSET)),
    ),
    set_labels=["train", "test"]
)
splitter.plots().show_png()

First we segment the data into training and testing periods based on a specified frequency and a combined period of TRAIN + TEST months.

The split argument defines the training set as the first TRAIN months and the testing set as the subsequent TEST months in each split, while the set_labels argument names these segments.

The splitter.plots().show_png() command results in the following visualization:

Easily cross-validate parameters to boost your trading strategy. To cross-validate parameters can provide a competitive advantage in the market.

In the first subplot, we see that each split (or row) contains adjacent training and testing sets, progressively rolling from past to present.

The second subplot illustrates the overlap of each data point across different ranges. Tip: For non-overlapping testing sets, use the setting EVERY = TRAIN.

Parameter optimization

Next, we’ll create a function to execute a trading strategy within a specified date range using a single parameter set, returning one key metric. Our strategy will be a simple EMA crossover combined with an ATR trailing stop.

def objective(data, fast_period=10, slow_period=20, atr_period=14, atr_mult=3):
    fast_ema = data.run("talib:ema", fast_period, short_name="fast_ema", unpack=True)
    slow_ema = data.run("talib:ema", slow_period, short_name="slow_ema", unpack=True)
    atr = data.run("talib:atr", atr_period, unpack=True)
    pf = vbt.PF.from_signals(
        data, 
        entries=fast_ema.vbt.crossed_above(slow_ema), 
        exits=fast_ema.vbt.crossed_below(slow_ema), 
        tsl_stop=atr * atr_mult, 
        save_returns=True,
        freq=TIMEFRAME
    )
    return pf.sharpe_ratio

print(objective(data))

By decorating our function with parameterized, we enable objective to accept a list of parameters and execute them across all combinations. We’ll then further enhance the function with another decorator, split, which runs the strategy on each date range specified by the splitter.

param_objective = vbt.parameterized(
    objective,
    merge_func="concat",
    mono_n_chunks="auto",  # merge parameter combinations into chunks
    execute_kwargs=dict(engine="pathos")  # run chunks in parallel using Pathos
)
cv_objective = vbt.split(
    param_objective,
    splitter=splitter, 
    takeable_args=["data"],  # select date range from data
    merge_func="concat", 
    execute_kwargs=dict(show_progress=True)
)

sharpe_ratio = cv_objective(
    data,
    vbt.Param(np.arange(10, 50), condition="slow_period - fast_period >= 5"),
    vbt.Param(np.arange(10, 50)),
    vbt.Param(np.arange(10, 50), condition="fast_period <= atr_period <= slow_period"),
    vbt.Param(np.arange(2, 5))
)
print(sharpe_ratio)

This tests over 3 million combinations of date ranges and parameters in just a few minutes.

Analyze the results

Let’s analyze the results by segmenting the fast and slow EMA periods. It highlights the minimal variation in the Sharpe ratio from the training to the testing set across at least 50% of the splits, where blue indicates a positive change.

sharpe_ratio_diff = test_sharpe_ratio - train_sharpe_ratio
sharpe_ratio_diff_median = sharpe_ratio_diff.groupby(
    ["fast_period", "slow_period"]
).median()
sharpe_ratio_diff_median.vbt.heatmap(
    trace_kwargs=dict(colorscale="RdBu")
).show_png()

The result is a heatmap showing the various Sharpe ratios across the slow and fast period combinations.

Easily cross-validate parameters to boost your trading strategy. To cross-validate parameters can provide a competitive advantage in the market.

Next steps

Although you might have developed a promising strategy on paper, cross-validating it is essential to confirm its consistent performance over time and to ensure it’s not merely a result of random fluctuations. Apply the techniques you learned here to your own strategy.