avatarVictor Sim

Summary

This context discusses the use of genetic algorithms to build trading strategies, highlighting their ability to find creative solutions and their differences from traditional gradient-based optimization methods.

Abstract

The text begins by acknowledging the often overlooked potential of genetic algorithms in finding creative solutions, despite their reputation for not converging effectively. It explains that genetic algorithms do not use partial derivatives, making their training algorithm less direct, but they can formulate solutions that would be impossible with traditional gradient-based optimization. The article then delves into how genetic algorithms work, comparing them to a brute-force type attack with the concept of evolution to speed up the process. It also mentions when genetic algorithms should not be used, such as for elementary tasks like regression and classification. The context then explores the application of genetic algorithms to stock trading, discussing the complexities and challenges involved. The author provides code snippets to illustrate the process, from generating agents to mutation, and concludes by suggesting improvements to the presented framework.

Opinions

  • Genetic algorithms are often ignored due to their reputation for not converging effectively.
  • Genetic algorithms can find creative solutions that would be impossible with traditional gradient-based optimization.
  • Genetic algorithms are not suitable for elementary tasks like regression and classification.
  • Genetic algorithms are not good for finding patterns but are useful for having a wide scope of view to find solutions that have not been considered.
  • Applying genetic algorithms to stock trading can be complex due to noise from different sources diluting the true pattern behind the data.
  • Genetic algorithms can be a shortcut through complex analysis in stock trading, simply testing different strategies to find the best one.
  • The author suggests improvements to the presented framework, such as recording the generation of past agents, linking the program to a paper trading account, and creating more trade types.

Using Genetic Algorithms to Build Trading Strategies

to show the ability of genetic algorithms to find creative solutions

Photo by Maarten Deckers on Unsplash

Note from Towards Data Science’s editors: While we allow independent authors to publish articles in accordance with our rules and guidelines, we do not endorse each author’s contribution. You should not rely on an author’s works without seeking professional advice. See our Reader Terms for details.

Genetic algorithms are often ignored, as another unsupervised learning algorithm that fails to converge effectively. This is partly true, as genetic algorithms do not use partial derivatives and therefore the training algorithm is less direct. However, genetic algorithms allow for the formulation of solutions that would have been impossible, using traditional gradient-based optimization.

Why is this so? If the gradient descent based model started at local minimum, it would be permanently stuck, as the gradient of the points on each side would be higher. However, due to the more stochastic nature for genetic algorithms it would eventually find the solution.

Additionally, neural networks require labelled data with well-defined loss functions. Genetic algorithms only require data, and a custom loss function (often called a fitness function for genetic algorithms) can be cleverly crafted to optimize certain features. This flexibility makes genetic algorithm stand out from the rest of the unsupervised algorithms.

How do genetic algorithms work?

Genetic algorithms are a form of a brute-force type attack to the problem. The only differences being using the concept of evolution to speed up the process. Here is how the genetic algorithm works:

  1. An agent is generated. It contains a set of weights that are compatible with the neural network.
  2. The agent’s fitness value is calculated
  3. Repeat this many times until you have a “population” of agents
  4. Sort the population by their fitness, with the agents with the best fitness at the top of the list.
  5. “Breed” the top agents together.
  6. Repeat until a satisfactory loss value has been reached.

By changing the fitness function and the input data, the genetic algorithm can be applied to literally every problem!

When a genetic algorithm should NOT be used:

For elementary tasks such as regression and classification, a deep neural network, trained using a gradient-based optimizer will always converge faster than a genetic algorithm.

Genetic algorithm are not good for finding patterns, but rather having a wide scope of view, to find solutions that have not been considered.

Applying genetic algorithms to stock trading:

Stock trading is complex, as the noise from different sources dilute the true pattern behind the data. Using a genetic algorithm is like taking a shortcut through all of this; disregarding the pattern recognition and the complex analysis. It simply tests different strategies, and finds the best strategy to trade a security. If a genetic algorithm fails to find a good solution this can only be one (or both!) of these two reasons:

  1. The genetic algorithm has not be trained for a long enough period of time

