How to create your own hedging trading algorithm in python

In this article, I explain how I created an artificial intelligence to perform automated trading for me on a daily basis.

With modern advances in machine learning and easy access to online data, it has never been easier to participate in quantitative trading. To make things even better, cloud tools like AWS make it easy to turn trading ideas into real, fully functional trading bots. I have been working with financial data for the past few years and recently decided to take the plunge and create a fully automated strategy.

Strategy

Since this is my first automation strategy, I decided to keep it fairly simple. I would use ML models to make informed decisions for swing trading on SPY (S&P 500 Index). I decided to use SPY as my preferred asset because it offers relatively low risk if something goes wrong with my strategy.

Goal: Swing trade SPY using informed buy/sell points determined by machine learning models.

Data

To make informed decisions about when to trade SPY, I trained an ML model that can predict trades based on daily historical data from various financial sectors and U.S. Treasury bills. I extracted approximately 20 years of daily data from these assets using the open source YFinance API.

import yfinance as yf
import pandas as pd

SPY_daily = yf.download('SPY')
energy_daily = yf.download('XLE')
materials_daily = yf.download('XLB')
industrial_daily = yf.download('XLI')
utilities_daily = yf.download('XLU')
health_daily = yf.download('XLV')
financial_daily = yf.download('XLF')
consumer_discretionary_daily = yf.download('XLY')
consumer_staples_daily = yf.download('XLP')
technology_daily = yf.download('XLK')
real_estate_daily = yf.download('VGSIX')
TYBonds_daily = yf.download('^TNX')
VIX_daily = yf.download('^VIX')

Below is all the data I pulled for the model over the past 20 years.

Original historical data graph

In the jupyter notebook, the data looks like this, each row is a separate date.

Raw historical data

Feature Engineering

Then, using the raw historical data I extracted, I generated additional features for my model. I exported a variety of technical analysis indicators, including simple moving averages, volatility, and the relative strength index (RSI), among others. To provide more variety, I calculated these technical indicators using 7, 20, 50, and 200 day windows.

def SMA(df, feature, window_size):
    new_col = 'MA' + feature + str(window_size)
    df[new_col] = df[feature].rolling(window=window_size).mean()
    return df

def Volitility(df, feature, window_size):
    new_col = 'VOLITILITY' + feature + str(window_size)
    returns = np.log(df[feature]/df[feature].shift())
    returns.fillna(0, inplace=True)
    df[new_col] = returns.rolling(window=window_size).std()*np.sqrt(window_size)
    return df

def RSI(df, feature, window_size):
    new_col = 'RSI' + feature + str(window_size)
    delta = df[feature].diff()
    delta = delta[1:]
    up, down = delta.clip(lower=0), delta.clip(upper=0)
    roll_up = up.rolling(window_size).mean()
    roll_down = down.abs().rolling(window_size).mean()
    RS = roll_up / roll_down
    RSI = 100.0 - (100.0 / (1.0 + RS))
    df[new_col] = RSI
    return df

Data label

Once all my features were derived, the hard part came, creating a way to label the data so that the machine learning model could be used in my swing trading strategy.

It is well known in the financial world that using historical data to predict future prices is nearly impossible. Most securities do not follow any clear statistical distribution, and attempts to build regression models to predict future prices are almost always completely useless.

Instead, it is better to create classification labels and use a classification model to predict the probability of certain “events”.

Triple barrier method

The Triple Barrier Method is an intuitive way to label financial data for machine learning models to predict trading outcomes. This method is taken from Marcos Lopez de Prado’s book “Advances in Financial Machine Learning” (which I highly recommend).

Here’s how the Triple Barrier Method works: First, determine the time frame you want to hold the trade. Let us define these 100 trading days as 100 trading days. Then, let’s determine an ideal take-profit threshold for any trade. We set it to 1 xcurrent market volatility. Finally, let’s set a theoretical stop loss for any trade. For demonstration purposes, we will also set it to 1xof current market volatility. These three conditions are our “three levels”.

