Rolling BTC Futures Forecasts with a Voting Classifier
Summary
This template builds an asset-by-asset classifier ensemble for forecasting whether the next close will be higher than the current close. Its input features are a moving-average based trend measure, the stochastic oscillator, normalized true range, and a ratio of short- to longer-window volume averages. Ridge and stochastic-gradient classifiers with varied random seeds are combined by voting, then trained on aligned observations after an initial data cutoff.
A specialized machine-learning backtest routine controls the training window, retraining cadence, prediction lookback, and evaluation start. The document describes a process and implementation, not performance evidence: it reports no returns, benchmark comparison, or robustness analysis. The binary next-period target and limited feature set may not capture trading costs or risk, and the notes about avoiding forward-looking features underscore the need to check feature timing and validation for leakage. Retraining settings also affect the compute and evaluation process.
Key ideas
- The prediction target labels whether the next close exceeds the current close.
- Features combine trend, stochastic, normalized true range, and relative volume measures.
- Ridge and stochastic-gradient classifiers are combined in a voting ensemble with varied random seeds.
- Models are fitted separately for each asset and periodically retrained through a specialized backtester.
- The template reports no performance results, and feature timing must be checked to avoid look-ahead leakage.
Tags
Full text
# strategy-ml-voting-crypto
# strategy-ml-voting-crypto
## Source (MIT)
```python
# # Machine Learning with a Voting Classifier: Retraining your Model
#
# With Quantiacs you can use machine learning methods for forecasting financial time series.
#
# In this template we show how to use the Quantiacs toolkit for efficiently **retraining** your model on a rolling basis.
#
# We will work with the BTC Futures Contracts and use voting on top of a combination of **Ridge Classifiers** and **Stochastic Gradient Descent Classifiers** as implemented in [**scikit-learn**](https://scikit-learn.org/stable/).
#
# We will use a **specialized** version of the Quantiacs backtester for this purpose, which dramatically speeds up the backtesting process when the models should be retrained on a regular basis.
#
# **Need help?** Check the [**Documentation**](https://quantiacs.com/documentation/en/) and find solutions/report problems in the [**Forum**](https://quantiacs.com/community/categories) section.
#
# **More help with Jupyter?** Check the official [**Jupyter**](https://jupyter.org/) page.
#
# Once you are done, click on **Submit to the contest** and take part to our competitions.
#
# API reference:
#
# * **data**: check how to work with [data](https://quantiacs.com/documentation/en/reference/data_load_functions.html);
#
# * **backtesting**: read how to run the [simulation](https://quantiacs.com/documentation/en/reference/evaluation.html) and check the results.
%%javascript
window.IPython && (IPython.OutputArea.prototype._should_scroll = function(lines) { return false; })
// disable widget scrolling
import logging
import pandas as pd
import xarray as xr
import numpy as np
import qnt.backtester as qnbt
import qnt.ta as qnta
def create_model():
"""This is a constructor for the ML model which can be easily modified using
different models or another logic for the combination.
"""
from sklearn.ensemble import VotingClassifier
from sklearn.linear_model import SGDClassifier, RidgeClassifier
import random
# We will use a model obtained combining by voting Ridge Classifiers and SGD Classifiers
# which use several random seeds to reduce overfitting:
classifiers = []
r = random.Random(13)
for i in range(42):
classifiers.append(('ridge' + str(i), RidgeClassifier(random_state=r.randint(0, pow(2, 32) - 1)),))
classifiers.append(('sgd' + str(i), SGDClassifier(random_state=r.randint(0, pow(2, 32) - 1)),))
model = VotingClassifier(classifiers)
return model
def get_features(data):
"""Builds the features used for learning:
* a trend indicator;
* the stochastic oscillator;
* volatility;
* volume.
"""
trend = qnta.roc(qnta.lwma(data.sel(field='close'), 70), 1)
# stochastic oscillator:
k, d = qnta.stochastic(data.sel(field='high'), data.sel(field='low'), data.sel(field='close'), 14)
volatility = qnta.tr(data.sel(field='high'), data.sel(field='low'), data.sel(field='close'))
volatility = volatility / data.sel(field='close')
volatility = qnta.lwma(volatility, 14)
volume = data.sel(field='vol')
volume = qnta.sma(volume, 5) / qnta.sma(volume, 60)
volume = volume.where(np.isfinite(volume), 0)
# combine the selected four features:
result = xr.concat(
[trend, d, volatility, volume],
pd.Index(
['trend', 'stochastic_d', 'volatility', 'volume'],
name = 'field'
)
)
return result.transpose('time', 'field', 'asset')
def get_target_classes(data):
"""Builds target classes which will be later predicted."""
price_current = data.sel(field='close')
price_future = qnta.shift(price_current, -1)
class_positive = 1
class_negative = 0
target_is_price_up = xr.where(price_future > price_current, class_positive, class_negative)
return target_is_price_up
def create_and_train_models(data):
"""Create and train the models working on an asset-by-asset basis."""
asset_name_all = data.coords['asset'].values
data = data.sel(time=slice('2013-05-01',None)) # cut the noisy data head before 2013-05-01
features_all = get_features(data)
target_all = get_target_classes(data)
models = dict()
for asset_name in asset_name_all:
# drop missing values:
target_cur = target_all.sel(asset=asset_name).dropna('time', 'any')
features_cur = features_all.sel(asset=asset_name).dropna('time', 'any')
# align features and targets:
target_for_learn_df, feature_for_learn_df = xr.align(target_cur, features_cur, join='inner')
if len(features_cur.time) < 10:
# not enough points for training
continue
model = create_model()
try:
model.fit(feature_for_learn_df.values, target_for_learn_df)
models[asset_name] = model
except KeyboardInterrupt as e:
raise e
except:
logging.exception('model training failed')
return models
def predict(models, data):
"""Performs prediction and generates output weights.
Generation is performed for several days in order to speed
up the evaluation.
"""
asset_name_all = data.coords['asset'].values
weights = xr.zeros_like(data.sel(field='close'))
for asset_name in asset_name_all:
if asset_name in models:
model = models[asset_name]
features_all = get_features(data)
features_cur = features_all.sel(asset=asset_name).dropna('time','any')
if len(features_cur.time) < 1:
continue
try:
weights.loc[dict(asset=asset_name,time=features_cur.time.values)] = model.predict(features_cur.values)
except KeyboardInterrupt as e:
raise e
except:
logging.exception('model prediction failed')
return weights
# The following cell runs the backtester into **Machine Learning retraining** mode. We specify the maximal length of the training period and the interval for retraining. Note that it is possible tor retrain the model every day after submissions to the Quantiacs servers.
weights = qnbt.backtest_ml(
train=create_and_train_models,
predict=predict,
train_period=10*365, # the data length for training in calendar days
retrain_interval=365, # how often we have to retrain models (calendar days)
retrain_interval_after_submit=1, # how often retrain models after submission during evaluation (calendar days)
predict_each_day=False, # Is it necessary to call prediction for every day during backtesting?
# Set it to true if you suspect that get_features is looking forward.
competition_type='cryptofutures', # competition type
lookback_period=365, # how many calendar days are needed by the predict function to generate the output
start_date='2014-01-01', # backtest start date
build_plots=True # do you need the chart?
)
```Shown in full with attribution under the source's licence. Licence: MIT
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.