Martingale Day-Trading with the Alpaca Trading API
Testing out an old betting system Martingale with algorithmic trading in Python. This article talks about applying a theoretical betting strategy to a day-trading algorithm’s position sizing.
Testing out an old betting system with algorithmic trading in Python.This article talks about applying a theoretical betting strategy to a day-trading algorithm’s position sizing. A sample Python implementation is provided. You can read about the logic below and let me walk you through the code, or if you’re already familiar with using the Alpaca trading API, you can jump right in to the code on GitHub here!
Algorithmic Martingale Betting in the Stock Market
What’s a Martingale System?
People have been trying to come up with “winning strategies” in games of chance for a long, long time. So-called “Martingale” strategies, originating from France in the 18th century, aim to create a scenario where a gambler is highly likely to walk away from a game of even odds with a profit.
For example, say the game is simple — we each bet a dollar on the outcome of a coin flip. If it lands on heads, I take your dollar, but if it lands on tails, you take mine. If you follow a Martingale strategy, your game plan is that, if you lose, you will double the bet size and play again. So if the coin landed on heads in the first game, meaning that I had won a dollar and you had lost one, in the second game, you would bet two dollars. If it landed on heads again, you would be down three dollars total, and so in the third game you would bet four dollars to recoup your loss.
In theory, assuming you and I both have infinite money to play with and that I will continue to match your bet indefinitely, you should eventually get a chance to walk away with a profit. Of course, there are some obvious problems applying this sort of strategy to betting in a real scenario, where those assumptions don’t apply. If you don’t have infinite money, you could run out of money very quickly. Because the outcome of each coin flip is independent of the last, the gambler’s perception of a long losing streak being unlikely meaning that they will make money following such a strategy is just the gambler’s fallacy in action.
What’s this have to do with the stock market?
If the coin flips were not entirely independent of each other, such a strategy might prove to be at least somewhat effective. A similar environment is modeled in the market. Look at a candlestick chart for some security’s price — if you consider the green candles heads and the red candles tails, you can see some parallels. Since each candle is probably not completely independent of the previous one, there’s a possibility that “streaks” of a certain length are more or less likely than random chance.
Here, we’ll try something a little different, in terms of technical analysis. As an example, we’ll put together a simple algorithm that day-trades the S&P 500. Rather than using traditional indicators to guess whether the price will go up or down, we will try to apply such a Martingale strategy as was discussed earlier — if we bet that it will move in one direction (by buying or shorting shares,) and if it does not, we’ll double our bet until it does. We’ll put it to the test using Alpaca’s paper trading API, and I’ll talk about some adjustments you might make to improve its performance.
While I’ll be using Alpaca’s paper trading API, which is available to anyone who signs up with an email on their website, I’ll also be using Polygon as the data source for the Python implementation, which is only freely provided to those who have a funded brokerage account with Alpaca. So, if you’d like to run this code, you’ll either need to have a brokerage account or pay for Polygon data directly.
Basics of Working with the Trading API
Even if you’re new to algorithmic trading, it’s easy to get started with Alpaca’s API. Let’s break down what we need to be able to do. We need to be able to programmatically place buy and sell orders, obviously. We also need to be able to know what happens to the orders we place — we want to know when they’ve been filled, particularly. We also need to be able to get price updates for the S&P 500 — since we’re day trading, we’ll want them pretty frequently, and we want them as up-to-date as we can get them.
Buying and selling is pretty easy with their REST API, so let’s start there. We’ll extend on the example I just linked and write a function that will place an order that will attempt to take us from our current position — which can be positive or negative — towards a target number of shares. One thing to note about the Alpaca API is that you cannot go directly from a long position to a short position — you must close your open position first. I.e., you need to stop at zero, or else you’ll get an API error. I’ve put together some sample code here that takes care of all that for you. (You can build your own file as you go, or you can just copy the full source from GitHub.)
import alpaca_trade_api as tradeapi
class MartingaleTrader(object):
def __init__(self):
# API authentication keys can be taken from the Alpaca dashboard.
# https://app.alpaca.markets/paper/dashboard/overview
self.key_id = "REPLACE_ME"
self.secret_key = "REPLACE_ME"
self.base_url = 'https://paper-api.alpaca.markets'
# The symbol we will be trading
self.symbol = 'SPY'
# When this variable is not None, we have an order open
self.current_order = None
# The closing price of the last aggregate we saw
self.last_price = 0
# The connection to the Alpaca API
self.api = tradeapi.REST(
self.key_id,
self.secret_key,
self.base_url
)
# Get our starting position, in case we already have one open
try:
self.position = int(self.api.get_position(self.symbol).qty)
except:
# No position exists
self.position = 0
def send_order(self, target_qty):
# We don't want to have two orders open at once
if self.current_order is not None:
self.api.cancel_order(self.current_order.id)
delta = target_qty - self.position
if delta == 0:
return
print(f'Ordering towards {target_qty}...')
try:
if delta > 0:
buy_qty = delta
if self.position < 0:
buy_qty = min(abs(self.position), buy_qty)
print(f'Buying {buy_qty} shares.')
self.current_order = self.api.submit_order(
self.symbol, buy_qty, 'buy',
'limit', 'day', self.last_price
)
elif delta < 0:
sell_qty = abs(delta)
if self.position > 0:
sell_qty = min(abs(self.position), sell_qty)
print(f'Selling {sell_qty} shares.')
self.current_order = self.api.submit_order(
self.symbol, sell_qty, 'sell',
'limit', 'day', self.last_price
)
except Exception as e:
print(e)
if __name__ == '__main__':
trader = MartingaleTrader()
# Order five shares
trader.send_order(5)
You might be wondering where the self.position variable gets updated — we’ll handle that with our next bit of code. In order to keep tabs on the orders that we’ve placed, we’ll open a streaming connection with the Alpaca API and watch the “trade_updates” channel for updates to the trades we’ve placed. With each order fill, there’s a “position_qty” variable that informs us of the total number of shares we have for the security, and we’ll use that to keep track of how many shares we’ve got.
conn = tradeapi.StreamConn(
self.key_id,
self.secret_key,
self.base_url
)
# Listen for updates to our orders
@conn.on(r'trade_updates')
async def handle_trade(conn, channel, data):
symbol = data.order['symbol']
if symbol != self.symbol:
# The order was for a position unrelated to this script
return
event_type = data.event
qty = int(data.order['filled_qty'])
side = data.order['side']
oid = data.order['id']
if event_type == 'fill' or event_type == 'partial_fill':
# Our position size has changed
self.position = int(data.position_qty)
print(f'New position size due to order fill: {self.position}')
if (event_type == 'fill' and self.current_order
and self.current_order.id == oid):
self.current_order = None
elif event_type == 'rejected' or event_type == 'canceled':
if self.current_order and self.current_order.id == oid:
# Our last order should be removed
self.current_order = None
elif event_type != 'new':
print(f'Unexpected order event type {event_type} received')
conn.run(['trade_updates'])
The last thing I mentioned was getting data updates. Below, I’ll discuss what we’re actually going to do with the data, but here’s the handler method for just retrieving it.
# Listen for second aggregates and perform trading logic
@conn.on(r'A$', [self.symbol])
async def handle_agg(conn, channel, data):
# Update price info
tick_open = self.last_price
tick_close = data.close
self.last_price = tick_close
conn.run([f'A.{self.symbol}'])
Implementing our Martingale Trading Strategy
Let’s go over the behavior of our initial attempt at implementing this strategy. Remember how I talked about a candlestick chart earlier? Let’s go ahead and pick a length of time for the “candles” — we’ll go with 5 seconds to start with. So every 5 seconds, we’ll check the price and run through a routine. There are three cases to consider.
- If there’s no ongoing streak yet, either because the program just started or because a streak just ended. Check for a move in the price. If it’s not moved since the last tick, don’t do anything. Otherwise, a new streak is started. If it’s moved up, enter a short position. If it’s moved down, enter a long position.
- If there’s an ongoing streak and it continues, i.e., the price has moved in the same direction as it moved the last time we checked, we’ll double our position.
- If there’s an ongoing streak and it breaks, i.e., the price has moved in the opposite direction as it moved the last time we checked, we’ll exit our position.
There’s obvious flaws with this trading strategy, even from the perspective of the gambling strategy that inspired it. The price change on each tick could be different, meaning that we aren’t necessarily guaranteed to make our loss back even when we do wind up on the winning side. However, let’s give it a try as it is, and I’ll talk about some potential improvements afterwards.
Here, on GitHub, is the full code for this implementation. Be sure to read through it and try to understand what’s going on. We initialize all our tracking variables with the MartingaleTrader object, and once it’s ready, the start_trading() method will open listeners to the data feeds we’ll be watching. handle_agg() is going to run every second when the data comes from Polygon and decide what to do about it, and handle_trade() is going to run any time Alpaca sees an update on one of our orders. To run the script yourself, you’ll need to supply the script with your own API keys, which can be generated on the Alpaca dashboard, by setting them in the MartingaleTrader class’ __init__() method. If you want to eventually take the algorithm live, you’ll only need to provide it with the info from the live dashboard instead.
Potential Improvements
This sample code provides a basic implementation for a trading strategy based on the Martingale betting system, but there’s a lot of room to grow and potentially even improve the profitability of the algorithm. I’ll list some potential improvements here, and if you want to give them a try, you can take a shot at editing the code in your own copy of the script.
- This algorithm effectively bets on “mean reversion,” as it bets that “streaks” in the stock’s price are likely to break sooner rather than later so that the price might return to its average value. What happens if we change it to bet on “momentum” instead, by having it guess that hot streaks are actually more likely to continue than to break? If you want to test it for yourself, it’s a simple code change to make: in handle_agg(), just before it sends an order, it multiplies the number of shares it wants to buy or sell by -1 if the streak is increasing. Instead, have it multiply “target_qty” by -1 if the streak is not increasing.
- 5 seconds as a tick interval was chosen arbitrarily. Setting “self.tick_size” in __init__() to a higher or lower value might yield better results. (A longer tick size means bigger price changes between each update, on average. Does that make any difference to our algorithm?)
- 10% of our total buying power being the “base bet” was also chosen arbitrarily. A lower value will likely increase the stability of the algorithm, while a higher value is likely to increase volatility. Do either of those help or harm it?
- The algorithm trades the S&P 500. Can you think of any other high-volume securities worth daytrading that might also work? (Cent levels will be crossed less frequently with less expensive stocks, but they will represent a bigger percentage difference in the price. Since we ignore any price change that doesn’t cross a cent level, a cheaper stock will have “larger” changes in the ticks that are part of a streak.)
- This implementation does not care about the magnitude of the changes in the price, only the direction. It thinks that a two cent increase is the same as a ten cent increase. Is there a way for us to use the size of the price changes to weight each of our “bets?” (It’s impossible to truly recreate the coin flipping game I discussed above in the market, as we cannot know what the size of the next candle will be. However, we might be able to get some benefits in our algorithm’s performance by tracking a moving average, or even an exponential moving average, of price changes, and assuming that the size of the next change will be roughly equivalent to that value.)
Technology and services are offered by AlpacaDB, Inc. Brokerage services are provided by Alpaca Securities LLC (alpaca.markets), member FINRA/SIPC. Alpaca Securities LLC is a wholly-owned subsidiary of AlpacaDB, Inc.