Automated Crypto Scalping with Alpaca

To execute this strategy successfully, we are building an algorithmic trading bot that will try to capture small profits trading ETH/USD.

Automated Crypto Scalping with Alpaca

What is Scalping?

Scalping is a trading style that specializes in profiting off of small price changes and making a fast profit off reselling. In day trading, scalping is a term for a strategy to prioritize making high volumes of small profits.1

The key idea is that it is easier to capture a small percentage change in the price of an asset than to predict a larger move. To execute this strategy successfully, a trader needs to have a tight control on their entry and exits. This is where having an algorithmic trading strategy can help keep a control on the prices we trade at.

What are we building?

We are building an algorithmic trading bot that will try to capture small profits trading ETH/USD but do it quite often (a few times within an hour hopefully). The bot will use data over the last 10 minutes to decide the buying and selling prices for our limit orders.

We will use the following logic to place our trades:

  1. If the current price of the asset is above the minimum price over the last 10 minutes, place a buy limit order at a price 0.2% above the minimum price. In code, this price is referenced as buying_price.
  2. The above step is immediately followed by placing a limit sell order at a price 0.2% below the maximum price of the asset over the last 10 minutes. In code, this price is referenced as selling_price.
  3. If we do not have any position on the asset we are looking to trade, have a buy order in place for the asset but if the current price of the asset is above 0.5% of our estimated price we would have sold our asset at, then we cancel our existing limit buy order. This indicates that the market is trending upwards and we need to recalibrate our buying and selling prices.
  4. If we do have a position, have a sell order in place for the asset but if the current price of the asset is 0.5% below the buying price of the asset, we cancel our sell limit order and close our position.

Ideally, we would like only checks 1 and 2 to execute alternatively (buy and sell). We might also encounter scenarios where the current price of the asset never reaches the limit price of our order. In such cases, checks 3 and 4 help limit our losses.

Let's Build

Before getting started, you will need to create an Alpaca account to use the paper trading as well as fetch market data for ETH/USD. You can get one by signing up here.

from alpaca.data.historical import CryptoHistoricalDataClient
from alpaca.data.requests import CryptoBarsRequest, CryptoQuotesRequest, CryptoTradesRequest
from alpaca.trading.requests import GetOrdersRequest
from alpaca.data.timeframe import TimeFrame
from alpaca.trading.client import TradingClient
from alpaca.trading.requests import MarketOrderRequest, LimitOrderRequest
from alpaca.trading.enums import OrderSide, TimeInForce, OrderStatus
from datetime import datetime
from dateutil.relativedelta import relativedelta
import json
import logging
import config
import asyncio

# ENABLE LOGGING - options, DEBUG,INFO, WARNING?
logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

We start by importing the necessary libraries including Alpaca-py. This is the latest official python SDK from Alpaca. It provides us with the necessary market data and trading endpoints.

Also, we enable logging to monitor the latest prices and bot status.

# Alpaca Trading Client
trading_client = TradingClient(
    config.APCA_API_KEY_ID, config.APCA_API_SECRET_KEY, paper=True)

# Alpaca Market Data Client
data_client = CryptoHistoricalDataClient()

# Trading variables
trading_pair = 'ETH/USD'
notional_size = 20000
spread = 0.00
total_fees = 0
buying_price, selling_price = 0.00, 0.00
buy_order_price, sell_order_price = 0.00, 0.00
buy_order, sell_order = None, None
current_price = 0.00
client_order_str = 'scalping'

# Wait time between each bar request
waitTime = 60

# Time range for the latest bar data
time_diff = 5

# Current position of the trading pair on Alpaca
current_position = 0

# Threshold percentage to cut losses (0.5%)
cut_loss_threshold = 0.005

# Alpaca trading fee
trading_fee = 0.003


Next, we define our trading client, market data client and trading variables we will use throughout the bot.

Trading client needs Alpaca's secrets as inputs while Market data does not.

async def main():
    '''
    Main function to get latest asset data and check possible trade conditions
    '''

    # closes all position AND also cancels all open orders
    trading_client.close_all_positions(cancel_orders=True)
    logger.info("Closed all positions")

    while True:
        logger.info('----------------------------------------------------')
        l1 = loop.create_task(get_crypto_bar_data(
            trading_pair))
        # Wait for the tasks to finish
        await asyncio.wait([l1])
        # Check if any trading condition is met
        await check_condition()
        # Wait for the a certain amount of time between each bar request
        await asyncio.sleep(waitTime)