Now that we have the barriers in place, we use the following steps to create a label for the spy trade:

1. Select a date we want to mark in the financial time series

2. 100 trading days in advance. Create a vertical barrier here. This barrier represents if the trade was held too long without a stop loss or hitting our take profit.

3. Create a horizontal barrier above 1x market volatility on our date. This barrier represents whether the trade has reached our take profit threshold.

4. Create another horizontal barrier with lower market volatility than our date. This barrier represents whether the transaction has been stopped.

5. Clearly mark our dates based on whether SPY hits a take-profit barrier, hits a stop-loss barrier, or is held too long

The final result for a single date will look like this:

triple barrier method

The example above shows the triple hurdle method for day trading in February 2017. The trade did not reach our profit or stop loss thresholds, but hit the vertical barrier first.

For my strategy, I adapted the triple barrier approach to only use two tags instead of three tags. If a theoretical trade on a given date reaches my take profit threshold, I label it “Profitable”, if not, I label it “No Profit”. This will make my data suitable for training a binary classification model, which will be easier to fine-tune. I also adjusted the take profit threshold to 2 times the current market volatility and the maximum holding period to only10 daysso that my strategy can trade faster.

Labeling my data in this way will help train my model to only apply to more immediate, large spend transactions.

def get_Daily_Volatility(close,span0=20):
    # simple percentage returns
    df0=close.pct_change()
    # 20 days, a month EWM's std as boundary
    df0=df0.ewm(span=span0).std()
    df0.dropna(inplace=True)
    return df0

def get_3_barriers(daily_volatility, price):
    #create a container
    barriers = pd.DataFrame(columns=['days_passed',
              'price', 'vert_barrier', \
              'top_barrier', 'bottom_barrier'], \
               index = daily_volatility.index)

    for day, vol in daily_volatility.iteritems():
        days_passed = len(daily_volatility.loc[daily_volatility.index[0] : day])
        #set the vertical barrier
        if (days_passed + t_final < len(daily_volatility.index) and t_final != 0):
            vert_barrier = daily_volatility.index[days_passed + t_final]
        else:
            vert_barrier = np.nan
        #set the top barrier
        if upper_lower_multipliers[0] > 0:
            top_barrier = prices.loc[day] + prices.loc[day] * upper_lower_multipliers[0] * vol
        else:
            #set it to NaNs
            top_barrier = pd.Series(index=prices.index)
        #set the bottom barrier
        if upper_lower_multipliers[1] > 0:
            bottom_barrier = prices.loc[day] - prices.loc[day] * upper_lower_multipliers[1] * vol
        else:
            #set it to NaNs
            bottom_barrier = pd.Series(index=prices.index)
        barriers.loc[day, ['days_passed', 'price', 'vert_barrier','top_barrier', 'bottom_barrier']] = \
            days_passed, prices.loc[day], vert_barrier, \
            top_barrier, bottom_barrier

    return barriers

def get_labels(barriers):

    labels = []
    size = [] # percent gained or lost

    for i in range(len(barriers.index)):
        start = barriers.index[i]
        end = barriers.vert_barrier[i]
        if pd.notna(end):
            # assign the initial and final price
            price_initial = barriers.price[start]
            price_final = barriers.price[end]
            # assign the top and bottom barriers
            top_barrier = barriers.top_barrier[i]
            bottom_barrier = barriers.bottom_barrier[i]
            #set the profit taking and stop loss conditons
            condition_pt = (barriers.price[start: end] >= top_barrier).any()
            condition_sl = (barriers.price[start: end] <= bottom_barrier).any()
            #assign the labels
            if condition_pt:
                labels.append(1)
            else:
                labels.append(0)
            size.append((price_final - price_initial) / price_initial)
        else:
            labels.append(np.nan)
            size.append(np.nan)

    return labels, sizes

