Creating a Slackbot on AWS using Golang – Part 2 – Price-based Alerting

Introduction

In the previous article, I talked about how to create a Slack Slash Command that would return the current price of a stock. So, you could enter the command /quote MSFT into a Slack message field and it would return the current price of Microsoft stock.

The Golang-based server was first run locally and then migrated to AWS using Elastic Beanstalk.

The article ended with a list of features that I would like to eventually implemented in my little Go/AWS/Slack application. This article, and subsequent articles, will focus on developing some of these features.

For this article, I wanted to implement an alerting feature in the Stockbot. With this feature, someone could enter a target price of a stock and be alerted when the current price of the stock went above or fell below the target. Maybe AMZN stock fell below $1000 a share and you want to rush to your financial advisor and buy a share or two?

This new feature requires creating a database which will be used to hold both the alerting subscriptions and the current prices of all of the stocks that all users are interested in. This will let us introduce how to set up a database in AWS and how to talk to that database from a Go application.

A New Branch

Let’s go to our Git repository for a second. We would like to create a feature branch for the new alerting feature. So let’s create a new branch on our local machine.

You can create a new branch through the command line

git checkout -b alerting

or the branch can be created from inside Visual Studio Code:

Business Requirements – Designing the New Slash Command

The requirements of the new command are simple.

A user will tell the Stockbot that they want to be notified asynchronously through Slack whenever the current price of a stock goes above or below a certain price target.

The Stockbot will poll the quote service at regularly-scheduled intervals and will retrieve the current prices of all of the stocks that users want to be alerted on. Whenever a price breaches the alerting price, the Stockbot will send a message to the user.

By default, the user will be notified in Slack by a Direct Message (DM). The user can also choose to be notified through a specific channel. Usually, that channel is a private channel that the user has set up, just for price alerts, but it can also be a public channel.

As far as additional user interactions go, we would also like a way to list all of the alerts that a user has, a way to delete a specific alert, and a way to delete all alerts.

When the user creates an alert, we want to make sure that the symbol is a valid stock. If not, an error should be returned. If the user already has an alert for this symbol, the alert will be updated with the new price target (and possibly with the new direction).

Given these requirements, we can design the new slash command.