Genetic algorithm, being a brute-force algorithm, requires a long period of time to narrow down the results. This is a large hurdle to overcome, as the computing power must be very high to overcome this problem

2. The loss function is faulty

This is much more common. The loss function that evaluates the network simply does not encapsulate the goal of the loss function.

The Code:

Disclaimer: This code has been adapted from the code here to make it compatible to optimize neural networks that create trading strategies.

Step 1| Prerequisites:

import random
import numpy as npdef sigmoid(x):
    return 1/(1+np.exp(-x))

These are the most basic dependencies for the project. To make the learning process better, you can add more activation functions for your suitable datasets.

Step 2| The Agent:

class Agent:
            def __init__(self,network):
                class neural_network:
                    def __init__(self,network):
                        self.weights = []
                        self.activations = []
                        for layer in network:
                            if layer[0] != None:
                                input_size = layer[0]
                            else:
                                input_size = network[network.index(layer)-1][1]
                            output_size = layer[1]
                            activation = layer[2]
                            self.weights.append(np.random.randn(input_size,output_size))
                            self.activations.append(activation)
                    def propagate(self,data):
                        input_data = data
                        for i in range(len(self.weights)):
                            z = np.dot(input_data,self.weights[i])
                            a = self.activations[i](z)
                            input_data = a
                        yhat = a
                        return yhat
                self.neural_network = neural_network(network)
                self.fitness = 0
            def __str__(self):
                    return 'Loss: ' + str(self.fitness[0])

Each agent has its own set of weights, generated randomly with the architecture of the neural network setup. The argument network is a list that looks like so:

network = [[5,10,sigmoid],[None,1,sigmoid]]

Each term in the list is describing a layer in the Neural Network. The first term in each nested list is the number of input neurons for the layer, the second term is the output and the third the activation function to be applied for the layer.

Step 3| Generating Agents:

def generate_agents(population, network):
            return [Agent(network) for _ in range(population)]

This is used to generate the agents, as detailed in step 1 of the methodology to run a genetic algorithm.

Step 4| Calculate Fitness:

def fitness(agents,X):
            neutral_range = 0
            for agent in agents:
                profit = 0
                qty = 10
                yhat = agent.neural_network.propagate(X[1:])
                for i in range(len(yhat)):
                    if 0.5 + neutral_range > yhat[i] and yhat[i] > 0.5 -neutral_range:
                        yhat[i] = None
                    elif 1-yhat[i] > yhat[i]-0:
                        yhat[i] = 0
                    elif 1-yhat[i] < yhat[i]-0:
                        yhat[i] = 1
                        
                correct_trades = []
                for i in range(1,len(X)):
                    if X[i][3] > X[i-1][3]:
                        correct_trades.append(1)
                    elif X[i][3] < X[i-1][3]:
                        correct_trades.append(0)
                for i in range(len(yhat)):
                    if yhat[i]:
                        if correct_trades[i] == yhat[i]:
                            profit += abs(X[i+1][3] - X[i][3])*qty
                        elif correct_trades[i] != yhat[i]:
                            profit -= abs(X[i+1][3] - X[i][3])*qty
                agent.fitness = profit
            return agents

This fitness function is a trading simulation, that uses past data to calculate the profits of the model. Each agent generates a list of trades, by outputting a sigmoid value. These values are then rounded to the nearest value. If this value is 0, the simulation will sell the stock. If this value is 1, the simulation will buy the stock.

It then calculates the profits, by comparing the best possible trade to the actual trade.

Step 5| Agent Selection:

def selection(agents):
            agents = sorted(agents, key=lambda agent: agent.fitness, reverse=False)
            print('\n'.join(map(str, agents)))
            agents = agents[:int(0.2 * len(agents))]
            return agents

This code essentially sorts the agents by their fitness value, from the lowest value to the highest. Keep in mind that the lower the fitness value, the better. It then shortens the list of agents to only the top 20% of the agents.

Step 6| Agent Crossover:

