Bitcoin (BTC) Breakout Strategy – Free Python Code

(To download an already completed copy of the Python strategy developed in this guide, visit our GitHub.)

In this article, we will code a custom breakout strategy geared specifically towards trading BTC/USD using Python and FXCM’s Rest API / WebSocket. The algorithm looks to take advantage of Bitcoin’s volatility by getting into a breakout trade as soon as a breakout occurs, using real time streaming tick data via WebSocket.

This strategy buys the moment Bitcoin’s price breaks above the 24-hour high and sells the moment Bitcoin’s price breaks below the 24-hour low. Profit targets (limit orders) are set at 1.5x the distance between the 24-hour high and low. While no actual stop loss orders are set for each trade, the strategy automatically closes out a trade when an opposing signal occurs (i.e. closes buy trades when a sell breakout occurs, closes sell trades when a buy breakout occurs.) This effectively gives the strategy a built in trailing stop.

Step 1. Download our “Python Strategy Template.py” file.

If you already have a copy of the “Python Strategy Template.py” you can go to Step 2. In a previous article, we developed a strategy template that makes future strategy development much easier to accomplish. We recommend readers go through this article at least once to understand how it works.

You may also download our template from our GitHub.

Step 2. Add User Parameters

We need to add 3 additional parameters to give our breakout strategy all the inputs it needs. We first want to add a parameter where we can select how many periods we want to look back to determine our channel’s highs/lows. We will call this channel_periods with a default value to 24. We also want to add parameters for trade size and limitmultiplier.

Setting the amount parameter equal to 1 means each trade will be 1/100th (0.01) BTC. That means that each dollar Bitcoin moves, would be a 1 cent gain/loss on our account.

The limitmultiplier parameter determines how far our profit target will be set in relation to the size of the price channel when the trade is opened. If the distance between channel high and channel low is $100 at the time a trade is opened, a limitmultiplier set to 1.5 would set the limit at $150.

###### USER PARAMETERS ######
token = 'INSERT-TOKEN-HERE'
symbol = 'BTC/USD'
timeframe = "H1"                     # (m1,m5,m15,m30,H1,H2,H3,H4,H6,H8,D1,W1,M1)
channel_periods = 24
amount = 1
limitmultiplier = 1.5
#############################

 

Step 3. Add enter(), exit(), crossedOver(), crossesUnder() and countOpenTrades() functions.

Before we write our trading logic inside the Update() function, there are a few ‘utility’ functions we need to add to our code that will make writing our strategy’s logic much simple.

 

The Custom enter() Function

First, let’s add a custom enter() function. When called, the enter() function places a market order. Normally, to place a Buy market order, we would use enter(“B”) and for a Sell market order, use enter(“S”). But this enter() function needs to be customized to accept a limit price as an argument from the Update() function. The function is already coded to accept the symbol and the amount parameters we created in our User Parameters section.

# This custom function places a market order in the direction BuySell, "B" = Buy, "S" = Sell, with a take profit set to limitprice, uses symbol, amount
def enter(BuySell,limitprice):
	direction = True;
	if BuySell == "S":
		direction = False;
	try:
		opentrade = con.open_trade(symbol=symbol, is_buy=direction,amount=amount, time_in_force='GTC',order_type='AtMarket',is_in_pips=False,limit=limitprice)
	except:
		print("	  Error Opening Trade.")
	else:
		print("	  Trade Opened Successfully.")

 

The exit() Function

Next, let’s add the exit() function. It works very similarly to the enter function. To close out all buy trades for our traded symbol, call exit(“B”). To close out all sell trades for our traded symbol, call exit(“S”). If we want to close out both buy and sell trades, we just call exit() with no arguments.

# This function closes all positions that are in the direction BuySell, "B" = Close All Buy Positions, "S" = Close All Sell Positions, uses symbol
def exit(BuySell=None):
	openpositions = con.get_open_positions(kind='list')
	isbuy = True
	if BuySell == "S":
		isbuy = False
	for position in openpositions:
		if position['currency'] == symbol:
			if BuySell is None or position['isBuy'] == isbuy:
				print("	  Closing tradeID: " + position['tradeId'])
				try:
					closetrade = con.close_trade(trade_id=position['tradeId'], amount=position['amountK'])
				except:
					print("	  Error Closing Trade.")
				else:
					print("	  Trade Closed Successfully.")

 

The crossesOver() Function

The next function is a workhorse for many different strategies. crossesOver() checks to see if one data stream’s value crossed over another data stream’s value in the previous candle/bar. It also can check to see if a data stream’s value crossed over a static value in the previous candle/bar. Our strategy’s logic has to do with current price crossing our price channel’s high or low. This will be one of the functions we use to do that. We will put our tick stream as crossesOver()’s first argument, and then our price channel’s high as our second argument. It will return a True value if the price crossed over the high; returns false if it did not crossover.

