Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 244 additions & 0 deletions source/shared/utilities/fxMacroDataClient.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
// Backtesting Engine in C++
//
// (c) 2026 Ryan McCaffery | https://mccaffers.com
// This code is licensed under MIT license (see LICENSE.txt for details)
// ---------------------------------------

#include "shared/utilities/fxMacroDataClient.hpp"

#include "shared/utilities/env.hpp"

#include <curl/curl.h>

#include <cctype>
#include <iomanip>
#include <sstream>
#include <stdexcept>
#include <utility>

namespace {

std::string trimTrailingSlash(std::string value) {
while (!value.empty() && value.back() == '/') {
value.pop_back();
}
return value;
}

std::string ensureLeadingSlash(const std::string& path) {
if (!path.empty() && path.front() == '/') {
return path;
}
return "/" + path;
}

std::string lower(std::string value) {
for (char& ch : value) {
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
}
return value;
}

std::string encode(std::string value) {
std::ostringstream out;
out << std::uppercase << std::hex;
for (unsigned char ch : value) {
if (std::isalnum(ch) || ch == '-' || ch == '_' || ch == '.' || ch == '~') {
out << static_cast<char>(ch);
} else {
out << '%' << std::setw(2) << std::setfill('0') << static_cast<int>(ch);
}
}
return out.str();
}

size_t writeCallback(char* ptr, size_t size, size_t nmemb, void* userdata) {
auto* out = static_cast<std::string*>(userdata);
out->append(ptr, size * nmemb);
return size * nmemb;
}

std::string performHttp(const std::string& method,
const std::string& url,
const std::string& body) {
CURL* curl = curl_easy_init();
if (!curl) {
throw std::runtime_error("Failed to initialize CURL");
}

std::string response;
curl_slist* headers = nullptr;
headers = curl_slist_append(headers, "Accept: application/json");

curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_USERAGENT, "backtesting-engine-cpp-fxmacrodata/1.0");
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);

if (method == "POST") {
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
}
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);

const CURLcode rc = curl_easy_perform(curl);
long status = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);

if (rc != CURLE_OK) {
throw std::runtime_error(std::string("FXMacroData request failed: ") + curl_easy_strerror(rc));
}
if (status < 200 || status >= 300) {
throw std::runtime_error("FXMacroData request returned HTTP " + std::to_string(status));
}
return response;
}

} // namespace

FxMacroDataClient::FxMacroDataClient(std::string apiKey,
std::string baseUrl,
Transport transport)
: apiKey_(apiKey.empty()
? env::getOr("FXMACRODATA_API_KEY", env::getOr("FXMD_API_KEY", ""))
: std::move(apiKey)),
baseUrl_(trimTrailingSlash(std::move(baseUrl))),
transport_(std::move(transport)) {}

std::string FxMacroDataClient::buildUrl(const std::string& path, QueryParams params) const {
if (!apiKey_.empty()) {
params.emplace_back("api_key", apiKey_);
}

std::ostringstream url;
url << baseUrl_ << ensureLeadingSlash(path);
char separator = '?';
for (const auto& [key, value] : params) {
url << separator << encode(key) << '=' << encode(value);
separator = '&';
}
return url.str();
}

nlohmann::json FxMacroDataClient::requestJson(const std::string& method,
const std::string& path,
QueryParams params,
const nlohmann::json& body) const {
const std::string bodyText = method == "POST" ? body.dump() : std::string{};
const std::string text = transport_
? transport_(method, buildUrl(path, std::move(params)), bodyText)
: performHttp(method, buildUrl(path, std::move(params)), bodyText);
return nlohmann::json::parse(text);
}

nlohmann::json FxMacroDataClient::dataCatalogue(const std::string& currency,
QueryParams params) const {
return requestJson("GET", "/data_catalogue/" + encode(lower(currency)), std::move(params));
}

nlohmann::json FxMacroDataClient::announcements(const std::string& currency,
const std::string& indicator,
QueryParams params) const {
return requestJson("GET", "/announcements/" + encode(lower(currency)) + "/" + encode(indicator), std::move(params));
}

nlohmann::json FxMacroDataClient::latestAnnouncements(const std::string& currency,
QueryParams params) const {
return requestJson("GET", "/announcements/" + encode(lower(currency)) + "/latest", std::move(params));
}

nlohmann::json FxMacroDataClient::announcementChanges(QueryParams params) const {
return requestJson("GET", "/announcements/changes", std::move(params));
}

nlohmann::json FxMacroDataClient::calendar(const std::string& currency,
QueryParams params) const {
return requestJson("GET", "/calendar/" + encode(lower(currency)), std::move(params));
}

nlohmann::json FxMacroDataClient::predictions(const std::string& currency,
const std::string& indicator,
QueryParams params) const {
return requestJson("GET", "/predictions/" + encode(lower(currency)) + "/" + encode(indicator), std::move(params));
}

nlohmann::json FxMacroDataClient::forex(const std::string& base,
const std::string& quote,
QueryParams params) const {
return requestJson("GET", "/forex/" + encode(lower(base)) + "/" + encode(lower(quote)), std::move(params));
}

nlohmann::json FxMacroDataClient::cot(const std::string& currency,
QueryParams params) const {
return requestJson("GET", "/cot/" + encode(lower(currency)), std::move(params));
}

nlohmann::json FxMacroDataClient::commodity(const std::string& indicator,
QueryParams params) const {
return requestJson("GET", "/commodities/" + encode(indicator), std::move(params));
}

nlohmann::json FxMacroDataClient::commoditiesLatest(QueryParams params) const {
return requestJson("GET", "/commodities/latest", std::move(params));
}

nlohmann::json FxMacroDataClient::curves(const std::string& currency,
QueryParams params) const {
return requestJson("GET", "/curves/" + encode(lower(currency)), std::move(params));
}

nlohmann::json FxMacroDataClient::curveProxies(const std::string& currency,
QueryParams params) const {
return requestJson("GET", "/curve_proxies/" + encode(lower(currency)), std::move(params));
}

nlohmann::json FxMacroDataClient::forwardCurves(const std::string& currency,
QueryParams params) const {
return requestJson("GET", "/forward_curves/" + encode(lower(currency)), std::move(params));
}

nlohmann::json FxMacroDataClient::rateDifferentials(const std::string& base,
const std::string& quote,
QueryParams params) const {
return requestJson("GET", "/rate_differentials/" + encode(lower(base)) + "/" + encode(lower(quote)), std::move(params));
}

nlohmann::json FxMacroDataClient::forwardDifferentials(const std::string& base,
const std::string& quote,
QueryParams params) const {
return requestJson("GET", "/forward_differentials/" + encode(lower(base)) + "/" + encode(lower(quote)), std::move(params));
}

nlohmann::json FxMacroDataClient::marketSessions(QueryParams params) const {
return requestJson("GET", "/market_sessions", std::move(params));
}

nlohmann::json FxMacroDataClient::riskSentiment(QueryParams params) const {
return requestJson("GET", "/risk_sentiment", std::move(params));
}

nlohmann::json FxMacroDataClient::news(const std::string& currency,
QueryParams params) const {
return requestJson("GET", "/news/" + encode(lower(currency)), std::move(params));
}

nlohmann::json FxMacroDataClient::pressReleases(const std::string& currency,
QueryParams params) const {
return requestJson("GET", "/press-releases/" + encode(lower(currency)), std::move(params));
}

nlohmann::json FxMacroDataClient::graphql(const std::string& query,
nlohmann::json variables) const {
return requestJson("POST", "/graphql", {}, {{"query", query}, {"variables", std::move(variables)}});
}

nlohmann::json FxMacroDataClient::request(const std::string& path,
QueryParams params,
std::string method,
nlohmann::json body) const {
return requestJson(std::move(method), path, std::move(params), body);
}
85 changes: 85 additions & 0 deletions source/shared/utilities/fxMacroDataClient.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Backtesting Engine in C++
//
// (c) 2026 Ryan McCaffery | https://mccaffers.com
// This code is licensed under MIT license (see LICENSE.txt for details)
// ---------------------------------------

#pragma once

#include <functional>
#include <string>
#include <utility>
#include <vector>

#include <nlohmann/json.hpp>

class FxMacroDataClient {
public:
using QueryParams = std::vector<std::pair<std::string, std::string>>;
using Transport = std::function<std::string(const std::string& method,
const std::string& url,
const std::string& body)>;

explicit FxMacroDataClient(
std::string apiKey = {},
std::string baseUrl = "https://api.fxmacrodata.com/v1",
Transport transport = {});

[[nodiscard]] nlohmann::json dataCatalogue(const std::string& currency,
QueryParams params = {}) const;
[[nodiscard]] nlohmann::json announcements(const std::string& currency,
const std::string& indicator,
QueryParams params = {}) const;
[[nodiscard]] nlohmann::json latestAnnouncements(const std::string& currency,
QueryParams params = {}) const;
[[nodiscard]] nlohmann::json announcementChanges(QueryParams params = {}) const;
[[nodiscard]] nlohmann::json calendar(const std::string& currency,
QueryParams params = {}) const;
[[nodiscard]] nlohmann::json predictions(const std::string& currency,
const std::string& indicator,
QueryParams params = {}) const;
[[nodiscard]] nlohmann::json forex(const std::string& base,
const std::string& quote,
QueryParams params = {}) const;
[[nodiscard]] nlohmann::json cot(const std::string& currency,
QueryParams params = {}) const;
[[nodiscard]] nlohmann::json commodity(const std::string& indicator,
QueryParams params = {}) const;
[[nodiscard]] nlohmann::json commoditiesLatest(QueryParams params = {}) const;
[[nodiscard]] nlohmann::json curves(const std::string& currency,
QueryParams params = {}) const;
[[nodiscard]] nlohmann::json curveProxies(const std::string& currency,
QueryParams params = {}) const;
[[nodiscard]] nlohmann::json forwardCurves(const std::string& currency,
QueryParams params = {}) const;
[[nodiscard]] nlohmann::json rateDifferentials(const std::string& base,
const std::string& quote,
QueryParams params = {}) const;
[[nodiscard]] nlohmann::json forwardDifferentials(const std::string& base,
const std::string& quote,
QueryParams params = {}) const;
[[nodiscard]] nlohmann::json marketSessions(QueryParams params = {}) const;
[[nodiscard]] nlohmann::json riskSentiment(QueryParams params = {}) const;
[[nodiscard]] nlohmann::json news(const std::string& currency,
QueryParams params = {}) const;
[[nodiscard]] nlohmann::json pressReleases(const std::string& currency,
QueryParams params = {}) const;
[[nodiscard]] nlohmann::json graphql(const std::string& query,
nlohmann::json variables = nlohmann::json::object()) const;
[[nodiscard]] nlohmann::json request(const std::string& path,
QueryParams params = {},
std::string method = "GET",
nlohmann::json body = nlohmann::json::object()) const;

[[nodiscard]] std::string buildUrl(const std::string& path, QueryParams params = {}) const;

private:
std::string apiKey_;
std::string baseUrl_;
Transport transport_;

[[nodiscard]] nlohmann::json requestJson(const std::string& method,
const std::string& path,
QueryParams params,
const nlohmann::json& body = nlohmann::json::object()) const;
};
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ add_executable(unit_tests
sweep.cpp
tradeManager.cpp
tickPacket.cpp
fxMacroDataClient.cpp
)

target_link_libraries(unit_tests PRIVATE
Expand Down
34 changes: 34 additions & 0 deletions tests/fxMacroDataClient.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#include <catch2/catch_test_macros.hpp>

#include <string>

#include "shared/utilities/fxMacroDataClient.hpp"

TEST_CASE("FxMacroDataClient builds authenticated macro endpoint requests", "[fxmacrodata]") {
std::string method;
std::string url;
std::string body;

FxMacroDataClient client{
"test-key",
"https://api.fxmacrodata.com/v1/",
[&](const std::string& m, const std::string& u, const std::string& b) {
method = m;
url = u;
body = b;
return R"({"ok":true})";
}};

const auto result = client.predictions("USD", "non_farm_payrolls", {{"limit", "1"}});
CHECK(result.at("ok").get<bool>());
CHECK(method == "GET");
CHECK(url == "https://api.fxmacrodata.com/v1/predictions/usd/non_farm_payrolls?limit=1&api_key=test-key");

client.rateDifferentials("EUR", "USD", {{"tenor", "2y"}});
CHECK(url == "https://api.fxmacrodata.com/v1/rate_differentials/eur/usd?tenor=2y&api_key=test-key");

client.graphql("query { marketSessions { name } }");
CHECK(method == "POST");
CHECK(url == "https://api.fxmacrodata.com/v1/graphql?api_key=test-key");
CHECK(body.find("marketSessions") != std::string::npos);
}
Loading