NaaN Tutorial: A Bitcoin Ticker

NaaN Tutorial: A Bitcoin Ticker

Getting started

The best way to learn a new software platform is to jump right in and start write code. This tutorial describes how one might develop a Bitcoin ticker, which provides the current price of Bitcoin denominated in US Dollars.

The first thing we need is a data source. CoinCap provides a convenient REST API. For example: https://api.coincap.io/v2/assets?ids=bitcoin returns current BTC price data in JSON format.

We can use the JavaScript fetch API to request a quote, but it's convenient to define a helper function because two separate asynchronous calls are required, and we need to do some error checking on responses.

closure fetch(url, local error, response, json) {
    `(error, response) = await((js.g||js.w).fetch(url))
    if !error {
        if response.status < 200 || response.status > 299
            error = Error("fetch failed: ${response.status} ${response.statusText}")
        else
            `(error, json) = await(response.json())
    }
    if !error
	list(false, new(json))
    else
        list(error)
};;

Note: NaaN doesn't need await, but our fetch is calling the inbuilt JavaScript fetch function, and it returns a promise that must be await-ed.

It's easiest to code for an API by looking at the actual output:

Play-lingo> fetch("https://api.coincap.io/v2/assets?ids=bitcoin")
$: (false, {
    data     : [
        {
            id               : "bitcoin",
            rank             : "1",
            symbol           : "BTC",
            name             : "Bitcoin",
            supply           : "19699693.0000000000000000",
            maxSupply        : "21000000.0000000000000000",
            marketCapUsd     : "1319619538604.7660844370540378",
            volumeUsd24Hr    : "6005566323.8827252766283060",
            priceUsd         : "66986.8072870356956546",
            changePercent24Hr: "0.8681538561676897",
            vwap24Hr         : "66912.2634252608629931",
            explorer         : "https://blockchain.info/" }],
    timestamp: 1716037392597 })
Play-lingo> 