In the main function we define our tasks using asyncio event loops. These tasks are defined to do 2 main things:

  1. Get latest bar data for our trading pair and calculate possible buying/selling prices
  2. Check for possible trade scenarios using the prices from the previous step.

In the snippet above, we are repeating the two steps every waitTime amount of seconds. We have defined it as 60 seconds for now but this can be tweaked further to optimize results.

async def get_crypto_bar_data(trading_pair):
   '''
   Get Crypto Bar Data from Alpaca for the last diff minutes
   '''
   time_diff = datetime.now() - relativedelta(minutes=diff)
   logger.info("Getting crypto bar data for {0} from {1}".format(
       trading_pair, time_diff))
   # Defining Bar data request parameters
   request_params = CryptoBarsRequest(
       symbol_or_symbols=[trading_pair],
       timeframe=TimeFrame.Minute,
       start=time_diff
   )
   # Get the bar data from Alpaca
   bars_df = data_client.get_crypto_bars(request_params).df
   # Calculate the order prices
   global buying_price, selling_price, current_position
   buying_price, selling_price = calc_order_prices(bars_df)

   if len(get_positions()) > 0:

       current_position = float(json.loads(get_positions()[0].json())['qty'])

       buy_order = False
   else:
       sell_order = False
   return bars_df

In the code snippet above, we are trying to gather the latest bar data for the asset for the last diff amount of minutes. In the variables defined earlier, we set this value to 5. This can be tweaked based on how frequent you would like your bot to check for bar data.

This is followed by using Alpaca-py's CryptoBarsRequest method that helps us create a custom request for the asset. The request object created can then be passed to our data client's get_crypto_bars method to get the latest bar data for our asset.

Once we have the latest bar data, it is passed into the calc_order_prices function. We will review this function more in depth later but in short it helps us calculate the latest buying and selling prices based on the bar data it receives.

We also update the current position of the asset in our portfolio if we have a position.

def calc_order_prices(bars_df):

    global spread, total_fees, current_price
    max_high = bars_df['high'].max()
    min_low = bars_df['low'].min()
    mean_vwap = bars_df['vwap'].mean()
    current_price = bars_df['close'].iloc[-1]

    logger.info("Closing Price: {0}".format(current_price))
    logger.info("Mean VWAP: {0}".format(mean_vwap))
    logger.info("Min Low: {0}".format(min_low))
    logger.info("Max High: {0}".format(max_high))

    # Buying price in 0.2% below the max high
    selling_price = round(max_high*0.998, 1)
    # Selling price in 0.2% above the min low
    buying_price = round(min_low*1.002, 1)

    buying_fee = trading_fee * buying_price
    selling_fee = trading_fee * selling_price
    total_fees = round(buying_fee + selling_fee, 1)

    logger.info("Buying Price: {0}".format(buying_price))
    logger.info("Selling Price: {0}".format(selling_price))
    logger.info("Total Fees: {0}".format(total_fees))

    # Calculate the spread
    spread = round(selling_price - buying_price, 1)

    logger.info(
        "Spread that can be captured with buying and selling prices: {0}".format(spread))

    return buying_price, selling_price

In the code snippet above, we are concerned with 3 prices from our bar data: closing price, minimum low, maximum high. Our buy/sell limit orders are based on these prices.

In this example, we are calculating the buying_price by taking the minimum of the bar data over the last few minutes (5 mins) + 0.2%. We add the 0.2% with the assumption that a limit order placed at a trivial percentage amount above the minimum low is more likely to be executed than the actual minimum low. Similarly, selling_price is calculated by taking the maximum of the bar data over the last few minutes - 0.2%.

Once we have our buying and selling prices, we can calculate the fees we will need to pay to execute both the buying and selling trades. Buying and selling crypto via Alpaca incurs fees based on our trailing 30 day trading volume. You can read more about the fee tiers here.

Once the fees, buying and selling prices are calculated, we log them to our console.

def get_positions():
   positions = trading_client.get_all_positions()

   return positions


def get_open_orders():

   orders = trading_client.get_orders()

   num_orders = len(orders)
   logger.info("Number of open orders: {0}".format(num_orders))

   global buy_order, sell_order

   for i in range(len(orders)):
       ord = json.loads(orders[i].json())
       logger.info("Order type: {0} Order side: {1} Order notional: {2}  Order Symbol: {3} Order Price: {4}".format(
           ord['type'], ord['side'], ord['notional'], ord['symbol'], ord['limit_price']))
       if ord['side'] == 'buy':
           buy_order = True
       if ord['side'] == 'sell':
           sell_order = True

   return num_orders

