Building an End-to-End Trading Bot using Alpaca’s API, CircleCI, and Slack
I’ll explore the inner workings of a trading bot that uses the Alpaca API, CircleCI, and Slack for trade notifications.
Please note that this article is for general informational purposes only. All examples are for illustrative purposes only. The views and opinions expressed are those of the author and do not reflect or represent the views and opinions of Alpaca. Alpaca does not recommend any specific securities or investment strategies.
This article originally appeared on Medium, written by June Rodriguez
In this article I’ll explore the inner workings of a trading bot that uses the Alpaca API, CircleCI, and Slack for trade notifications. In summary, the bot scans for trading opportunities based on the top losing stocks and popular crypto assets from YahooFinance! It then utilizes the Alpaca API to execute buy & sell orders and sends notifications via Slack about the trades it has made. Here’s an example of a notification the bot sends me via Slack showing the trades it made in my paper trading Alpaca account:
Here’s the GitHub repo if you’d like to skip to the ReadMe and clone the project to make your own modifications.
Let’s dive deep into the logic of the scripts used in the project!
Assumed prerequisite understanding of the reader:
- Object oriented programming (classes & methods) in Python
- How to build a Slack application (here’s a good video on this)
Firstly, what is Alpaca?
Alpaca trading is a commission-free API-first stock brokerage that offers users the ability to buy and sell stocks, exchange-traded funds (ETFs), and cryptocurrency through a single platform. The Alpaca platform provides users with access to real-time market data, account management, and trading functionalities through their REST API, which enables developers to build and integrate their own trading applications.
Additionally, Alpaca offers users the ability to automate their trading strategies through a Paper Trading account, which allows users to simulate trades without risking real money. If you’re looking to create very custom trading algorithms using your Python scripts, this is a solid option to go with!
Overview of the Project Structure
The project consists of several Python files, a config.yml
file for CircleCI, and a creds.cfg
configuration file containing Alpaca and Slack API keys. The main Python files are:
trading_classes.py
| Contains theTradingOpportunities
andAlpaca
classes, which handle the logic for identifying trading opportunities and executing trades using the Alpaca API, respectively.slack_app_notification.py
| Contains theslack_app_notification
function, which generates a formatted string with a summary of the trades made by the bot and sends it as a Slack notification to your desired channel.main.py
| The entry point of the application, which brings together the functionality of theTradingOpportunities
,Alpaca
, andslack_app_notification
classes and functions.
The TradingOpportunities Class
The TradingOpportunities
class is responsible for scraping YahooFinance! to identify trading opportunities based on the top losing stocks and popular crypto assets. It does this using the get_trading_opportunities
and get_asset_info
methods.
get_trading_opportunities() Method
This method scrapes YahooFinance! using the yfinance
package to obtain the top losing stocks of the day and most popular crypto assets. It first gets the top losing stocks by percentage change for the day using get_day_losers
. Then, it obtains the top traded crypto assets by market cap using get_top_crypto
.
get_asset_info() Method
This method filters the list of assets obtained from get_trading_opportunities
by checking each asset's technical indicators and picking just the oversold assets as buying opportunities. I’ve used Bollinger Bands and RSI for this public repo, so feel free to make tweaks to this part of the code. Lines 22–49 do all the magic here:
def get_asset_info(self, df=None):
"""
Description:
Grabs historical prices for assets, calculates RSI and Bollinger Bands tech signals, and returns a df with all this data for the assets meeting the buy criteria.
Argument(s):
• df: a df can be provided to specify which assets you'd like info for since this method is used in the Alpaca class. If no df argument is passed then tickers from get_trading_opportunities() method are used.
"""
# Grab technical stock info:
if df is None:
all_tickers = self.all_tickers
else:
all_tickers = list(df["yf_ticker"])
df_tech = []
for i, symbol in tqdm(
enumerate(all_tickers),
desc="• Grabbing technical metrics for "
+ str(len(all_tickers))
+ " assets",
):
try:
Ticker = yf.Ticker(symbol)
Hist = Ticker.history(period="1y", interval="1d")
for n in [14, 30, 50, 200]:
# Initialize MA Indicator
Hist["ma" + str(n)] = sma_indicator(
close=Hist["Close"], window=n, fillna=False
)
# Initialize RSI Indicator
Hist["rsi" + str(n)] = RSIIndicator(
close=Hist["Close"], window=n
).rsi()
# Initialize Hi BB Indicator
Hist["bbhi" + str(n)] = BollingerBands(
close=Hist["Close"], window=n, window_dev=2
).bollinger_hband_indicator()
# Initialize Lo BB Indicator
Hist["bblo" + str(n)] = BollingerBands(
close=Hist["Close"], window=n, window_dev=2
).bollinger_lband_indicator()
df_tech_temp = Hist.iloc[-1:, -16:].reset_index(drop=True)
df_tech_temp.insert(0, "Symbol", Ticker.ticker)
df_tech.append(df_tech_temp)
except:
KeyError
pass
df_tech = [x for x in df_tech if not x.empty]
df_tech = pd.concat(df_tech)
# Define the buy criteria
buy_criteria = (
(df_tech[["bblo14", "bblo30", "bblo50", "bblo200"]] == 1).any(axis=1)
) | ((df_tech[["rsi14", "rsi30", "rsi50", "rsi200"]] <= 30).any(axis=1))
# Filter the DataFrame
buy_filtered_df = df_tech[buy_criteria]
# Create a list of tickers to trade
self.buy_tickers = list(buy_filtered_df["Symbol"])
return buy_filtered_df
The Alpaca Class
The Alpaca
class is responsible for executing buy and sell orders using the Alpaca API. It contains the sell_orders
and buy_orders
methods to handle these actions.
sell_orders() Method
This method iterates through the assets in the user’s Alpaca account and checks if they meet the selling criteria based on technical indicators that signal they’re overbought. If they do, it generates a sell order for the asset. This is probably the most complex part of the project since we need to free up cash if no existing positions are overbought (and subsequently sold) so the algorithm can continue with buying oversold assets. I’ve incorporated logic that checks if cash is < 10% of the total portfolio and if so, sells an equal amount of the top 25% of performing assets in the portfolio to fill that gap. Line 61 of the snippet below is where this logic is captured.
def sell_orders(self):
"""
Description:
Liquidates positions of assets currently held based on technical signals or to free up cash for purchases.
Argument(s):
• self.df_current_positions: Needed to inform how much of each position should be sold.
"""
# Get the current time in Eastern Time
et_tz = pytz.timezone('US/Eastern')
current_time = datetime.now(et_tz)
# Define the sell criteria
TradeOpps = TradingOpportunities()
df_current_positions = self.get_current_positions()
df_current_positions_hist = TradeOpps.get_asset_info(
df=df_current_positions[df_current_positions['yf_ticker'] != 'Cash'])
# Sales based on technical indicator
sell_criteria = ((df_current_positions_hist[['bbhi14', 'bbhi30', 'bbhi50', 'bbhi200']] == 1).any(axis=1)) | \
((df_current_positions_hist[['rsi14', 'rsi30', 'rsi50', 'rsi200']] >= 70).any(axis=1))
# Filter the DataFrame
sell_filtered_df = df_current_positions_hist[sell_criteria]
sell_filtered_df['alpaca_symbol'] = sell_filtered_df['Symbol'].str.replace('-', '')
symbols = list(sell_filtered_df['alpaca_symbol'])
# Determine whether to trade all symbols or only those with "-USD" in their name
if self.is_market_open():
eligible_symbols = symbols
else:
eligible_symbols = [symbol for symbol in symbols if "-USD" in symbol]
# Submit sell orders for eligible symbols
executed_sales = []
for symbol in eligible_symbols:
try:
if symbol in symbols: # Check if the symbol is in the sell_filtered_df
print("• selling " + str(symbol))
qty = df_current_positions[df_current_positions['asset'] == symbol]['qty'].values[0]
self.api.submit_order(
symbol=symbol,
time_in_force='gtc',
qty=qty,
side="sell"
)
executed_sales.append([symbol, round(qty)])
except Exception as e:
continue
executed_sales_df = pd.DataFrame(executed_sales, columns=['ticker', 'quantity'])
if len(eligible_symbols) == 0:
self.sold_message = "• liquidated no positions based on the sell criteria"
else:
self.sold_message = f"• executed sell orders for {''.join([symbol + ', ' if i < len(eligible_symbols) - 1 else 'and ' + symbol for i, symbol in enumerate(eligible_symbols)])}based on the sell criteria"
print(self.sold_message)
# Check if the Cash row in df_current_positions is at least 10% of total holdings
cash_row = df_current_positions[df_current_positions['asset'] == 'Cash']
total_holdings = df_current_positions['market_value'].sum()
if cash_row['market_value'].values[0] / total_holdings < 0.1:
# Sort the df_current_positions by profit_pct descending
df_current_positions = df_current_positions.sort_values(by=['profit_pct'], ascending=False)
# Sell the top 25% of performing assets evenly to make Cash 10% of the total portfolio
top_half = df_current_positions.iloc[:len(df_current_positions) // 4]
top_half_market_value = top_half['market_value'].sum()
cash_needed = total_holdings * 0.1 - cash_row['market_value'].values[0]
for index, row in top_half.iterrows():
print("• selling " + str(row['asset']) + " for 10% portfolio cash requirement")
amount_to_sell = int((row['market_value'] / top_half_market_value) * cash_needed)
# If the amount_to_sell is zero or an APIError occurs, continue to the next iteration
if amount_to_sell == 0:
continue
try:
self.api.submit_order(
symbol=row['asset'],
time_in_force="day",
type="market",
notional=amount_to_sell,
side="sell"
)
executed_sales.append([row['asset'], amount_to_sell])
except APIError:
continue
# Set the locale to the US
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
# Convert cash_needed to a string with dollar sign and commas
cash_needed_str = locale.currency(cash_needed, grouping=True)
print("• Sold " + cash_needed_str + " of top 25% of performing assets to reach 10% cash position")
return executed_sales_df
buy_orders() Method
This method takes a list of tickers that meet the buying criteria based on YahooFinance! stocks and crypto assets and creates buy orders for each of them using the Alpaca API. It calculates the amount to buy based on the user’s available buying power and the current price of the asset.
slack_app_notification() Function
The slack_app_notification
function generates a formatted summary of the bot's trades and sends it as a Slack notification. It first retrieves the trade history from the Alpaca API using the get_activities
method, then it parses the trade information and formats it into a human-readable message. Finally, it sends the message to a specified Slack channel using the Slack API. You can make tweaks in the main() function (covered next) for when you’d like the bot to send notifications in the channel you’ve enabled your Slack app in.
main.py — Bringing It All Together
main.py
is the entry point of the application and brings together the functionality of the TradingOpportunities
, Alpaca
, and slack_app_notification
classes and functions. It gathers all user configuration details stored in the creds.cfg
file to authenticate all the API connections.
Pushing it to production via CircleCI
CircleCI is a continuous integration and delivery platform that automates the build, test, and deployment of applications. It allows developers to focus on writing code and ensures that the code they write is properly tested and deployed to production. I’ve very easily deployed this bot through the platform with its quick connectivity to GitHub.
CircleCI gives a bunch of free credits monthly, so you can use their servers to run this algorithm many times over. It’s currently set to run 6 times daily in the repo (see the config.yml
file) for example, but I think the free credits are enough to run it tens of times daily.
Closing remarks
Give the repo a try and I’m curious to read your thoughts on what changes you’d make to the trading logic or the pipelines.
Please note that the trading bot shared in this article and associated GitHub repository is for educational and informational purposes only. I am not a financial advisor and I do not make any representations or warranties as to the accuracy, completeness, or timeliness of any of the information provided. The use of this bot and any trading strategies discussed or shared is at your own risk. I am not responsible for any financial outcomes resulting from the use of this bot, and any investment decisions you make are solely your own. Before using this bot or making any investment decisions, please consult with a licensed financial advisor.
Alpaca does not prepare, edit, or endorse Third Party Content. Alpaca does not guarantee the accuracy, timeliness, completeness or usefulness of Third Party Content, and is not responsible or liable for any content, advertising, products, or other materials on or available from third party sites.
All investments involve risk and the past performance of a security, or financial product does not guarantee future results or returns. There is no guarantee that any investment strategy will be successful in achieving its investment objectives. Diversification does not ensure a profit or protection against a loss. There is always the potential of losing money when you invest in securities, or other financial products. Investors should consider their investment objectives and risks carefully before investing.
Securities brokerage services are provided by Alpaca Securities LLC ("Alpaca Securities"), member FINRA/SIPC, a wholly-owned subsidiary of AlpacaDB, Inc. Technology and services are offered by AlpacaDB, Inc.
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 securities or cryptocurrencies, or open a brokerage account or cryptocurrency account in any jurisdiction where Alpaca Securities or Alpaca Crypto respectively, are not registered or licensed, as applicable.