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
3 changes: 2 additions & 1 deletion alphapy/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from alphapy.frame import Frame
from alphapy.frame import frame_name
from alphapy.frame import read_frame
from alphapy.fxmacrodata import get_fxmacrodata_data
from alphapy.globals import ModelType
from alphapy.globals import Partition, datasets
from alphapy.globals import PSEP, SSEP, USEP
Expand Down Expand Up @@ -76,7 +77,6 @@

logger = logging.getLogger(__name__)


#
# Function get_data
#
Expand Down Expand Up @@ -729,6 +729,7 @@ def get_yahoo_data(schema, subschema, symbol, intraday_data, data_fractal,
#

data_dispatch_table = {'google' : get_google_data,
'fxmacrodata' : get_fxmacrodata_data,
'iex' : get_iex_data,
'pandas' : get_pandas_data,
'quandl' : get_quandl_data,
Expand Down
106 changes: 106 additions & 0 deletions alphapy/fxmacrodata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
################################################################################
#
# Package : AlphaPy
# Module : fxmacrodata
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
################################################################################

"""FXMacroData daily FX reference-rate adapter for MarketFlow."""

import logging
import os

import pandas as pd
import requests


logger = logging.getLogger(__name__)

FXMACRODATA_API_ROOT = 'https://api.fxmacrodata.com/v1'
FXMACRODATA_PAGE_SIZE = 100


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.

The parameters match AlphaPy's market-data dispatch contract. Native
reference-observation OHLC is preserved when present; otherwise the daily
reference value is copied into OHLC and volume is set to zero.

"""

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 or not pair.isalpha() or not pair.isascii():
logger.error("FXMacroData symbol must be formatted like EURUSD or EUR/USD")
return df

base = pair[:3]
quote = pair[3:]
url = '/'.join([FXMACRODATA_API_ROOT.rstrip('/'), 'forex', base, quote])
base_params = {'start_date': from_date, 'end_date': to_date}
api_key = (os.environ.get('FXMACRODATA_API_KEY') or
os.environ.get('FXMD_API_KEY'))
if api_key:
base_params['api_key'] = api_key

rows = []
offset = 0
try:
while True:
params = dict(base_params)
params.update({'limit': FXMACRODATA_PAGE_SIZE, 'offset': offset})
response = requests.get(url, params=params, timeout=30)
if not response.ok:
logger.info("FXMacroData returned HTTP %s for %s",
response.status_code, symbol.upper())
return df
payload = response.json()
page = payload.get('data', []) if isinstance(payload, dict) else []
if not isinstance(page, list):
logger.info("FXMacroData returned invalid data for %s", symbol.upper())
return df
rows.extend(row for row in page if isinstance(row, dict))
if len(page) < FXMACRODATA_PAGE_SIZE:
break
offset += FXMACRODATA_PAGE_SIZE
except (requests.RequestException, ValueError, TypeError):
logger.info("Could not retrieve %s data with FXMacroData", symbol.upper())
return df

records = []
for row in rows:
try:
value = float(row['val'])
record = (
row['date'],
float(row.get('open', value)),
float(row.get('high', value)),
float(row.get('low', value)),
float(row.get('close', value)),
0.0,
)
except (KeyError, TypeError, ValueError):
continue
records.append(record)

if records:
df = pd.DataFrame.from_records(
records,
columns=['date', 'open', 'high', 'low', 'close', 'volume'])
df.drop_duplicates(subset=['date'], keep='first', inplace=True)
df.sort_values('date', inplace=True)
df.reset_index(drop=True, inplace=True)

return df
13 changes: 13 additions & 0 deletions docs/user_guide/market_flow.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ that are shortened.
we recommend that you save the data on an ongoing basis for a
a larger backtesting window.

FXMacroData daily FX reference rates are available through the
``fxmacrodata`` schema. Use six-letter currency-pair subjects such as
``EURUSD`` (``EUR/USD`` is also accepted). FX history normally requires an
API key, read from ``FXMACRODATA_API_KEY`` or ``FXMD_API_KEY`` in the
environment. Returned official reference observations are mapped to
the standard OHLCV frame. When the API provides reference-observation OHLC,
those values are preserved; otherwise the daily reference value is used for
all four price fields and volume is zero. Intraday retrieval is not supported.

This MarketFlow adapter supports FX rates only. FXMacroData catalogue,
macroeconomic history, release calendar, forecasts, COT, commodities,
sessions, news, and seasonality surfaces are not exposed by AlphaPy.

Domain Configuration
--------------------

Expand Down
106 changes: 106 additions & 0 deletions tests/test_fxmacrodata_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import unittest

from alphapy import fxmacrodata


class TestFXMacroDataData(unittest.TestCase):

def test_get_fxmacrodata_data(self):
class MockResponse:
ok = True
status_code = 200

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, timeout):
calls['url'] = url
calls['params'] = params
calls['timeout'] = timeout
return MockResponse()

original_get = fxmacrodata.requests.get
try:
fxmacrodata.requests.get = mock_get
df = fxmacrodata.get_fxmacrodata_data(
'fxmacrodata',
'',
'EUR/USD',
False,
'1D',
'2026-01-01',
'2026-01-02',
2,
)
finally:
fxmacrodata.requests.get = original_get

self.assertEqual(calls['url'], 'https://api.fxmacrodata.com/v1/forex/EUR/USD')
self.assertEqual(calls['params']['start_date'], '2026-01-01')
self.assertEqual(calls['params']['limit'], 100)
self.assertEqual(calls['timeout'], 30)
self.assertEqual(list(df.columns), ['date', 'open', 'high', 'low', 'close', 'volume'])
self.assertEqual(list(df['close']), [1.1, 1.2])

def test_get_fxmacrodata_data_preserves_ohlc_and_paginates(self):
calls = []

class MockResponse:
ok = True
status_code = 200

def __init__(self, rows):
self.rows = rows

def json(self):
return {'data': self.rows}

first_page = [
{'date': '2026-01-02', 'val': 1.2,
'open': 1.1, 'high': 1.3, 'low': 1.0, 'close': 1.25}
] * 100
second_page = [{'date': '2026-01-01', 'val': '1.05'}]

def mock_get(url, params, timeout):
calls.append(dict(params))
rows = first_page if params['offset'] == 0 else second_page
return MockResponse(rows)

original_get = fxmacrodata.requests.get
try:
fxmacrodata.requests.get = mock_get
df = fxmacrodata.get_fxmacrodata_data(
'fxmacrodata', '', 'EURUSD', False, '1D',
'2026-01-01', '2026-01-02', 2)
finally:
fxmacrodata.requests.get = original_get

self.assertEqual([call['offset'] for call in calls], [0, 100])
self.assertEqual(list(df['date']), ['2026-01-01', '2026-01-02'])
self.assertEqual(df.iloc[1]['open'], 1.1)
self.assertEqual(df.iloc[1]['high'], 1.3)
self.assertEqual(df.iloc[1]['low'], 1.0)
self.assertEqual(df.iloc[1]['close'], 1.25)

def test_get_fxmacrodata_data_rejects_invalid_pair_without_request(self):
original_get = fxmacrodata.requests.get
try:
fxmacrodata.requests.get = lambda *args, **kwargs: self.fail('unexpected request')
df = fxmacrodata.get_fxmacrodata_data(
'fxmacrodata', '', 'EUR1USD', False, '1D',
'2026-01-01', '2026-01-02', 2)
finally:
fxmacrodata.requests.get = original_get

self.assertTrue(df.empty)


if __name__ == '__main__':
unittest.main()