DEV Community

Cover image for 🐍 Building an FX Trading Edge: Creating a Python Client for the FXMacroData API
Robert Tidball for FXMacroData

Posted on • Originally published at fxmacrodata.com

🐍 Building an FX Trading Edge: Creating a Python Client for the FXMacroData API

When building a Python library, the goal is to turn a complex, boilerplate-heavy process (raw API calls) into a simple, elegant one-liner. The FXMacroData API provides real-time macroeconomic indicators for major currency pairsβ€”a goldmine for quant traders and analysts.

Raw API calls force developers to repeat code for authentication, error checking, and URL construction. Embracing the DRY (Don't Repeat Yourself) principle, I set out to build a dedicated Python library on top of it, creating a user-friendly wrapper. This article walks you through the core components of that wrapper, covering synchronous and asynchronous clients, proper exception handling, and utility functions.


1. Project Scaffolding and Handling Authentication

A good library starts with an intuitive entry point. My goal was to turn an HTTP request into a clean Python method call like:

client.get("aud", "inflation")
Enter fullscreen mode Exit fullscreen mode

The Client Constructor

The Client class holds the base URL and API key. The FXMacroData API has a unique feature: USD data is public, but other currencies require an API key. The constructor handles this requirement upfront.

# client.py or async_client.py
from typing import Optional
from .exceptions import FXMacroDataError

class Client:
    BASE_URL = "https://fxmacrodata.com/api"

    def __init__(self, api_key: Optional[str] = None):
        """
        Synchronous FXMacroData Client.
        api_key: Required for non-USD currencies. USD is public.
        """
        self.api_key = api_key
Enter fullscreen mode Exit fullscreen mode

2. Core Logic: The Synchronous Client (Client)

The synchronous Client uses the popular requests library. The main logic resides in the get method, which dynamically constructs the URL and enforces the API key requirement.

The get Method: Dynamic URL Construction and Key Check

# client.py
def get(
    self,
    currency: str,
    indicator: str,
    start_date: Optional[str] = None,
    end_date: Optional[str] = None,
) -> dict:
    currency = currency.lower()
    url = f"{self.BASE_URL}/{currency}/{indicator}"

    headers = {}
    if currency != "usd":
        if not self.api_key:
            # Custom exception is crucial for user-friendly errors
            raise FXMacroDataError(f"API key required for {currency.upper()} endpoints.")
        headers["X-API-Key"] = self.api_key

    params = {}
    # ... params and API call logic ...
Enter fullscreen mode Exit fullscreen mode

Robust Error Handling with Custom Exceptions

A robust library must handle failures gracefully. I created a custom exception, FXMacroDataError, to catch network issues and non-200 status codes, returning a clear, actionable message.

# exceptions.py
class FXMacroDataError(Exception):
    """Custom exception for FXMacroData client errors."""
    pass
Enter fullscreen mode Exit fullscreen mode

Core request logic with the error wrapper:

# client.py (continued)
try:
    response = requests.get(url, headers=headers, params=params)
except Exception as e:
    raise FXMacroDataError(f"Request failed: {e}")

if response.status_code != 200:
    # Raise a clear error if the API returns a problem
    raise FXMacroDataError(f"API Error ({response.status_code}): {response.text}")

return response.json()
Enter fullscreen mode Exit fullscreen mode

3. Advanced Feature: The Asynchronous Client (AsyncClient)

For automated trading bots or high-traffic dashboards, asynchronous programming is essential for performance. The AsyncClient uses the aiohttp library for non-blocking I/O.

Asynchronous Session Management

I implemented async context managers (__aenter__ and __aexit__) to ensure the aiohttp.ClientSession is created and properly closed, preventing resource leaks.

# async_client.py
import aiohttp
# ... imports ...

class AsyncClient:
    # ... init ...

    async def __aenter__(self) -> "AsyncClient":
        self.session = aiohttp.ClientSession()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        if self.session:
            await self.session.close()
            self.session = None
Enter fullscreen mode Exit fullscreen mode

This allows concurrent execution, where the total time is the maximum delay, not the sum:

import asyncio
from fxmacrodata import AsyncClient

async def main():
    async with AsyncClient(api_key="YOUR_KEY") as client:
        # Concurrent calls are now trivial
        data_aud = client.get("aud", "inflation")
        data_eur = client.get("eur", "gdp")

        aud_data, eur_data = await asyncio.gather(data_aud, data_eur)
        # ...
Enter fullscreen mode Exit fullscreen mode

4. Utility: Cleaning Up the Data

Data consumers expect chronologically sorted data, but APIs don't always guarantee it. A small utility function ensures the output is always clean time-series data, checking for either a 'date' or 'release_date' key.

# utils.py
def sort_by_date(data_list):
    """Sorts a list of indicator data dictionaries by 'date' or 'release_date'."""
    return sorted(data_list, key=lambda x: x.get('date') or x.get('release_date'))
Enter fullscreen mode Exit fullscreen mode

Building this wrapper solidified my understanding of Object-Oriented Design, the crucial performance trade-offs between Synchronous vs. Asynchronous networking, and the importance of a great Developer Experience through custom exceptions. You can explore the full source code on GitHub.

The final step was packaging the library and publishing it to PyPI. If you're building a tool to integrate real-time FX macro data, or just want a pattern for creating your own wrapper, the structure of this library is a solid foundation. Happy coding!

β€” Rob @ FXMacroData

Top comments (0)