# Returns true if stream1 crossed over stream2 in most recent candle, stream2 can be integer/float or data array
def crossesOver(stream1, stream2):
	# If stream2 is an int or float, check if stream1 has crossed over that fixed number
	if isinstance(stream2, int) or isinstance(stream2, float):
		if stream1[len(stream1)-1] <= stream2: return False else: if stream1[len(stream1)-2] > stream2:
				return False
			elif stream1[len(stream1)-2] < stream2:
				return True
			else:
				x = 2
				while stream1[len(stream1)-x] == stream2:
					x = x + 1
				if stream1[len(stream1)-x] < stream2:
					return True
				else:
					return False
	# Check if stream1 has crossed over stream2
	else:
		if stream1[len(stream1)-1] <= stream2[len(stream2)-1]: return False else: if stream1[len(stream1)-2] > stream2[len(stream2)-2]:
				return False
			elif stream1[len(stream1)-2] < stream2[len(stream2)-2]:
				return True
			else:
				x = 2
				while stream1[len(stream1)-x] == stream2[len(stream2)-x]:
					x = x + 1
				if stream1[len(stream1)-x] < stream2[len(stream2)-x]:
					return True
				else:
					return False

 

The crossesUnder() Function

This function is the mirror opposite of crossesOver(). It returns a True value if the first data stream crossed under the second; returns false if it did not cross under. Just like crossesOver(), the first argument must be a data array, the second can be either a data array or a int/float.

# Returns true if stream1 crossed under stream2 in most recent candle, stream2 can be integer/float or data array
def crossesUnder(stream1, stream2):
	# If stream2 is an int or float, check if stream1 has crossed under that fixed number
	if isinstance(stream2, int) or isinstance(stream2, float):
		if stream1[len(stream1)-1] >= stream2:
			return False
		else:
			if stream1[len(stream1)-2] < stream2: return False elif stream1[len(stream1)-2] > stream2:
				return True
			else:
				x = 2
				while stream1[len(stream1)-x] == stream2:
					x = x + 1
				if stream1[len(stream1)-x] > stream2:
					return True
				else:
					return False
	# Check if stream1 has crossed under stream2
	else:
		if stream1[len(stream1)-1] >= stream2[len(stream2)-1]:
			return False
		else:
			if stream1[len(stream1)-2] < stream2[len(stream2)-2]: return False elif stream1[len(stream1)-2] > stream2[len(stream2)-2]:
				return True
			else:
				x = 2
				while stream1[len(stream1)-x] == stream2[len(stream2)-x]:
					x = x + 1
				if stream1[len(stream1)-x] > stream2[len(stream2)-x]:
					return True
				else:
					return False

 

The countOpenTrades() Function

The final utility function we will add is the countOpenTrades() function. As the name suggests, this function counts how many open positions we have in our account for the strategy’s symbol. Calling countOpenTrades(“B”) returns the number of open buy trades/tickets, countOpenTrades(“S”) returns the number of open sell trades/tickets, countOpenTrades() returns the number of all open trades/tickets.

# Returns number of Open Positions for symbol in the direction BuySell, returns total number of both Buy and Sell positions if no direction is specified
def countOpenTrades(BuySell=None):		
	openpositions = con.get_open_positions(kind='list')
	isbuy = True
	counter = 0
	if BuySell == "S":
		isbuy = False
	for position in openpositions:
		if position['currency'] == symbol:
			if BuySell is None or position['isBuy'] == isbuy:
				counter+=1
	return counter

 

Step 4. Calculate Initial Channel High and Low Values Inside the Prepare() Function

The Prepare() function is where we pull our historical price data before the strategy begins to run our strategy’s trading logic. But because our strategy requires knowing the high and low price values for the last ‘channel_periods’ number of candles/bars, we need to calculate the intial high/low values inside Prepare() as well.

# This function runs once at the beginning of the strategy to run initial one-time processes/computations
def Prepare():
	global pricedata
	global channel_high
	global channel_low
	
	print("Requesting Initial Price Data...")
	pricedata = con.get_candles(symbol, period=timeframe, number=numberofcandles)
	print(pricedata)	
	print("Initial Price Data Received...")
	
	# Calculate Initial Channel High/Low
	channel_high = max(pricedata['bidhigh'][-channel_periods:])
	channel_low = min(pricedata['bidlow'][-channel_periods:])
	print("	  Calculating Channel High/Low Values.")
	print("	  Channel High: " + str(channel_high))
	print("	  Channel Low: " + str(channel_low))

 

Step 5. Writing our Trading Logic Inside the Update() Function

