From d6f0db9586e4194b1c6b138cd22cb7fd95d8b479 Mon Sep 17 00:00:00 2001 From: Robert Tidball Date: Wed, 8 Jul 2026 17:48:41 +1000 Subject: [PATCH] Add FXMacroData market data source --- alphapy/data.py | 60 ++++++++++++++++++++++++++++++++++ tests/test_fxmacrodata_data.py | 51 +++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 tests/test_fxmacrodata_data.py diff --git a/alphapy/data.py b/alphapy/data.py index b2f4bcc..9e0517a 100644 --- a/alphapy/data.py +++ b/alphapy/data.py @@ -76,6 +76,8 @@ logger = logging.getLogger(__name__) +FXMACRODATA_API_ROOT = 'https://fxmacrodata.com/api/v1' + # # Function get_data @@ -724,11 +726,69 @@ def get_yahoo_data(schema, subschema, symbol, intraday_data, data_fractal, return df +# +# Function get_fxmacrodata_data +# + +def get_fxmacrodata_data(schema, subschema, symbol, intraday_data, data_fractal, + from_date, to_date, lookback_period): + r"""Get daily FX reference rates from FXMacroData. + + FXMacroData returns one official reference value per currency pair and + date. The value is copied into open, high, low, and close with zero volume + so MarketFlow can consume it through the normal OHLCV path. + + """ + + df = pd.DataFrame() + if intraday_data: + logger.info("FXMacroData supports daily reference rates, not intraday bars") + return df + + pair = symbol.upper().replace('/', '').replace('-', '').replace('_', '') + if len(pair) != 6: + logger.error("FXMacroData symbol must be formatted like EURUSD or EUR/USD") + return df + + base = pair[:3] + quote = pair[3:] + url = SSEP.join([FXMACRODATA_API_ROOT.rstrip('/'), 'forex', base, quote]) + params = { + 'start_date': from_date, + 'end_date': to_date, + 'limit': 5000, + } + api_key = os.environ.get('FXMACRODATA_API_KEY') + if api_key: + params['api_key'] = api_key + + try: + response = requests.get(url, params=params) + response.raise_for_status() + rows = response.json().get('data', []) + except Exception: + logger.info("Could not retrieve %s data with FXMacroData", symbol.upper()) + return df + + records = [] + for row in rows: + value = float(row['val']) + records.append((row['date'], value, value, value, value, 0.0)) + + if records: + df = pd.DataFrame.from_records( + records, + columns=['date', 'open', 'high', 'low', 'close', 'volume']) + + return df + + # # Data Dispatch Tables # data_dispatch_table = {'google' : get_google_data, + 'fxmacrodata' : get_fxmacrodata_data, 'iex' : get_iex_data, 'pandas' : get_pandas_data, 'quandl' : get_quandl_data, diff --git a/tests/test_fxmacrodata_data.py b/tests/test_fxmacrodata_data.py new file mode 100644 index 0000000..a64ee60 --- /dev/null +++ b/tests/test_fxmacrodata_data.py @@ -0,0 +1,51 @@ +import unittest + +from alphapy import data + + +class TestFXMacroDataData(unittest.TestCase): + + def test_get_fxmacrodata_data(self): + class MockResponse: + def raise_for_status(self): + pass + + def json(self): + return { + 'data': [ + {'date': '2026-01-02', 'val': 1.2}, + {'date': '2026-01-01', 'val': 1.1}, + ] + } + + calls = {} + + def mock_get(url, params): + calls['url'] = url + calls['params'] = params + return MockResponse() + + original_get = data.requests.get + try: + data.requests.get = mock_get + df = data.get_fxmacrodata_data( + 'fxmacrodata', + '', + 'EUR/USD', + False, + '1D', + '2026-01-01', + '2026-01-02', + 2, + ) + finally: + data.requests.get = original_get + + self.assertEqual(calls['url'], 'https://fxmacrodata.com/api/v1/forex/EUR/USD') + self.assertEqual(calls['params']['start_date'], '2026-01-01') + self.assertEqual(list(df.columns), ['date', 'open', 'high', 'low', 'close', 'volume']) + self.assertEqual(list(df['close']), [1.2, 1.1]) + + +if __name__ == '__main__': + unittest.main()