5 min read
HTTPS Trade Parser

This project is a hello world to constructing a HTTP request for a REST API endpoint /fapi/v1/aggTrades using C++. In this example, I used a simple synchronous implementation using HTTP with SSL following the example from Boost. For simplicity, I parsed the returned buffer as a JSON formmated string into a collection of Trade objects.

The speed measurement of the parsing algorithm per trade is determined after the construction of the Trade objects. More specifically, the speed per trade is computed by the total running time to parse divided by the number of trades in the collection. The time complexity of the parsing algorithm is as below. In summary, the time complexity scales linearly with the size of the JSON formatted string.

  • O(n) for tokenizing the string
  • O(n) for parsing the tokens into collections
  • Total time complexity O(n) + O(n) = 2O(n) = O(n)

HTTP GET Request

std::string httpGetBinanceAggregateTrades(const std::string& symbol, const int limit) {

    int useLimit = 500;
    if (limit > 0 && limit <= 1000) {
        useLimit = limit;
    }

    // TODO :: validate symbols before querying to see if it is valid
    
    const auto host = "fapi.binance.com";
    const auto port = "443";
    const auto target = std::format("/fapi/v1/aggTrades?symbol={}&limit={}", symbol, std::to_string(useLimit));
    const int version = 11; // http v1.1

    try {
        // IO Context for all I/O operations
        boost::asio::io_context ioContext;
        
        // SSL Context is required and is for holding certificates
        boost::asio::ssl::context sslContext(boost::asio::ssl::context::tlsv12_client);
        
        // Load SSL Certificates
        dw::loadRootCertificates(sslContext); // throws exception
        //sslContext.
        
        // Verify the remote server's certificate
        sslContext.set_verify_mode(boost::asio::ssl::verify_peer); // throws exception
        
        // Setup objects for performing I/O
        boost::asio::ip::tcp::resolver tcpResolver(ioContext);
        boost::asio::ssl::stream<boost::beast::tcp_stream> tcpStream(ioContext, sslContext);
        
        // Set SNI Hostname for handshake
        if (!SSL_set_tlsext_host_name(tcpStream.native_handle(), host)) {
            boost::beast::error_code errorCode{ static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category() };
            throw boost::beast::system_error{ errorCode };
        }
        
        // Look up domain name
        auto const results = tcpResolver.resolve(host, port);
        
        // Make the connection to the IP address from look up
        boost::beast::get_lowest_layer(tcpStream).connect(results); // throw exception
        
        // Perform the SSL Handshake
        tcpStream.handshake(boost::asio::ssl::stream_base::client); // throw exception
        
        // Set up an HTTP Get request message
        boost::beast::http::request<boost::beast::http::string_body> httpRequest{ boost::beast::http::verb::get, target, version };
        httpRequest.set(boost::beast::http::field::host, host);
        httpRequest.set(boost::beast::http::field::user_agent, BOOST_BEAST_VERSION_STRING);
        httpRequest.set(boost::beast::http::field::accept, "application/json");
        
        // Send the HTTP Request to the remote host
        boost::beast::http::write(tcpStream, httpRequest); // throws exception
        
        // This buffer is used for reading and must be persisted
        boost::beast::flat_buffer responseBuffer;
        
        // Declare a container to hold the response
        boost::beast::http::response<boost::beast::http::dynamic_body> httpResponse;
        
        // Receive the HTTP Response
        boost::beast::http::read(tcpStream, responseBuffer, httpResponse); // throws exception
                    
        // Gracefull close the stream
        boost::beast::error_code errorCode;
        tcpStream.shutdown(errorCode);
        
        if (errorCode != boost::asio::ssl::error::stream_truncated) {
            throw boost::beast::system_error{ errorCode };
        }

        // Write the message to a string 
        return boost::beast::buffers_to_string(httpResponse.body().data());
    }
    catch (std::exception const& e) {
        //std::cerr << "Error: " << e.what() << std::endl;
        return "";
    }		
}

Parsing