Note: A standard NaaN convention is to return both error and data values from functions, similar to golang. Multiple return values are structured as a LISP tuple. The first return value is the error, which is false on success. In the REPL transcript above, the line starting with $: (false, { indicates that the fetch call was successful.

The price in USD is a string, so ignoring errors, our ticker can return the value with a simple dereference:

Play-lingo> fetch("https://api.coincap.io/v2/assets?ids=bitcoin").1.data.0.priceUsd
$: "67023.6401055720426276"
Play-lingo> 

We've created a Bitcoin ticker, but this relies on a single data source. What if it is down? A more robust implementation would use several. Let's make a ticker that processes multiple data sources.

Multiple data sources

Play-lingo> fetch("https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin")
$: (false, [
    {
        id                  : "bitcoin",
        symbol              : "btc",
        name                : "Bitcoin",
        image               : "https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1696501400",
        current_price       : 67064,
        market_cap          : 1321475329138,
        market_cap_rank     : 1,
        fully_diluted_valuation: 1408681787603,
        total_volume        : 21346139784,
        [...]
        atl_date            : "2013-07-06T00:00:00.000Z",
        roi                 : null,
        last_updated        : "2024-05-18T13:33:58.447Z" }])

CoinGecko provides a quote, but the data schema is a bit different than CoinCap. To accommodate multiple data sources it's best to define an extensible table with information about each one.

quoters=[
    {
        name: "CoinCap"
        url: "https://api.coincap.io/v2/assets?ids=bitcoin"
        extract: function(json) { tofloat(json.data.0.priceUsd) } },
    {
        name: "CoinGecko"
        url: "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin"
        extract: function(json) { tofloat(json.0.current_price) } },
    {
        name: "CoinLore"
        url: "https://api.coinlore.net/api/tickers/?limit=1"
        extract: function(json) { json.data.0.symbol=="BTC" && tofloat(json.data.0.price_usd) } },
    {
        name: "Gemini"
        url: "https://api.gemini.com/v2/ticker/btcusd"
        extract: function(json) { (tofloat(json.bid) + tofloat(json.ask)) / 2 } } ]

In the table above, each data source has an entry with the name, the fetch URL, and an extract function that is executed on the retrieved data to obtain the USD quote.

The following function calls each data source in turn, formats the result, and then reports the overall range of quote values.

// run
// Run the Bitcoin quoter, reporting available values.
//
function run(local item, range) {

    // runOne
    // Run a quotation from one data source, returning when it's complete.
    //
    closure runOne(item, local error, btcq, usd) {
        `(error, btcq) = fetch(item.url)
        print("${item.name.concat(space.repeat(10)).slice(0,15)}: ")
        if btcq {
            usd = item.extract(btcq)
            printline("${prepad(usd.toFixed(2), 10)} usd/btc")
            if !range.0 || range.0>usd
                range.0 = usd
            if !range.1 || range.1<usd
                range.1 = usd }
        else
            printline("  ${ErrorString(error)}")
    }

    // output the results
    range = []
    for item in quoters
        runOne(item)

    printline(strcat("BTC quoted price range: ", range.0, " to ", range.1, "USD"))
    range
}

Here are the results of executing our new run function:

Play-lingo> run()
CoinCap        :   66989.29 usd/btc
CoinGecko      :   66927.00 usd/btc
CoinLore       :   66982.01 usd/btc
Gemini         :   66903.88 usd/btc
range: 66903.88 to 66989.28812178948
$: [66903.88, 66989.28812178948]
Play-lingo> 

Nice! We now a have a resilient, if very basic BTC ticker.

Testing error handling

It's a truism that untested code doesn't work. Let's try simulating an error on one of the data sources by inserting a couple lines at the beginning of our fetch:

closure fetch(url, ...
    if url.startsWith("https://api.coinlore")
        return (list(Error("failure test")))
    ...

And now the test:

Play-lingo> run()
CoinCap        :   66896.35 usd/btc
CoinGecko      :   66951.00 usd/btc
CoinLore       :   failure test 
Gemini         :   66835.93 usd/btc
range: 66835.925 to 66951.0
$: [66835.925, 66951.0]
Play-lingo> 

Evidently we got lucky this time, with our simulated error reported correctly.

Faster performance with parallel calls

True programmers never let success get in the way of further improvements[1]. It is much faster to call the data sources in parallel.

Note: executing concurrent operations is such a common task there is a library function for it: asyncArray(elements, maxActive, worker). This calls worker for each item in elements, up to maxActive at a time.

The new runpar ("Run Parallel") function:

// runpar
// Run the Bitcoin quoter with data sources in parallel.
//
function runpar(local result, range, results) {

    // printOne
    // Print a quotation from one data source.
    //
    function printOne(result, local error, btcq, item, usd) {
        `(error, btcq, item) = result
        print("${item.name.concat(space.repeat(10)).slice(0,15)}: ")
        if btcq {
            usd = item.extract(btcq)
            printline("${prepad(usd.toFixed(2), 10)} usd/btc")
            if !range.0 || range.0>usd
                range.0 = usd
            if !range.1 || range.1<usd
                range.1 = usd }
        else
            printline("  ${ErrorString(error)}")
    }

    // request quotes from known data sources
    range = []
    results = asyncArray(quoters, 10, closure quoteOne(item) {
       	`(error, data) = fetch(item.url)
        list(error, data, item)
    }).wait()

    // output the results
    for result in results
        printOne(result)

    printline(strcat("range: ", range.0, " to ", range.1))
    range
}

The parallel results appear similar, but they all arrive at once, in the time of the slowest responder:

Play-lingo> runpar()
CoinCap        :   67035.06 usd/btc
CoinGecko      :   66913.00 usd/btc
CoinLore       :   67011.30 usd/btc
Gemini         :   66938.53 usd/btc
range: 66913.0 to 67035.06321850642
$: [66913.0, 67035.06321850642]

Future Improvements

Programmers are often tempted to keep making improvements until time runs out. One of the risks is that they will implement what they think of in the moment and only later realize there is a better way, or a more important capability was left undone. Sometimes a better approach is to just list the future possibilities, declare completion, and move on to other tasks. Later, when it's time to work on this again, all of the possible improvements can be considered at once.

The runpar method completes only after the slowest data source returns either an error or success, which can cause substantial delays. There are several possible resolutions:

  • Run a watchdog timer during each request, and cancel after a fixed timeout period. The asyncArray worker is passed a cancel function for this purpose.
  • Another approach to cancellation is to use a JavaScript AbortController on the fetch API calls inside our fetch function.
  • Alternatively runpar can return after the first one or two data sources respond. This does not give an accurate price range, but that may not be required. This is the fastest approach.

Live Demo

Please try this out for yourself. The code above is preloaded into the terminal.

This terminal is a bit small, and has no data persistence. If you really want to explore NaaN please install the NaaN interpreter and NaanIDE that are available under the MIT license.

Summary

  • We tried out some data source URLs using a simple helper function.
  • We called an API endpoint to retrieve the BTC quote from a data source.
  • Using a table of data sources we created a ticker to call them in sequence.
  • We tested that our code reports a data source error.
  • We changed the ticker to call the data sources in parallel.
  • We recorded a list of future improvements and completed the task.

Please stay tuned for a future post describing the code above step by step, along with the philosophy underlying the NaaN design decisions.


  1. Even if they should. It's important to know when to work and when to relax. ↩︎

Richard Zulch

Richard Zulch

Richard C. Zulch is a technologist and inventor, and been a founder, developer, and CTO. He has deeply analyzed the software of over 100 startups for M&A investors, strongly informing NaaN's design.
San Francisco Bay Area, California