Autonomous stock options trader powered by NEAT (NeuroEvolution of Augmented Topologies)
This repo is no longer actively developed or maintained. It was a way for me to experiment with my favorite machine learning algorithm and get some experience building a larger python project. I still think the underlying hypothesis is plausible in the limit of time and compute, but due to performance reasons I would rather use a modern machine learning framework, multiple neural networks for separate distinct tasks, and better high-level design.
Instead of trying to predict the direction of the market like most traders, we can take a different approach by providing traders with opportunities to place bets. By using machine learning to manage risk, we can essentially become the "casino" and aim to outperform the market through many small, contra-bets. Paradoxically, if markets are efficient enough, we should not expect to do any better so a positive result would be surprising.
Stock options are the perfect instrument for this experiment due to the market being two-sided and zero-sum. There is also a very distinct trade-off between probability of profit and payout, depending on what strikes and expirations are chosen.
Fitness is determined by calculating the total portfolio value vs the value of a buy-and-hold strategy at the end of the simulation period.
By default, networks are not initially connected. Connections and new nodes must arise via mutations over time. The probabilities of forming new connections and nodes are greater than the probability of their removal.
The current inputs and outputs are present: Inputs:
- portfolio cash
- portfolio shares,
- closing (end of day) price of security
- MACD (moving average convergence/divergence)
- MACD signal
- MACD difference
- Bollinger Bands mid
- Bollinger Bands high
- Bollinger Bands low
- RSI (relative strength index)
Outputs:
- Buy (currently means buyback/close short options position)
- Hold (do nothing)
- Sell (currently means open new short options position)
- Delta (only used when opening new option to determine strike)
- Theta (only used when opening new option to determine expiration)
Each agent (128 in each population) is given 100 shares of TSLA to start and ran through a random, 90 day period from the training set. Concurrently, a completely separate dataset is used to run a cross-validation simulation. The cross-validation fitness has no impact on NEAT and is used to gauge overfitting.
An agent can only open and close a single covered-call. A covered-call is when one sells to open an options contract using their owned shares as collateral.
Only the closing prices are considered so agents may only place one trade per day. Low-frequency trading is the goal.
Data was sourced using Thomas Yeng's etrade cache. The neatrader.preprocess
module was created for importing and normalizing these data into csv files, which are then exported to the resources
module.
docker pull chadbowman0/neatrader:latest
First, clone neatrader
and neat-python
:
git clone git@github.com:ChadBowman/neatrader.git ~/neatrader
git clone git@github.com:ChadBowman/neat-python.git ~/neat-python
Create a virtual environment and install neat-python
:
python3 -m venv ~/neatrader/env && source ~/neatrader/env/bin/activate
python3 -m pip install --upgrade pip ~/neat-python
Install neatrader
:
python3 -m pip install ~/neatrader
generations per iteration
(int) is optional. Omit if you just want to see some plots and results quickly. Set to a larger number for better results.
docker run chadbowman0/neatrader:latest <generations per iteration>
python3 -m neatrader <generations per iteration>
To run tests:
python3 -m nose -v --nocapture --logging-level=INFO
Generational performance is continually printed to the output. After every 3 (configurable) generations, plots are created which give visual representation to the winning network, population species, average/best fitness, and the winning agent's simulated trades for a new random, 90 day period. Unfortunately, the species and fitness graphs are not that interesting due to only having 3 generations per iteration. Increase this number if you want to see this improved and leverage other functionalities from NEAT.
Here are some examples: