Skip to content
Draft
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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ add_library(pineforge STATIC
src/ta_volatility_trend.cpp
src/timeframe.cpp
src/timezone.cpp
src/fxmacrodata.cpp
)

# Canonical alias for downstream find_package(PineForge) consumers.
Expand Down
77 changes: 77 additions & 0 deletions include/pineforge/fxmacrodata.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#pragma once

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

namespace pineforge {

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 api_key = {},
std::string base_url = "https://api.fxmacrodata.com/v1",
Transport transport = {});

std::string data_catalogue(const std::string& currency,
QueryParams params = {}) const;
std::string announcements(const std::string& currency,
const std::string& indicator,
QueryParams params = {}) const;
std::string latest_announcements(const std::string& currency,
QueryParams params = {}) const;
std::string announcement_changes(QueryParams params = {}) const;
std::string calendar(const std::string& currency,
QueryParams params = {}) const;
std::string predictions(const std::string& currency,
const std::string& indicator,
QueryParams params = {}) const;
std::string forex(const std::string& base,
const std::string& quote,
QueryParams params = {}) const;
std::string cot(const std::string& currency, QueryParams params = {}) const;
std::string commodity(const std::string& indicator,
QueryParams params = {}) const;
std::string commodities_latest(QueryParams params = {}) const;
std::string curves(const std::string& currency, QueryParams params = {}) const;
std::string curve_proxies(const std::string& currency,
QueryParams params = {}) const;
std::string forward_curves(const std::string& currency,
QueryParams params = {}) const;
std::string rate_differentials(const std::string& base,
const std::string& quote,
QueryParams params = {}) const;
std::string forward_differentials(const std::string& base,
const std::string& quote,
QueryParams params = {}) const;
std::string market_sessions(QueryParams params = {}) const;
std::string risk_sentiment(QueryParams params = {}) const;
std::string news(const std::string& currency, QueryParams params = {}) const;
std::string press_releases(const std::string& currency,
QueryParams params = {}) const;
std::string graphql(const std::string& query,
const std::string& variables_json = "{}") const;
std::string request(const std::string& path,
QueryParams params = {},
std::string method = "GET",
std::string body = {}) const;

std::string build_url(const std::string& path, QueryParams params = {}) const;

private:
std::string api_key_;
std::string base_url_;
Transport transport_;

std::string send(const std::string& method,
const std::string& path,
QueryParams params,
const std::string& body = {}) const;
};

} // namespace pineforge
194 changes: 194 additions & 0 deletions src/fxmacrodata.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
#include <pineforge/fxmacrodata.hpp>

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

namespace pineforge {
namespace {

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

std::string ensure_leading_slash(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();
}

} // namespace

FxMacroDataClient::FxMacroDataClient(std::string api_key,
std::string base_url,
Transport transport)
: api_key_(std::move(api_key)),
base_url_(trim_trailing_slash(std::move(base_url))),
transport_(std::move(transport)) {}

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

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

std::string FxMacroDataClient::send(const std::string& method,
const std::string& path,
QueryParams params,
const std::string& body) const {
if (!transport_) {
throw std::runtime_error("FxMacroDataClient requires a transport callback");
}
return transport_(method, build_url(path, std::move(params)), body);
}

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

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

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

std::string FxMacroDataClient::announcement_changes(QueryParams params) const {
return send("GET", "/announcements/changes", std::move(params));
}

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

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

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

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

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

std::string FxMacroDataClient::commodities_latest(QueryParams params) const {
return send("GET", "/commodities/latest", std::move(params));
}

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

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

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

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

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

std::string FxMacroDataClient::market_sessions(QueryParams params) const {
return send("GET", "/market_sessions", std::move(params));
}

std::string FxMacroDataClient::risk_sentiment(QueryParams params) const {
return send("GET", "/risk_sentiment", std::move(params));
}

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

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

std::string FxMacroDataClient::graphql(const std::string& query,
const std::string& variables_json) const {
return send("POST", "/graphql", {}, "{\"query\":\"" + query + "\",\"variables\":" + variables_json + "}");
}

std::string FxMacroDataClient::request(const std::string& path,
QueryParams params,
std::string method,
std::string body) const {
return send(std::move(method), path, std::move(params), body);
}

} // namespace pineforge
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ set(TEST_SOURCES
test_metrics
test_drawing
test_margin_call
test_fxmacrodata
)

find_package(Threads REQUIRED)
Expand Down
45 changes: 45 additions & 0 deletions tests/test_fxmacrodata.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#include <pineforge/fxmacrodata.hpp>

#include <stdexcept>
#include <string>

static void require(bool condition, const std::string& message) {
if (!condition) {
throw std::runtime_error(message);
}
}

int main() {
std::string method;
std::string url;
std::string body;

pineforge::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 std::string{"{\"ok\":true}"};
});

require(client.calendar("USD", {{"days_ahead", "30"}}) == "{\"ok\":true}",
"transport result should be returned");
require(method == "GET", "calendar should use GET");
require(url == "https://api.fxmacrodata.com/v1/calendar/usd?days_ahead=30&api_key=test-key",
"calendar URL should include normalized currency and auth");

client.rate_differentials("EUR", "USD", {{"tenor", "2y"}});
require(url == "https://api.fxmacrodata.com/v1/rate_differentials/eur/usd?tenor=2y&api_key=test-key",
"rate differential endpoint should include both currencies");

client.graphql("query { marketSessions { name } }");
require(method == "POST", "GraphQL should use POST");
require(url == "https://api.fxmacrodata.com/v1/graphql?api_key=test-key",
"GraphQL URL should include auth");
require(body.find("marketSessions") != std::string::npos,
"GraphQL query should be placed in the request body");

return 0;
}