# Time-series cross-validation (/features/optimization/time-series-cross-validation)

## Purged CV \[#purged-cv]

New in v2024.4.1

✅ Added support for walk-forward cross-validation (CV) with purging, as well as combinatorial
CV with purging and embargoing, based on Marcos Lopez de Prado's
[Advances in Financial Machine Learning](https://www.wiley.com/en-us/Advances+in+Financial+Machine+Learning-p-9781119482086).

```python title="Create and plot a combinatorial splitter with purging and embargoing"
>>> splitter = vbt.Splitter.from_purged_kfold(
...     vbt.date_range("2024", "2025"),
...     n_folds=10,
...     n_test_folds=2,
...     purge_td="3 days",
...     embargo_td="3 days"
... )
>>> splitter.plots().show()
```

Purged combinatorial cross-validation folds with train and test sets during 2024. [Figure data (JSON)](/assets/figures/features/optimization/purged-cross-validation.dfd99f035a1b.json)

## CV decorator \[#cv-decorator]

New in 1.8.1

✅ Most cross-validation tasks involve testing a grid of parameter combinations on the training data,
selecting the best parameter combination, and validating it on the test data. This process
must be repeated for each split. The cross-validation decorator combines the parameterized and
split decorators to automate this task.

```python title="Cross-validate a SMA crossover using random search"
>>> @vbt.cv_split(
...     splitter="from_rolling",
...     splitter_kwargs=dict(length=365, split=0.5, set_labels=["train", "test"]),
...     takeable_args=["data"],
...     parameterized_kwargs=dict(random_subset=100, seed=42),
...     merge_func="concat"
... )
... def sma_crossover_cv(data, fast_period, slow_period, metric):
...     fast_sma = data.run("sma", fast_period, hide_params=True)
...     slow_sma = data.run("sma", slow_period, hide_params=True)
...     entries = fast_sma.real_crossed_above(slow_sma)
...     exits = fast_sma.real_crossed_below(slow_sma)
...     pf = vbt.PF.from_signals(data, entries, exits, direction="both")
...     return pf.deep_getattr(metric)

>>> sma_crossover_cv(
...     vbt.YFData.pull("BTC-USD", start="4 years ago"),
...     vbt.Param(np.arange(20, 50), condition="x < slow_period"),
...     vbt.Param(np.arange(20, 50)),
...     "trades.expectancy"
... )
```

Split 7/7: 100%

```text title="Output"
split  set    fast_period  slow_period
0      train  20           25               8.015725
       test   20           23               0.573465
1      train  40           48              -4.356317
       test   39           40               5.666271
2      train  24           45              18.253340
       test   22           36             111.202831
3      train  20           31              54.626024
       test   20           25              -1.596945
4      train  25           48              41.328588
       test   25           30               6.620254
5      train  26           32               7.178085
       test   24           29               4.087456
6      train  22           23              -0.581255
       test   22           31              -2.494519
dtype: float64
```

!!! info "Tutorial"
    Learn more in the [Cross-validation](/tutorials/cross-validation) tutorial.

## Split decorator \[#split-decorator]

New in 1.8.1

✅ Normally, to run a function on each split, you need to build a splitter specifically targeted
at the input data provided to the function. This means that each time the input data changes, you must
recreate the splitter. The split decorator automates this process by wrapping the function,
giving it access to all arguments so it can make splitting decisions as needed.
Essentially, it can "infect" any Python function with splitting functionality 🦠

```python title="Get total return from holding in each quarter"
>>> @vbt.split(
...     splitter="from_grouper",
...     splitter_kwargs=dict(by="Q"),
...     takeable_args=["data"],
...     merge_func="concat"
... )
... def get_quarter_return(data):
...     return data.returns.vbt.returns.total()

>>> data = vbt.YFData.pull("BTC-USD")
>>> get_quarter_return(data.loc["2021"])
Date
2021Q1    1.005805
2021Q2   -0.407050
2021Q3    0.304383
2021Q4   -0.037627
Freq: Q-DEC, dtype: float64

>>> get_quarter_return(data.loc["2022"])
Date
2022Q1   -0.045047
2022Q2   -0.572515
2022Q3    0.008429
2022Q4   -0.143154
Freq: Q-DEC, dtype: float64
```

!!! info "Tutorial"
    Learn more in the [Cross-validation](/tutorials/cross-validation) tutorial.

## Splitter \[#splitter]

New in 1.8.0

✅ Splitters in [scikit-learn](https://scikit-learn.org/stable/) are not ideal for validating ML-based
and rule-based trading strategies. VBT provides a juggernaut class that supports many splitting
schemes that are safe for backtesting, including rolling windows, expanding windows, time-anchored windows,
random windows for block bootstraps, and even Pandas-native `groupby` and `resample` instructions such as
"M" for monthly frequency. As a bonus, the produced splits can be easily analyzed and
visualized! For example, you can detect any split or set overlaps, convert all splits into a single
boolean mask for custom analysis, group splits and sets, and analyze their distribution relative to each other.
This class contains more lines of code than the entire [backtesting.py](https://github.com/kernc/backtesting.py)
package, so do not underestimate the new king in town! 🦏

```python title="Roll a 360-day window and split it equally into train and test sets"
>>> data = vbt.YFData.pull("BTC-USD", start="4 years ago")
>>> splitter = vbt.Splitter.from_rolling(
...     data.index,
...     length="360 days",
...     split=0.5,
...     set_labels=["train", "test"],
...     freq="daily"
... )
>>> splitter.plots().show()
```

Rolling 360-day BTC-USD windows divided equally into train and test sets. [Figure data (JSON)](/assets/figures/features/optimization/splitter.102d7b03605c.json)

!!! info "Tutorial"
    Learn more in the [Cross-validation](/tutorials/cross-validation) tutorial.