The Update() function is processed every time a candle/bar closes as well as each time we receive a real-time price update. It will determine when trades should be opened/closed. The first thing we must do is insure we are referencing our global variables for channel_high and channel_low.

# This function is run every time a candle closes
def Update(tickdata=None, tickdataframe=None):
	global channel_high
	global channel_low

Next, we need to create our block of code that will run each time a candle/bar closes. To determine whether Update() is being called by a real-time price update or by a candle closing (via our StrategyHeartBeat() function), we note the value of the argument ‘tickdata’. ‘tickdata’ will only have a value when the Update() function is called by a real-time price update. If Update() is called by a close of bar, we can assume the ‘tickdata’ variable will be equal to its default value of None. So when ‘tickdata’ equals None, we want to update our channel_high and channel_low variables by retrieving the highest high and lowest low for the past ‘channel_periods’ amount of candles/bars.

	# Close of Bar Actions
	if tickdata == None:
		print(str(dt.datetime.now()) + "	 " + timeframe + " Bar Closed - Running Update Function...")
		
		# Update Channel High/Low
		channel_high = max(pricedata['bidhigh'][-channel_periods:])
		channel_low = min(pricedata['bidlow'][-channel_periods:])
		print("	  Calculating Channel High/Low Values.")
		print("	  Channel High: " + str(channel_high))
		print("	  Channel Low: " + str(channel_low))

		print(str(dt.datetime.now()) + "	 " + timeframe + " Update Function Completed.\n")

Now that we know we will have the most updated values for channel_high and channel_low, we now can check to see if we should place buy/sell trade each time we receive a price update.

We use our crossesOver() and crossesUnder() functions with the most recent bid price, channel_high, and channel_low. If the recent bid price crossed above the channel_high, we Buy and close out any existing Sell positions. If the recent bid price crossed below the channel_low, we Sell and close out any existing Buy positions. Also note that we calculate our limit price using the channel_high, channel_low and limitmultiplier.

	# Tick Actions
	else:
	
		# Buy Logic - If price crosses over channel_high and we have no Buy trades open, Buy
		if crossesOver(tickdataframe['Bid'], channel_high) and countOpenTrades("B") == 0:
			print(str(dt.datetime.now()) + "	 Price Broke Above Channel. BUY SIGNAL!")
			print(str(dt.datetime.now()) + "	 Opening Buy Trade...")
			# Limit is distance between Channel High & Low mulitpled by limitmultiplier
			limit = channel_high + (channel_high - channel_low) * limitmultiplier
			enter("B",limit)
			if countOpenTrades("S") > 0:
				exit("S")
			
		# Sell Logic - If price crosses under channel_low and we have no Sell trades open, Sell
		if crossesUnder(tickdataframe['Bid'], channel_low) and countOpenTrades("S") == 0:
			print(str(dt.datetime.now()) + "	 Price Broke Below Channel. SELL SIGNAL!")
			print(str(dt.datetime.now()) + "	 Opening Sell Trade...")
			# Limit is distance between Channel High & Low mulitpled by limitmultiplier
			limit = channel_low - (channel_high - channel_low) * limitmultiplier
			enter("S",limit)
			if countOpenTrades("B") > 0:
				exit("B")

 

Step 6. Subscribe to Tick Data

Typically, we initialize our Python trading strategy by running our Prepare() and StrategyHeartBeat() functions. But since this strategy also requires real-time tick data, we must subscribe to that as well. We will use fxcmpy wrapper’s subscribe_market_data and set_max_prices functions to subscribe and store the most recent 100 ticks. This will call the Update() function each time there is a price update.

Prepare() # Initialize strategy
con.subscribe_market_data(symbol, (Update,)) # Subscribe to Tick Data
con.set_max_prices(100) # Set maximum number of most recent Ticks to store inside of dataframe
StrategyHeartBeat() # Run strategy

 

Step 7. Run our strategy inside our command console.

The last step is to run our strategy inside our command console. The python file we created I saved on to my desktop, so I execute the strategy by calling it like this:

The strategy is now up and running and will open and close trades per our rules!

 

What Next?

This breakout strategy was tailored specifically for a high volatility instrument like BTC/USD, but there are definitely ways it can be expanded upon and improved. Please edit and make this strategy your own! Also, make sure to check out our other Python strategies we have available at https://github.com/fxcm/RestAPI


Risk Warning: The FXCM Group does not guarantee accuracy and will not accept liability for any loss or damage which arise directly or indirectly from use of or reliance on information contained within the webinars. The FXCM Group may provide general commentary which is not intended as investment advice and must not be construed as such. FX/CFD trading carries a risk of losses in excess of your deposited funds and may not be suitable for all investors. Please ensure that you fully understand the risks involved.