# how many days we hold the stock which set the vertical barrier
t_final = 10
#the up and low boundary multipliers
upper_lower_multipliers = [2, 2]
#allign the index

vol_df = get_Daily_Volatility(full_df.SPY_Close)
prices = full_df.SPY_Close[vol_df.index]
barriers = get_3_barriers(vol_df, prices)
barriers.index = pd.to_datetime(barriers.index)
labs, size = get_labels(barriers)
full_df = full_df[full_df.index.isin(barriers.index)]

Model

I try to use many different models to make smart trade predictions. I first tried the LSTM deep neural network, which I have found very useful in the past for understanding complex time series. However, after running multiple experiments, I couldn’t get a result without overfitting, and the out-of-sample results were very poor.

I ultimately decided to use a model that is more suitable for tabular data: CatBoost. This open source gradient boosting model has a Python library that is super intuitive to use and performs very well.

Data preparation

In order to use my data in a CatBoost model, it needs to be flattened so that it contains not just data for a single date, but also historical data so that the model can understand what historical trends influence good trades. I wrote this expand_features function that expands each feature, getting the percentage change of each feature relative to the first 100 data points in the time series.

def percentage_change(initial,final):
    return ((final - initial) / initial)

def expand_features(full_df):

    window=100

    new_df = pd.DataFrame()
    for col in full_df.columns:
        print(col)
        if not col.startswith('label'):
            column = full_df[col]
            for i in range(1, window):
                shifted = column.shift(i)
                new_df['Shifted' + str(i) + col] = percentage_change(shifted, column)
        else:
            new_df[col] = full_df[col]

    return new_df

full_df = expand_features(full_df)

The data now fed into my Catboost model looks like this, where each column is expanded to include the percentage change over the past x days. There are now 20,301 unique features entered into the model, representing a broad range of historical market indicators.

Extended data with historical characteristics, annotated by “ShiftedX”

Catboost model

The data is then fit to the following model to predict whether buying SPY on each date will result in a profitable trade.

classification_params = {'loss_function':'Logloss',
          'eval_metric':'AUC',
          'early_stopping_rounds': 2,
          'verbose': 200,
          'random_seed': SEED
         }

    model = CatBoostClassifier(**classification_params)
    model.fit(X_train, Y_train,
            eval_set=(X_test, Y_test),
            use_best_model=True, )

Model analysis

Cross-validation

To validate my model, I split the dataset into a training set and an “out-of-sample” test set. The train dataset consists of data from March 2000 to August 2020, and my test set consists of data from August 2020 to November 2022. composition.

KFold cross-validation was used to determine model performance using my training data. This involves splitting the training data into 5 different parts. Then, rotate 4 of the 5 segments to train the model, while using 1 segment to validate and fine-tune model performance.

Model performance

Once I was confident about the model architecture, I trained the final model using the entire training data set and tested sample performance on the test data.

The results showed that the model performed much better than I expected, with a testROC AUC of 0.69. Although this is not a stunning ROC score, I was impressed to see this result in the financial data. Financial data is incredibly random, and outcomes are difficult to predict. Anything better than flipping a coin is considered an advantage in our trading strategy.

The model appears to be overfitted, with a trainingROC AUC of 0.98,much better than the test data performanceof 0.69. Honestly, I’m not too worried about this overfitting because the model clearly yields an advantage and looks much better than guesswork. However, I think there are definitely more opportunities to generalize this model to prevent overfitting. I believe this can be done by merging more data or using a bagging ensemble like Random Forest to fit my data.

ROC curve for my final model

Feature analysis

The feature importance of my model looks very interesting. The model primarily prioritizes the volatility of various financial sectors to predict whether buying SPY on any given date will produce a winning trade. Whatever the reason, features based on the energy sector have always made a huge contribution to my models.

The fact that my model prioritizes volatility makes perfect sense. Winning trades are characterized by large increases in our assets, and market volatility is a great way to predict large moves.

Importance of SHAP features to my model

Summary