Let's take a look at these helper functions defined above. Both of these functions use trading client methods. The first one is quite trivial in its implementation and retrieves the open positions on our Alpaca trading account.

get_open_orders retrieves all the open orders (buy limit/sell limit) we have on our trading account. Based on the open order in place, we set the global variables buy_order and sell_order. We will use these variables later when we are checking for our trade conditions. It is also important to log the order in place which makes it easier to monitor the bot status.

async def post_alpaca_order(buy_price, sell_price, side):
   '''
   Post an order to Alpaca
   '''
   global buy_order_price, sell_order_price, buy_order, sell_order
   try:
       if side == 'buy':
           # print("Buying at: {0}".format(price))
           limit_order_data = LimitOrderRequest(
               symbol="ETHUSD",
               limit_price=buy_price,
               notional=notional_size,
               side=OrderSide.BUY,
               time_in_force=TimeInForce.GTC,
               client_order_id="scalping_bot")
           buy_limit_order = trading_client.submit_order(
               order_data=limit_order_data
           )
           buy_order_price = buy_price
           sell_order_price = sell_price
           # buy_order = True
           logger.info(
               "Buy Limit Order placed for ETH/USD at : {0}".format(buy_limit_order.limit_price))
           return buy_limit_order
       else:
           limit_order_data = LimitOrderRequest(
               symbol="ETHUSD",
               limit_price=sell_price,
               notional=notional_size,
               side=OrderSide.SELL,
               time_in_force=TimeInForce.GTC,
               client_order_id="scalping_bot"
           )
           sell_limit_order = trading_client.submit_order(
               order_data=limit_order_data
           )
           sell_order_price = sell_price
           buy_order_price = buy_price
           # sell_order = True
           logger.info(
               "Sell Limit Order placed for ETH/USD at : {0}".format(sell_limit_order.limit_price))
           return sell_limit_order

   except Exception as e:
       logger.exception(
           "There was an issue posting order to Alpaca: {0}".format(e))
       return False

Next, let's go through how the order execution works for this bot. buying_price, selling_price and side of the trade are passed in as parameters to the function above. Based on the side of the trade, we create a limit order request using Alpaca-py's LimitOrderRequest method. Here, we can specify the symbol of the asset, amount we would like to trade (notional), the side of the trade and time till when the order should stay in place (time_in_force). The limit order request we just created can then be submitted as an order using our trading client's submit_order method. Once the orders are placed, we set the order prices sell_order_price and buy_order_price`. These are important variables as they tell us what prices our order was placed at. buying_price and selling_price keep changing every time we run through the main loop (every 60 seconds) but sell_order_price and buy_order_price should not change once the order has been placed. This is to ensure that we do not keep moving our target entry and exit once we have established one side of the trade. If our existing orders are canceled, the order prices should reset to when they are placed again.

async def check_condition():
   '''
   Check the market conditions to see what limit orders to place
 
   Strategy:
   - Only consider placing orders if the spread is greater than the total fees after fees are taken into account
   - If the spread is greater than the total fees and we do not have a position, then place a buy order
   - If the spread is greater than the total fees and we have a position, then place a sell order
   - If we do not have a position, a buy order is in place and the current price is more than price we would have sold at, then close the buy limit order
   - If we do have a position, a sell order is in place and the current price is less than price we would have bought at, then close the sell limit order
   '''
   global buy_order, sell_order, current_position, current_price, buying_price, selling_price, spread, total_fees, buy_order_price, sell_order_price
   get_open_orders()
   logger.info("Current Position is: {0}".format(current_position))
   logger.info("Buy Order status: {0}".format(buy_order))
   logger.info("Sell Order status: {0}".format(sell_order))
   logger.info("Buy_order_price: {0}".format(buy_order_price))
   logger.info("Sell_order_price: {0}".format(sell_order_price))
   # If the spread is less than the fees, do not place an order
   if spread < total_fees:
       logger.info(
           "Spread is less than total fees, Not a profitable opportunity to trade")
   else:
       # If we do not have a position, there are no open orders and spread is greater than the total fees, place a limit buy order at the buying price
       if current_position <= 0.01 and (not buy_order) and current_price > buying_price:
           buy_limit_order = await post_alpaca_order(buying_price, selling_price, 'buy')
           sell_order = False
           if buy_limit_order:  # check some attribute of buy_order to see if it was successful
               logger.info(
                   "Placed buy limit order at {0}".format(buying_price))
 
       # if we have a position, no open orders and the spread that can be captured is greater than fees, place a limit sell order at the sell_order_price
       if current_position >= 0.01 and (not sell_order) and current_price < sell_order_price:
           sell_limit_order = await post_alpaca_order(buying_price, selling_price, 'sell')
           buy_order = False
           if sell_limit_order:
               logger.info(
                   "Placed sell limit order at {0}".format(selling_price))
 
       # Cutting losses
       # If we have do not have a position, an open buy order and the current price is above the selling price, cancel the buy limit order
       logger.info("Threshold price to cancel any buy limit order: {0}".format(
                   sell_order_price * (1 + cut_loss_threshold)))
       if current_position <= 0.01 and buy_order and current_price > (sell_order_price * (1 + cut_loss_threshold)):
           trading_client.cancel_orders()
           buy_order = False
           logger.info(
               "Current price > Selling price. Closing Buy Limit Order, will place again in next check")
       # If we have do have a position and an open sell order and current price is below the buying price, cancel the sell limit order
       logger.info("Threshold price to cancel any sell limit order: {0}".format(
                   buy_order_price * (1 - cut_loss_threshold)))
       if current_position >= 0.01 and sell_order and current_price < (buy_order_price * (1 - cut_loss_threshold)):
           trading_client.cancel_orders()
           sell_order = False
           logger.info(
               "Current price < buying price. Closing Sell Limit Order, will place again in next check")

