Creating a Stock Watchlist in Kotlin

Picture of author

By Sean Soper

October 29, 2021

Photo by Oren Elbaz on Unsplash.

Introduction

The stock watchlist is likely one of the first visual tools a new retail investor will be introduced to. It’s both easy to understand and abuse as the new trader fills it up with every speculative penny stock they stumble across. For this project we will use Batil, an open-source library written in Kotlin that can integrate with your broker, to simplify the creation of our real-time stock watchlist.

Setup Batil

Using Batil you can programmatically lookup account info, get options chains and create orders as just a few examples. Before we get to the good stuff we’ll want to ensure you can even connect to your E*TRADE account. You’ll need to request two sets of consumer keys and secrets, one for sandbox and the other production. For the sandbox version, sign into your E*TRADE account and head over to Customer Service ➡ Message Center ➡ Contact Us. From there select the account you want to associate with your API key. For the subject, select API Sandbox Auto and for the topic select Sandbox Key. Expect to hear back within a few hours. To access the production API you’ll need to send a signed copy of the Developer Agreement to [email protected]. A more indepth walk thru can be found here.

Armed with our keys we can then move onto getting Docker running. We need Docker since it is used to access a Chromium instance that can login to the E*TRADE website to retrieve the necessary OAuth keys. No one said E*TRADE’s chosen method of authentication was straight forward. Anyways, let’s go ahead and download Docker and then run the following command to start the container:

% docker container run -d -p 9222:9222 zenika/alpine-chrome \
  --no-sandbox \
  --remote-debugging-address=0.0.0.0 \
  --remote-debugging-port=9222 \
  --user-agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36" \
  about:blank

If you happen to be on an Apple M1 then you’ll want to use this container which has been built for arm64.

% docker container run -d -p 9222:9222 avidtraveler/alpine-chrome \
  --no-sandbox \
  --remote-debugging-address=0.0.0.0 \
  --remote-debugging-port=9222 \
  --user-agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36" \
  about:blank

With the boring stuff out of the way we can now go ahead and create our Kotlin project in IntelliJ and give it a fun name. You’ll need to add a dependency on the Batil package in your build.gradle.kts file.

repositories {
    mavenCentral()
}

dependencies {
    implementation("com.seansoper:batil:1.0.1")
}

Create the Session

Remember those E*TRADE keys you retrieved earlier? You’ll want to place them in a YAML file that’s accessible from where you’re running your app. Its format should be identical to this substituting your values of course. Oh and this part is super important, never ever check this file into git. If someone else got a hold of the values in this file they could do Very Bad Things such as buying PSTH warrants. Might as well set your money on fire 💵🔥.

Now that we have the boring (and slightly scary) stuff out of the way, let’s go ahead and make that much anticipated connection to the E*TRADE API. Every interaction with the API requires passing a Session object so let’s make it easy to create one. And since we are making a real-time watchlist, we’ll need to use production.

fun createSession(): Session {
    val clientConfig = ClientConfig(Paths.get("/path/to/batil.yaml"), verbose = true, production = true)
    val globalConfig = GlobalConfig.parse(clientConfig)
    val client = Authorization(globalConfig, production = true, verbose = true)

    return client.renewSession() ?: client.createSession()
}

Provided your credentials are valid and Docker is running, you should see some cryptic Chromium log messages in your console.

Create Watchlist

We’ll need an object which can accept a list of tickers and keep track of their prices as they are returned by the API. We’ll also want to highlight an increase or decrease in a ticker’s price using both a character, either ↑ or ↓, and color using ANSI terminal codes.

class Watchlist(val tickers: List<String>) {

    private val reset = "\u001B[0m"
    private val red = "\u001B[31m"
    private val green = "\u001B[32m"
    private val default = " "

    val prices: MutableMap<String, Triple<Float, String, String>> = tickers.associate {
        it to Triple(0f, default, reset)
    }.toMutableMap()
}

The prices object looks complicated but imagine it representing data like this.

{ "TickerA": { "31.00", "↑", "green color" },
  "TickerB": { "64.00", "↓", "red color" } }

We’ll want a way to update the price so let’s add a method to our Watchlist class that can handle that as well as translate an increase or decrease in price into the correct character and color codes.

fun setPrice(ticker: String, price: Float) {
    val (trend, color) = getTrendColor(ticker, price)
    prices[ticker] = Triple(price, trend, color)
}

private fun getTrendColor(ticker: String, price: Float): Pair<String, String> {
    return prices[ticker]?.first?.let { currentPrice ->
        if (currentPrice > 0f) {
            if (currentPrice > price) {
                "↑" to green
            } else if (currentPrice < price) {
                "↓" to red
            } else {
                default to reset
            }
        } else {
            default to reset
        }
    } ?: (default to reset)
}

Scan the Watchlist

With our Session and Watchlist we can move onto actually scanning for changes in our tickers’ prices. In addition to making a call to the E*TRADE Market endpoint we will also utilize Kotlin coroutines to create a running loop with a delay of two seconds.

suspend fun scan(watchlist: Watchlist, session: Session, delay: Long = 2000L) {
    val service = Market(session, production = true, verbose = false)
    println()

    while (true) {
        service.tickers(watchlist.tickers)?.let {
            it.forEach { security ->
                security.symbol?.let { ticker ->
                    security.tickerData.lastTrade?.let { price ->
                        watchlist.setPrice(ticker, price)
                    }
                }
            }
        }

        val results = watchlist.prices.map {
            "${it.value.third}${it.key} at ${it.value.first} ${it.value.second}"
        }

        print("\r${results.joinToString("  ")}")

        delay(delay)
    }
}

That suspend keyword is important, it allows the program to run without blocking which is important if we had other code running at the same time. Once the call to Market.tickers returns we unwrap the values and then update the Watchlist object. Finally, we construct a single-line string using the updated values and print it out. Note the use of print("\r…") which resets the carriage position to the beginning of the line so that values update in place on the console.

Final Product

Now we need to put all this together with some tickers and call it.

fun main(args: Array<String>) {
    val session = createSession()
    val watchlist = Watchlist(tickers = listOf(
        "PLTR",
        "TSLA",
        "CLOV",
        "SOFI",
        "AMC"
    ))

    runBlocking {
        scan(watchlist, session)
    }
}

These are not your father’s blue chip stocks but that’s fine since we want to see some activity to verify our code works. The runBlocking is another special keyword which “bridges the non-coroutine world of a regular fun main() and the code with coroutines”. In short, use it when calling code powered by Kotlin coroutines.

When running your app, you should see console output that looks similar to this.

A full summary of the code changes to achieve this can be found here.