I have now created a model that takes in historical financial data for a given date and predicts the probability that buying SPY will result in a significantly profitable trade over the next 10 days

Input: Historical financial information for a given date

Output: Probability that buying SPY will result in a meaningfully profitable trade in the next 10 days

Backtest

Now that I have a functional model that serves as the predictive engine for our strategy, I can get to the really fun part: figuring out how well the swing trading strategy performs.

Set the optimal threshold

Before actually testing the model, I first need to determine the probability threshold generated by the model that should trigger a trade.

I could simply say “Buy SPY if the model yields probability greater than 50%“. However, this is usually not the best case for binary classification models, as we must consider the risk of false positives and false negatives. Instead, I determine the optimal threshold by finding the output probability that yields the largestF score on the training data. Fortunately, this is easy in Python! The optimal transaction threshold ended up being0.46.

from sklearn.metrics import precision_recall_curve, f1_score
import numpy as np

#Create a Precision/Recall curve for our training data
precision_train, recall_train, pr_thresholds_train =
    precision_recall_curve(Y_train, probabilities_train)
fscore_train =
    2 * (precision_train * recall_train) / (precision_train + recall_train)

#Find optimal thresh on PR curve train
ix = np.argmax(fscore_train)
optimal_threshold = pr_thresholds_train[ix]

Perform backtest

I then wrote a function in Python to determine the profitability of our strategy on out-of-sample data.

This strategy involves swing trading SPY as follows:

  1. Extract financial data for a specific date, generate the necessary features and feed it into our in the model.
  2. Buy SPY if our model produces a probability greater than46%
  3. Set our trade to a take profit of 2 times the current market volatility and set a reasonable stop losses to limit risk.

To determine how many shares to purchase on a given trade, I simply divide our current portfolio balance by 5x SPY’s current market cap. This would allow us to easily do around 5 transactions at any given time without running out of cash.

The backtest performs very well on out-of-sample data, generating returns of50%in our time frame. By comparison, the overall market return was just12% . Our strategy also produced consistent returns with few drawdowns.

Return percentage for out-of-sample data

I also calculated some common statistics to evaluate the performance of our strategy over the testing time frame using a theoretical starting balance of $10,000.

Total Return: 50.97%

Total Transactions: 284

Total Net Profit: $5107.70

Profit coefficient: 2.67

Percentage of profitable trades: 46.47%

Average net trading profit: $17.98

Maximum drawdown: $-554.87

Surprisingly, our strategy was only profitable on46.47% of trades. However, since the model was trying to predict a big move up in the stock, the winning trades more than made up for our losing trades. Even with a large number of losing trades, the average net profit per trade was$17.98.

Infrastructure

To put my policy into use, I used the AWS Cloud Development Kit (CDK) to create the infrastructure to host my policy. The strategy was then set up to interact with Alpaca ( https://alpaca.markets/ ), an API for stock and cryptocurrency trading.

First, I stored the model in AWS S3, a simple cloud storage solution. I then created 2 separate Lambda functions: a Buy Function and a Sell Function. The buyfunction will run at the end of each trading day, generate model predictions of whether to place a trade, and set up the trade using the Alpaca API. The sellfunction then continuously scans open trades to see if they have reached the threshold for closing, either because they have hit the stop loss, profit threshold, or have been held longer than 10 sky. Finally, I created two DynamoDB tables, aTransactions tablethat tracks current open trades, and aHistorical Transactions table that tracks historical trades.

Historical transaction DynamoDB table

Bringing it all together

Fast forward to today: I have been running a paper trading strategy at Alpaca Brokerage for about 6 months. It performed very similarly to the backtest results, producing consistent profits with very few drawdowns. Up6% so far since inception. It will be interesting to see how it continues to perform over time.

Live trading results

The knowledge points of the article match the official knowledge files, and you can further learn related knowledge. Algorithm skill tree Home page Overview 56886 people are learning the system

syntaxbug.com © 2021 All Rights Reserved.