The code snippet above describes our scalping strategy. We start by logging the current status of the orders placed (buy/sell limit orders) and our current position. If the spread we can capture is greater than the current price of the asset, we can consider placing an order otherwise it does not make much sense.

If the spread is greater than the current price, there are a few scenarios to consider:

  1. If we do not have a current position, do not have an open buy limit order and the current price is less than the buying price, place a buy limit order.
  2. Similarly if we do have a position, do not have an open sell limit order and the current price of the asset is less than the sell_order_price when the buy trade was established, then we can place a sell limit order. It is important to compare the current price to the sell_order_price as it needs to be in relation to the buy order we placed and the prices at that moment.
  3. In a scenario where we do not have a position, have a buy order placed but the price has trended upwards and is above the sell order price relative to the current price by a small threshold percentage, then we can cancel our buy limit order and recalculate the prices we need to place our trades at.
  4. In the opposite scenario, if we do have a position, have a sell order placed but the price has trended downwards and is below the buy_order_price by a small threshold percentage, then we can cancel our sell limit order and recalculate the prices we need to place our trades at.

The code above outlines these scenarios and logs the current status based on the path it follows.


Takeaways

Alpaca-py makes it super easy to fetch positions, orders, retrieve market data and place orders. The documentation is amazing and gives a clear explanation on how to use it.

When the spreads are thin, we can expect the algorithm to execute more trades but for a smaller profit. On the other hand, if the spread to capture is large, it might take a while for the trade to execute. These tradeoffs can be adjusted by tweaking variables like diff, cut_loss_threshold and the percentage difference from the minimum low and maximum high you decide to place your buying and selling prices at. Keep in mind that higher this percentage is the lower spread you can capture.

Sources

  1. https://www.investopedia.com/articles/trading/05/scalping.asp

Please note that this article is for informational purposes only. The example above is for illustrative purposes only. Actual crypto prices may vary depending on the market price at that particular time. Alpaca Crypto LLC does not recommend any specific cryptocurrencies.

Cryptocurrency is highly speculative in nature, involves a high degree of risks, such as volatile market price swings, market manipulation, flash crashes, and cybersecurity risks. Cryptocurrency is not regulated or is lightly regulated in most countries. Cryptocurrency trading can lead to large, immediate and permanent loss of financial value. You should have appropriate knowledge and experience before engaging in cryptocurrency trading. For additional information please click here.

Cryptocurrency services are made available by Alpaca Crypto LLC ("Alpaca Crypto"), a FinCEN registered money services business (NMLS # 2160858), and a wholly-owned subsidiary of AlpacaDB, Inc. Alpaca Crypto is not a member of SIPC or FINRA. Cryptocurrencies are not stocks and your cryptocurrency investments are not protected by either FDIC or SIPC. Please see the Disclosure Library for more information.

This is not an offer, solicitation of an offer, or advice to buy or sell cryptocurrencies, or open a cryptocurrency account in any jurisdiction where Alpaca Crypto is not registered or licensed, as applicable.