def unflatten(flattened,shapes):
            newarray = []
            index = 0
            for shape in shapes:
                size = np.product(shape)
                newarray.append(flattened[index : index + size].reshape(shape))
                index += size
            return newarray
        
        def crossover(agents,network,pop_size):
            offspring = []
            for _ in range((pop_size - len(agents)) // 2):
                parent1 = random.choice(agents)
                parent2 = random.choice(agents)
                child1 = Agent(network)
                child2 = Agent(network)
                
                shapes = [a.shape for a in parent1.neural_network.weights]
                
                genes1 = np.concatenate([a.flatten() for a in parent1.neural_network.weights])
                genes2 = np.concatenate([a.flatten() for a in parent2.neural_network.weights])
                
                split = random.randint(0,len(genes1)-1)child1_genes = np.array(genes1[0:split].tolist() + genes2[split:].tolist())
                child2_genes = np.array(genes1[0:split].tolist() + genes2[split:].tolist())
                
                child1.neural_network.weights = unflatten(child1_genes,shapes)
                child2.neural_network.weights = unflatten(child2_genes,shapes)
                
                offspring.append(child1)
                offspring.append(child2)
            agents.extend(offspring)
            return agents

This is the most complex part of the program. In the crossover process two random parents are chosen from within the selected agents. The nested list of weights are reduced to one long list. A splitting point is randomly picked. This determines how much of each parent’s weights the child will be composed of. This child’s weights is then reformatted and the child is added to the list of agents.

Step 7| Mutation:

def mutation(agents):
            for agent in agents:
                if random.uniform(0.0, 1.0) <= 0.1:
                    weights = agent.neural_network.weights
                    shapes = [a.shape for a in weights]flattened = np.concatenate([a.flatten() for a in weights])
                    randint = random.randint(0,len(flattened)-1)
                    flattened[randint] = np.random.randn()newarray = []
                    indeweights = 0
                    for shape in shapes:
                        size = np.product(shape)
                        newarray.append(flattened[indeweights : indeweights + size].reshape(shape))
                        indeweights += size
                    agent.neural_network.weights = newarray
            return agents

Genetic Algorithms are technically “blind” as they have no insight to how the surface plotted by the loss function looks like. This means it is very easy for the genetic algorithm to get stuck at a certain point. Mutation allows for slight, random, infrequent changes to stir the pot and get the system out of local minimas.

Step 8| Executing the program:

def split_sequences(sequences, n_steps):
    X, y = list(), list()
    for i in range(len(sequences)):
        end_ix = i + n_steps
        if end_ix > len(sequences)-1:
            break
        seq_x, seq_y = sequences[i:end_ix, :], sequences[end_ix, :]
        X.append(seq_x)
        y.append(seq_y)
    return np.array(X), np.array(y)
np.random.seed(0)            
X,y = split_sequences(np.random.randn(1000,1),5)
X = np.reshape(X,(X.shape[0],X.shape[1]))
network = [[5,10,sigmoid],[None,1,sigmoid]]
ga = genetic_algorithm
agent = ga.execute(100,100,10000,X,network)

To execute the program we need data. For some reason, the SSL certificate would not let me get financial data from yfinance or alphavantage. I have not been able to run this program, so I just used random numpy arrays as data.

If you want to use your own financial data, the concept would be to have an array of closing price data split it into chunks of size-n.

For reproducibility, this program will set its own seed, making the randomness fixed. This is to decrease the randomness, to allow for the tweaking of parameters like population size, epochs and the neural network architecture. The more interesting parameter here is the length of each chunk of data to be fed into the network. Tweaking this parameter will find the optimum period of time in which the program will work.

Conclusion:

As I have said in many other articles of my own, this is merely the framework for what can be done with the concept. There are countless things that you can do to improve my program. Here are a few of them:

  1. Record generation of past agents

By recording the generation of past agents, it prevents the program from regenerating agents that have been generated before.

2. Link the program to a paper trading account

Alpaca is the obvious choice to use a paper trading portfolio. It can make trades quickly, allowing for easy analysis of the program’s performance.

3. Create more trade types

This is the hardest of the three. Add different trades such as stop-loss and put and buy trades, allowing for much more nuanced and layered trading strategies, layering different trade types onto each other, to maximize profits.

My links:

If you want to see more of my content, click this link.

Machine Learning
Programming
Data Science
Recommended from ReadMedium