Monday, August 11, 2025

Building a Stock Watching App


Carrying on from where we left off, it's time to build an app to watch these stock prices in Neptune. This consists of three parts:
  • a web app that lives in the browser and listens on a websocket for updates;
  • a web server that lives in the cloud and receives price updates and distributes them to interested listeners;
  • a simple tool for updating the stock prices.
I'm going to do this across four episodes:
  • this time we're going to build the webapp and have it run with a "mock" (or double) price provider, entirely in the browser, so we can have something working;
  • next time, we're going to set up the web server in the cloud and use that to deliver fake starting prices, but we won't generate any updates;
  • the third installment will be to allow us to generate a local publishing tool with updates stock prices and communicates with the cloud, but "hacking in" the websocket subscription;
  • and then we'll connect all of that to our Neptune database, rounding off the corners and getting everything working.

Webapps are Easy

It seems to me that the act of writing a webapp is relatively easy and not really worthy of discussion here, particularly when it is this easy. So I'm just going to present it and provide a handful of comments.

Let's start with the HTML:
<!DOCTYPE html>
<html>
    <head>
        <title>Stock Watcher</title>
        <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
        <link rel="stylesheet" href="css/stocks.css" />
        <script type="module" src="js/stocks.js"></script>
        <template id="stockrow"><tr><td class='ticker'>fred<td class='price'>2020</td></tr></template>
    </head>
    <body>
        <div class="title">Stock Watcher</div>
        <table class="stock-table">
            <tbody>
                <tr><th class="ticker">Ticker</th><th class="price">Price</th></tr>
            </tbody>
        </table>
    </body>
</html>

NEPTUNE_WEBAPP_MOCK:neptune/app/index.html

Yes, it really is this simple. Basically it's a title and a table. The table is, of course, initially empty. Most of the ceremony relates to including JavaScript and CSS. The <meta> tag is required to get out of "stupid mode" on mobile devices.

The only mildly interesting thing here is the <template> line, which defines a template for each row we want to insert into table (one for each stock we will be watching).

The CSS is largely equally dull:
div.title {
    text-align: center;
    font-family: Arial, Helvetica, sans-serif;
    font-size: 2rem;
    font-weight: bold;
    margin-bottom: 0.5rem;
}

th.ticker, th.price {
    font-size: 1.3rem;
}

table.stock-table {
    width: 100%;
    max-width: 600px;
    margin: auto;
}

.ticker {
    text-align: left;
    width: 50%;
}

.price {
    text-align: right;
    width: 50%;
    padding: 5px;
}

td.ticker {
    font-weight: bold;
}

td.price.green {
    background-color: green;
}

td.price.faded {
    background-color: transparent;
    transition: background-color 1.5s;
}

td.price {
    background-color: transparent;
}

NEPTUNE_WEBAPP_MOCK:neptune/app/css/stocks.css

Again, this is fairly vanilla CSS that I threw together (you could probably do better). The one slightly difficult thing is my implementation of the "repeated yellow-fade technique" which involves the class faded with its associated transition. When prices are updated, we want to make them flash green (if they've gone up) or red (if they've gone down). At the moment, our test data is only considering them going up, so I've only got as far as green. But we want this to transition back to transparent after a short while.

The JavaScript

The JavaScript is spread over three files (at the moment):
  • a main file (stocks.js) that sets everything up;
  • a mockquoter that is currently responsible for generating prices, but will eventually be replaced by a websocket listener;
  • a quotewatcher that is responsible for accepting the updates and updating the display.
The stocks.js file basically sets up to handle the load event and get everything running:
import { MockStockQuoter } from './mockstockquoter.js';
import { QuoteWatcher } from './quotewatcher.js';

window.addEventListener("load", () => {
    var table = document.querySelector(".stock-table tbody");
    var templ = document.getElementById("stockrow");
    var quoteWatcher = new QuoteWatcher(table, templ);
    new MockStockQuoter().provideQuotesTo(quoteWatcher);
});

NEPTUNE_WEBAPP_MOCK:neptune/app/js/stocks.js

It pulls the table and the template out of the DOM and passes them to a QuoteWatcher that it creates.

It then creates a MockStockQuoter and tells it to pass quotes to the watcher.

The mockstockquoter.js file is just there to provide some stream of data to check that we can get something working:
class MockStockQuoter {
    constructor() {
        this.eiqq = 2205;
    }

    provideQuotesTo(lsnr) {
        var me = this;
        lsnr.quotes([{ticker: "EIQQ", price: 2197}, {ticker: "MODD", price: 1087}, {ticker: "QUTI", price: 3030}]);


        setTimeout(() => me.nextQuote(lsnr), 1500);
    }

    nextQuote(lsnr) {
        lsnr.quotes([{ticker: "EIQQ", price: this.eiqq}]);
        this.eiqq += 10;
        if (this.eiqq < 2300) {
            setTimeout(() => this.nextQuote(lsnr), 3000);
        }
    }
}

export { MockStockQuoter }

NEPTUNE_WEBAPP_MOCK:neptune/app/js/mockstockquoter.js

The main thing here is provideQuotesTo. This immediately turns around and sends a list of quotes to the listener. It then kicks off a timer to send an update.

The constructor and nextQuote then collaborate to send updates to the EIQQ stock price every 3s until the price goes above 2300. This gives us a repeatable test bed where every time we load the page we receive one initial message and ten updates.

Finally, what passes for heavy lifting here, the code to update the table when we receive updates is in quotewatcher.js:
class QuoteWatcher {
    constructor(table, templ) {
        this.table = table;
        this.templ = templ;
        this.curr = [];
    }

    quotes(lq) {
        for (var q of lq) {
            var matched = false;
            for (var r of this.curr) {
                if (q.ticker == r.ticker) {
                    this.updatePrice(r.elt, q.price);
                    fadeColor(r.elt.querySelector(".price"), "green");
                    matched = true;
                    break;
                }
            }
            if (!matched) {
                var node = this.templ.content.cloneNode(true).children[0];
                node.querySelector(".ticker").innerText = q.ticker;
                this.updatePrice(node, q.price);
                this.table.appendChild(node);

                this.curr.push({ticker: q.ticker, price: q.price, elt: node});
            }
        }
    }

    updatePrice(node, price) {
        var quote = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(price/100)
        node.querySelector(".price").innerText = quote;
    }
}

function fadeColor(elt, style) {
    elt.classList.add(style);
    elt.classList.remove("faded");
    setTimeout(() => {
        elt.classList.add("faded");
    }, 10);
}

export { QuoteWatcher } 

NEPTUNE_WEBAPP_MOCK:neptune/app/js/quotewatcher.js

The important method here is quotes: this is called every time there is new quote information and contains a list of pairs of symbol and price. If we already have the symbol, we update the price and make it flash; if we don't have the symbol, we add a new row at the end (yes, we could sort alphabetically, but we can also not). Note that we copy all of the incoming information into our own list (this.curr) and also track the DOM element we created (elt) for easier updating.

The updatePrice method is really just about formatting the price as dollars, given that the data format is in an (integer) number of pence.

The fadeColor function is the implementation of the "repeated yellow fade technique". You can't just add the faded class, because it could already be on the element from "last time". So we need to remove it first. But browsers optimise for change-unchange pairs, so that doesn't do anything either. So we need to remove it now, then come back in "a little while" and add it back on.

Conclusion

Yes, that really is it for the mock application. It really isn't that complicated.

No comments:

Post a Comment