enum class NextLineState {
    TradeID,
    Price,
    Quantity,
    FirstID,
    LastID,
    Timestamp,
    IsMaker,
    Unknown
};

std::vector<AggregateTrade> parseAsTrades(const std::string& theString) {

    boost::char_separator<char> seperator("[{,:}]");
    boost::tokenizer tokens{ theString, seperator };

    std::vector<AggregateTrade> parsedTrades;

    AggregateTrade toInsert;
    NextLineState lineState = NextLineState::Unknown;
    bool tradeComplete = false;
    bool invalidSymbol = false;
    for (const auto& token : tokens) {			
        if (token.compare("\"a\"") == 0) {
            lineState = NextLineState::TradeID;
        }
        else if (token.compare("\"p\"") == 0) {
            lineState = NextLineState::Price;
        }
        else if (token.compare("\"q\"") == 0) {
            lineState = NextLineState::Quantity;
        }
        else if (token.compare("\"f\"") == 0) {
            lineState = NextLineState::FirstID;
        }
        else if (token.compare("\"l\"") == 0) {
            lineState = NextLineState::LastID;
        }
        else if (token.compare("\"T\"") == 0) {
            lineState = NextLineState::Timestamp;
        }
        else if (token.compare("\"m\"") == 0) {
            lineState = NextLineState::IsMaker;
        }
        else {
            switch (lineState) {
            case NextLineState::TradeID:
                toInsert.AggregateTradeID = std::atoll(token.c_str());
                break;
            case NextLineState::Price:
                toInsert.Price = std::atof(token.substr(1, token.size() - 2).c_str());
                break;
            case NextLineState::Quantity:
                toInsert.Quantity = std::atof(token.substr(1, token.size() - 2).c_str());
                break;
            case NextLineState::FirstID:
                toInsert.FirstTradeID = std::atoll(token.c_str());
                break;
            case NextLineState::LastID:
                toInsert.LastTradeID = std::atoll(token.c_str());
                break;
            case NextLineState::Timestamp:
                toInsert.Timestamp = std::atoll(token.c_str());
                break;
            case NextLineState::IsMaker:
                toInsert.IsBuyerMaker = (token == "false") ? false : true;
                tradeComplete = true;
                break;
            case NextLineState::Unknown:
                break;
            }
            if (tradeComplete) {
                parsedTrades.push_back(toInsert);
                tradeComplete = false;
            }
        }			
    }
    return parsedTrades;
}

Results

These results are generated on Windows with Release configuration. It is generated by running build/../tradeparser.exe "BTCUSDT" "$(NUMTRADES)". I only performed the metrics three times per number of trades. Performing more tests and averaging the results will reduce variance and produce a better expected value. It is single threaded and generated on an Intel i7-10850H CPU.

TradesParse Time (ns)Trade Parse Time (ns)Samples
522,4004,480 ± 2803
50139,6672,793 ± 4723
250561,6332,246 ± 7173
5001,055,6332,111 ± 6373
10001,642,3331,642 ± 1213

Notes for Improvements

  1. Add asynchronous methods for retrieving trades from a symbol.
  2. Understand Certificate Authority for SSL more comprehensively.
    1. Right now it loads from a file that may expire and this involves updating the certificate later.
    2. Note curl is able to return the message body without explicitly loading a SSL certificate. I am not sure how to replicate that at the moment.
      1. curl -X GET -H "Accept: application/json" "https://fapi.binance.com/fapi/v1/aggTrades?symbol=BTCUSDT&limit=3"
  3. Parse the HTML body as it is streaming rather than streamed by reading in chunks to fill the buffer.
    1. Figure out how to identify complete and incomplete objects in an incoming chunk.
      1. Note: We need to know or compute the chunk size. If we have an idea of the chunk size, then we can identify what sections of the buffer or even the chunks that contain a complete object.
    2. Figure out how to label sections of the buffer as parsed (completed objects), otherwise we will need to keep parsing the beginning of the buffer every time a chunk is received if we are not clear of the chunk size.