/quote-alert [symbol price [below]] [symbol delete] [deleteall] [#channel]

Examples:

/quote-alertlists all of the alerts you have
/quote-alert HELPprints a help message
/quote-alert MSFT 130sends an alert when Microsoft stock reaches $130
/quote-alert MSFT 130 #myalertssends an alert to the #myalert channel when MSFT stock reaches $130
/quote-alert MSFT 130 BELOWsends an alert when Microsoft stock goes below $130
/quote-alert MSFT deleteremoves the existing alert on MSFT stock that you have subscribed to
/quote-alert deletealldeletes all alerts that you have

The Alerts Database

Given these requirements, we can design the schema for a database that will hold the subscriptions. The database can also hold current prices.

Amazon’s RDS service gives the developer a choice of several different databases to create. For this exercise, let’s choose Postgres since it is one of the databases available on the RDS Free Tier.

Every alert should have at least the following properties:

  • A unique id
  • The id of the Slack user that created the alert
  • The symbol of the stock that the user wants to monitor
  • The target price of the stock
  • The “direction” of the check (above or below the price)
  • The Slack channel that the user wants to be notified in
    • If the channel is empty, then the user should be sent a direct message through Slack
  • An indication that tells us whether this alert has been triggered
    • In case the sending of the alert is slow, we don’t want alerts to pile up

We also would like a simple table that holds the current price for each symbol that has an alert on it.

Let’s look at the SQL that will be used to create the database. Since we will be creating a Postgres database, the SQL below has the Postgres dialect.

create type slackstockbot.direction as enum ('ABOVE', 'BELOW');

alter type slackstockbot.direction owner to magmasystems;

create table slackstockbot.alertsubscription
(
  id serial not null
     constraint alertsubscription_pk
        primary key,
  slackuser varchar(128) not null,
  symbol varchar(16) not null,
  targetprice double precision not null,
  wasnotified boolean default false,
  direction slackstockbot.direction default 'ABOVE'::slackstockbot.direction,
  channel varchar(128) default ''::character varying not null
);

alter table slackstockbot.alertsubscription owner to magmasystems;

create unique index alertsubscription_id_uindex
  on slackstockbot.alertsubscription (id);

create table slackstockbot.stockprice
(
  symbol varchar(32) not null,
  price double precision not null,
  time timestamp
);

alter table slackstockbot.stockprice owner to magmasystems;

create index stockprice_symbol_index
  on slackstockbot.stockprice (symbol);

In addition to the two tables shows above, we may want to think of having a table with administrative info, such as the time that the last polling was done, the frequency of the polling, and the name of the quote service to pull from. We will leave this for a future exercise.

Finding Price Breaches using SQL

We can join the AlertSubscriptions with the current prices and find all rows that have prices that are either above or below the price target.

SELECT a.slackuser, a.webhook, a.symbol, a.targetprice, a.direction, p.price
  FROM slackstockbot.alertsubscription a, slackstockbot.stockprice p
  WHERE a.wasnotified = false AND a.symbol = p.symbol AND p.price > 0 AND
     ( (a.direction = 'ABOVE' AND p.price >= a.targetprice) OR 
       (a.direction = 'BELOW' AND p.price <= a.targetprice) )

Creating the Database in AWS

If you recall from the previous article, we created a development environment for the Slack Stock Bot on Elastic Beanstalk.

If you click on the green box, you will see the dashboard for the SlackStockBot environment.

In the side panel, click on Configuration. Then scroll down until you see a panel for the Database. You will notice that it is empty.

After you click on the Modify link, you will see a list of databases that are associated with this environment. Click the Create Database button.

You will be presented with a list of database engines. If you are looking to save money, make sure that you check the box at the bottom which will only present you with options that are eligible for the RDS Free Tier. We will choose Postgres.

Name the database and pick the authentication credentials.

In the Network and Security section, I like to make the database publicly accessible so that I can administer the database from my local machine using tools like DbVisualizer or DataGrip.

After the database is created, I will use something like DataGrip to create the tables using the SQL that was shown above.

In addition to setting up this Postgres database in AWS, I also set up a local version of the database for local testing. If you recall from the first article, we used localtunnel in order to have Slack interact with a local version of the Slack Stock Bot.

Progress So Far

We designed the API for the new /quote-alert command. We also created a database and created the two tables that will hold the alert subscriptions and the local prices.

The next stage is to create a new Slash Command in Slack and hook it up to the new version of our server. Then we will write the Golang code which implements the AlertManager.

Adding the New Slash Command to Slack

In the previous article, we saw how to add a new Slash Command to Slack. Let’s do the same thing again. We will create the new /quote-alert slash command.

Once the Slash Command has been created, we need to give it permission to send a message to a user directly and to a specific channel. Click on the OAuth & Permissions link on the side panel.

Then pull down the dropdown under Select Permission Scopes and choose the two permissions.

Click on the Save Changes button.

Now that the permissions have been granted to perform certain actions, we need to set up two Webhooks for the communication. First, enable Webhooks in your command.

In the side panel, click on Incoming Webhooks, and make sure that the webhooks are activated.

Scroll down a bit and create two new webhooks.

At the end of this process, you should have two Webhooks, one for posting to a channel and one for sending a message to a user.

By default, a /quote-alert will send a price alert directly to the user with one of the webhooks. If you enter the command

/quote-alert MSFT 130 #myalerts

then the alert will be sent to the #myalerts channel, using the other webhook.


Modifying the Golang Source

Now that all of the environmental stuff has been set up, we can write some Golang code.

The Source code is located here (alerting branch)
https://github.com/magmasystems/SlackStockSlashCommand

In order to access the Postgres database, we use the pq package. You need to install this package from github.com/lib/pq, and then reference it within the application.

go get github.com/lib/pq

Major Changes to the Code

There have been several things added to the version of the Slack Stock Bot that was developed in the previous article. We are not going to cover each change in this article. But, at a high level, those changes include:

  • The introduction of environment-specific configuration files (appSettings[.env].json), plus a configuration manager
  • A logging manager
  • A Slack Messaging package that encapsulates all interactions with Slack
  • An AlertManager that encapsulates all of the price breach alerting logic
  • Integration with Postgres (either local or RDS)

Changes to the Configuration File

A new Database section has been added to the appSettings.json file. This contains the standard database connection information that will be used to connect to Postgres. There are two new fields for the webhooks that the alerting mechanism will use to send messages back to Slack. Finally, there is the quoteInterval, which is the number of seconds that will elapse between price checks. Bear in mind that the free quote services will limit the number of quotes that you can request per day, so you do not want your price checker running too frequently.

{
   "apiKeys": {
       "quandl": "[Your Quandl API Key]",
       "worldtrading": "[Your World Trading Data API Key]",
       "alphavantage": "[Your AlphaVantage API Key]"
   },
   "driver": "alphavantage",
   "slackSecret": [Your Slack App's Secret Key]",
   "webhook": "https://hooks.slack.com/services/[Your Webhook for Channels]",
   "dmwebhook": "https://hooks.slack.com/services/[Your Webhook for direct messaging]",
   "port": 5000,
   "database": {
       "host": "slackstockbot.XXXXXXXX.us-east-2.rds.amazonaws.com",
       "port": 5432,
       "dbname": "slackstockbot",
       "user":  "[Your database user name]",
       "password": "[Your database password]",
       "SSL": true
   },
   "quoteCheckInterval": 600,
   "disablePriceBreachChecking": false
}

Polling for Price Breaches

In application.go, a Ticker is created using an interval which is set in the appSettings.json configuration file. Every time the ticker elapses, a function is called to check the prices.

// Create a ticker that will continually check for a price breach
if !appSettings.DisablePriceBreachChecking {
    priceBreachCheckingTicker = time.NewTicker(time.Duration(appSettings.QuoteCheckInterval) * time.Second)
    defer priceBreachCheckingTicker.Stop()

    // Every time the ticker elapses, we check for a price breach
    go func() {
        for range priceBreachCheckingTicker.C {
            onPriceBreachTickerElapsed()
        }
    }()
}

The responsibility for the price checks is in the AlertManager. We pass a callback function that the AlertManager calls for every price breach. This callback will create an informative message and will post it to Slack using a webhook.

// onPriceBreachTickerElapsed - This gets called every time the Price Breach Ticker ticks
func onPriceBreachTickerElapsed() {
    theAlertManager.CheckForPriceBreaches(theBot, func(notification alerts.PriceBreachNotification) {
        outputText := fmt.Sprintf("%s has gone %s the target price of %3.2f. The current price is %3.2f.\n",
            notification.Symbol, notification.Direction, notification.TargetPrice, notification.CurrentPrice)
        postSlackNotification(notification, outputText)
    })
}

The check for price breaches works like this:

  • Get a list of all of the stocks that have alerts on them
  • Call the quote service to get the current prices for all of the stocks
  • Save the prices to the database
  • Use SQL to check for price breaches. The SQL code for the check is shown at the start of this article.
  • For each alert that was triggered, set a flag that “logically deletes” the alert so that we do not check again.
    • We can enhance the /quote-alert command so that an alert can be reset
  • Call the passed-in callback function, which is responsible for alerting Slack.
// CheckForPriceBreaches - gets called by the application at periodic intervals to check for price breaches
func (alertManager *AlertManager) CheckForPriceBreaches(stockbot *stockbot.Stockbot, callback func(PriceBreachNotification)) {
    // Get the latest quotes
    prices := alertManager.GetQuotesForAlerts(stockbot)
    if prices == nil {
        return
    }

    // Save the prices to the database
    alertManager.SavePrices(prices)

    // Check for any price breaches
    notifications := alertManager.GetPriceBreaches()

    // Go through all of the price breaches and notify the Slack user
    for _, notification := range notifications {
        // Set the wasNotified field to TRUE on the alert
        alertManager.setWasNotified(notification.SubscriptionID)

        // Do the notification to slack synchronously
        callback(notification)
    }
}

A Word About Architecture and Strategy

By this time, you must be wondering why we used a SQL-based function to detect price breaches, especially if we were ever going to support real-time streaming quotes. After all, making calls to the database is costly in terms of performance, latency, and (in the case of RDS), monetary cost.

Wouldn’t we be much better off using some in-memory collections? For example, we could use a map where the keys are the list of stocks that have active alerts, and each value could be a sorted collection of alerts.

One of the reasons that I chose the database-centric way of doing the comparison is just so that I could find a way to introduce a database in this series of articles. I wanted to give the reader exposure to uses databases both in Golang and in an Elastic Beanstalk environment.

If we wanted to be architecturally flexible, we could introduce a Strategy Pattern. We could have a strategy for database-based comparisons and a different strategy for memory-based comparisons.

We can implement the Strategy Pattern by using a factory to create the quote comparator, and we can assign the comparator to a field within the AlertManager struct.

type QuoteComparator interface {
    findPriceBreaches(alerts AlertMap, currentQuotes []QuoteInfo)
}

type AlertManager struct {
    . . .
    quoteComparator QuoteComparator
    . . .
}

func createAlertManager() {
    . . . 
    alertManager.quoteComparator = quoteComparatorFactory("memory")
    . . . 
}

func quoteComparatorFactory(strategy string) (comparator QuoteComparator, errs error) {
    switch strategy {
    case "database":
        return &amp;DatabaseQuoteComparator{}, nil
    case "memory":
        return &amp;MemoryQuoteComparator{}, nil
    default:
        return nil, errors.New("the strategy cannot be found")
    }
}

Deploying to Elastic Beanstalk

We need to change the Buildfile so that it fetches the pq library for Postgres, and so that the app’s configuration file is located in the same directory as the binary. The new Buildfile is:

go get github.com/nlopes/slack
go get github.com/lib/pq
go build -o bin/application application.go
cp ./appSettings.json bin

An important thing to note is that, by default, Go applications on Elastic Beanstalk use Port 5000. If you change the port from within the configuration file, then you should also tell Slack that the Stock Bot command uses the new port.

Another thing that we might want to consider is, instead of using RDS, using Docker and deploying our own Postgres database. Elastic Beanstalk fully supports setting up Go applications using Docker. We can leave Docker to a future article.

Testing the new Slash Command

Let’s put in an alert for Johnson and Johnson’s stock.

/quote-alert JNJ 140.0

We see that the Slack Stock Bot works

That message looks a lot nicer than just printing out plain old text. Slack allows you to format output in different ways.

attachment := slack.Attachment{
    Color:    "good",
    Fallback: "You successfully posted by Incoming Webhook URL!",
    Text: outputText,
    //Footer:        "slack api",
    //FooterIcon:    "https://platform.slack-edge.com/img/default_application_icon.png",
    Ts: json.Number(strconv.FormatInt(time.Now().Unix(), 10)),
}

msg := slack.WebhookMessage{
    Attachments: []slack.Attachment{attachment},
    Username:    slackUserName,
    Channel:     slackChannel,
}
    
slack.PostWebhook(getWebhook(slackChannel, appSettings), &amp;msg)

Slack also supports something called Blocks, which allow more complex formatting and options for the user to interact with your message. Conceivably, we can use Blocks to present a “Buy” or “Sell” button, which would generate an order to the user’s financial advisor.

Merging the Alerting Branch Into Master

Now that we are done implementing the alerting feature, we can merge the alerting branch back into the master.

Go to the Github repository.

Click on the green button that is labeled Compare & pull request.

Type in some comments and then click on Create pull request.

You will see that there are no merge conflicts. Click on the Merge pull request button.

Confirm the merge

You will get the confirmation that everything was merged successfully, Now you can pull the new branch to your local machine.

Summary

In this article, we enhanced the original Slack Stock Bot code so that the user could subscribe to alerts. The alerts were stored in an AWS RDS database, and we used a SQL-based strategy to detect any price breaches. We came up with a new Slash Command called /quote-alert which allows a user to create or delete a price breach alert. Finally, we deployed the new code to Elastic Beanstalk and successfully tested it out.

In the next article, we will make a few more enhancements. One of the things that I am thinking of is making the price comparison into an AWS Lambda function. We can also use the new Slack Messaging to implement a simple workflow. We should start putting in unit tests, and we can start taking advantage of AWS CodeBuild and CodeDeploy.

Stay tuned for the next article.

Appendix

Trouble Connecting to the Postgres Database from Elastic Beanstalk

If you find that you are having problems connecting the Slack Stock Bot to RDS, go into the EC2 instance that hosts the database and change the Incoming Connection rules.

  1. Go into the RDS dashboard and find your database. Then click on the name of your database.
  2. In the dashboard for your database, go to the Security Group Rules section, and find the Security Group that is associated with Inbound connections. Click on that.
  3. In the Security Group, look at the Inbound tab. Make sure that port 5432 (Postgres) is open to your application.