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 stringO(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.
| Trades | Parse Time (ns) | Trade Parse Time (ns) | Samples |
|---|---|---|---|
| 5 | 22,400 | 4,480 ± 280 | 3 |
| 50 | 139,667 | 2,793 ± 472 | 3 |
| 250 | 561,633 | 2,246 ± 717 | 3 |
| 500 | 1,055,633 | 2,111 ± 637 | 3 |
| 1000 | 1,642,333 | 1,642 ± 121 | 3 |
Notes for Improvements
- Add asynchronous methods for retrieving trades from a symbol.
- Understand Certificate Authority for SSL more comprehensively.
- Right now it loads from a file that may expire and this involves updating the certificate later.
- Note
curlis able to return the message body without explicitly loading a SSL certificate. I am not sure how to replicate that at the moment.curl -X GET -H "Accept: application/json" "https://fapi.binance.com/fapi/v1/aggTrades?symbol=BTCUSDT&limit=3"
- Parse the HTML body as it is streaming rather than streamed by reading in chunks to fill the buffer.
- Figure out how to identify complete and incomplete objects in an incoming chunk.
- 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.
- 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.
- Figure out how to identify complete and incomplete objects in an incoming chunk.