You've successfully subscribed to Alpaca Learn | Developer-First API for Crypto and Stocks
Great! Next, complete checkout for full access to Alpaca Learn | Developer-First API for Crypto and Stocks
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.
Success! Your billing info is updated.
Billing info update failed.
Search
Use Cases

Building an End-to-End Trading Bot using Alpaca’s API, CircleCI, and Slack

Alpaca Team
Alpaca Team

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:

Again, this is my paper trading account — I wouldn’t trust this much money to my algo (yet) 💰

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:


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:

  1. trading_classes.py | Contains the TradingOpportunities and Alpaca classes, which handle the logic for identifying trading opportunities and executing trades using the Alpaca API, respectively.
  2. slack_app_notification.py | Contains the slack_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.
  3. main.py | The entry point of the application, which brings together the functionality of the TradingOpportunities, Alpaca, and slack_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.

Use CasesTrading APICommunity Examples

Alpaca Team

API-first stock brokerage. *Securities are offered through Alpaca Securities LLC* http://alpaca.markets/#disclosures