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
124 changes: 124 additions & 0 deletions findatapy/market/datavendorfxmacrodata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
__author__ = "saeedamen" # Saeed Amen

#
# Copyright 2016 Cuemacro
#
# 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
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on a "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
# See the License for the specific language governing permissions and
# limitations under the License.
#

import requests

import pandas as pd

from findatapy.market.datavendor import DataVendor
from findatapy.util import DataConstants, LoggerManager


class DataVendorFXMacroData(DataVendor):
"""Reads daily FX spot rates from FXMacroData into findatapy."""

def __init__(self):
super(DataVendorFXMacroData, self).__init__()

def load_ticker(self, md_request):
logger = LoggerManager().getLogger(__name__)

if md_request.freq != "daily":
logger.warning("FXMacroData currently supports daily FX spot rates")
return None

tickers = md_request.vendor_tickers or md_request.tickers
fields = md_request.fields or ["close"]

if tickers is None:
logger.warning("No FXMacroData tickers specified")
return None

data_frames = []

for library_ticker, vendor_ticker in zip(md_request.tickers, tickers):
frame = self._download_fx_pair(md_request, vendor_ticker, fields)

if frame is None or frame.empty:
continue

frame.columns = [
f"{library_ticker}.{field}" for field in frame.columns
]
data_frames.append(frame)

if len(data_frames) == 0:
return None

data_frame = pd.concat(data_frames, axis=1).sort_index()
data_frame.index.name = "Date"

logger.info(
f"Completed request from FXMacroData for {list(data_frame.columns)}")

return data_frame

def _download_fx_pair(self, md_request, ticker, fields):
base, quote = self._split_pair(ticker)
constants = DataConstants()
url = (
f"{constants.fxmacrodata_base_url}/forex/"
f"{base.lower()}/{quote.lower()}"
)
params = {
"start_date": self._format_date(md_request.start_date),
"end_date": self._format_date(md_request.finish_date),
}

if md_request.fxmacrodata_api_key:
params["api_key"] = md_request.fxmacrodata_api_key

response = requests.get(url, params=params, timeout=30)
response.raise_for_status()
rows = response.json().get("data", [])

if len(rows) == 0:
return None

data_frame = pd.DataFrame(rows)
data_frame["Date"] = pd.to_datetime(data_frame["date"])
data_frame = data_frame.set_index("Date").sort_index()

field_map = {}

for field in fields:
field_map[field] = (
data_frame["val"]
if field in ["open", "high", "low", "close"]
else data_frame.get(field)
)

return pd.DataFrame(field_map, index=data_frame.index)

@staticmethod
def _split_pair(ticker):
clean_ticker = ticker.replace("/", "").replace("-", "").upper()

if len(clean_ticker) != 6:
raise ValueError(
"FXMacroData tickers should be six-letter FX pairs "
"such as EURUSD"
)

return clean_ticker[:3], clean_ticker[3:]

@staticmethod
def _format_date(value):
if hasattr(value, "strftime"):
return value.strftime("%Y-%m-%d")

return str(value)
5 changes: 5 additions & 0 deletions findatapy/market/marketdatagenerator.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ def get_data_vendor(self, md_request):
from findatapy.market.datavendorweb import DataVendorFXCM
data_vendor = DataVendorFXCM()

elif data_source == "fxmacrodata":
from findatapy.market.datavendorfxmacrodata import \
DataVendorFXMacroData
data_vendor = DataVendorFXMacroData()

elif data_source == "alfred":
from findatapy.market.datavendorfred import DataVendorALFRED
data_vendor = DataVendorALFRED()
Expand Down
22 changes: 21 additions & 1 deletion findatapy/market/marketdatarequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def __init__(self, data_source: str = None,
fred_api_key: str = None,
alpha_vantage_api_key: str = None,
eikon_api_key: str = None,
fxmacrodata_api_key: str = None,
pretransformation: str = None,
vintage_as_index: bool = None,
vintage_download: dict = None,
Expand Down Expand Up @@ -152,6 +153,8 @@ def __init__(self, data_source: str = None,
alpha_vantage_api_key = data_constants.alpha_vantage_api_key
if eikon_api_key is None:
eikon_api_key = data_constants.eikon_api_key
if fxmacrodata_api_key is None:
fxmacrodata_api_key = data_constants.fxmacrodata_api_key
if data_vendor_custom is None:
data_vendor_custom = data_constants.data_vendor_custom
if arcticdb_dict is None:
Expand Down Expand Up @@ -220,6 +223,8 @@ def __init__(self, data_source: str = None,
self.alpha_vantage_api_key = \
copy.deepcopy(md_request.alpha_vantage_api_key)
self.eikon_api_key = copy.deepcopy(md_request.eikon_api_key)
self.fxmacrodata_api_key = \
copy.deepcopy(md_request.fxmacrodata_api_key)

self.pretransformation = copy.deepcopy(md_request.pretransformation)
self.vintage_as_index = copy.deepcopy(md_request.vintage_as_index)
Expand Down Expand Up @@ -278,6 +283,7 @@ def __init__(self, data_source: str = None,
self.fred_api_key = fred_api_key
self.alpha_vantage_api_key = alpha_vantage_api_key
self.eikon_api_key = eikon_api_key
self.fxmacrodata_api_key = fxmacrodata_api_key

self.pretransformation = pretransformation
self.vintage_as_index = vintage_as_index
Expand Down Expand Up @@ -354,7 +360,7 @@ def data_source(self, data_source):
try:
valid_data_source = ["ats", "bloomberg", "dukascopy", "fred",
"gain", "google", "quandl", "yahoo",
"boe", "eikon"]
"boe", "eikon", "fxmacrodata"]

if not data_source in valid_data_source:
LoggerManager().getLogger(__name__).warning(
Expand Down Expand Up @@ -664,6 +670,12 @@ def date_parser(self, date):
except:
# logger.warning("Attempted to parse date")
pass

try:
date1 = datetime.datetime.strptime(date, "%Y-%m-%d")
except:
# logger.warning("Attempted to parse date")
pass
else:
import pandas

Expand Down Expand Up @@ -831,6 +843,14 @@ def eikon_api_key(self):
def eikon_api_key(self, eikon_api_key):
self.__eikon_api_key = eikon_api_key

@property
def fxmacrodata_api_key(self):
return self.__fxmacrodata_api_key

@fxmacrodata_api_key.setter
def fxmacrodata_api_key(self, fxmacrodata_api_key):
self.__fxmacrodata_api_key = fxmacrodata_api_key

@property
def pretransformation(self):
return self.__pretransformation
Expand Down
7 changes: 6 additions & 1 deletion findatapy/util/dataconstants.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ class DataConstants(object):
'yahoo' : 1, # yfinance already threads requests, so don't do it twice!
'other' : 4,
'dukascopy' : 3, # do not do too many!
'fxcm' : 4}
'fxcm' : 4,
'fxmacrodata' : 1}

# Seconds for timeout
timeout_downloader = {'dukascopy' : 120}
Expand Down Expand Up @@ -275,6 +276,10 @@ class DataConstants(object):
####### FXCM API (contact FXCM to get this)
fxcm_api_key = "x"

####### FXMacroData settings
fxmacrodata_base_url = "https://fxmacrodata.com/api/v1"
fxmacrodata_api_key = key_store("FXMacroData")

####### Eikon settings
eikon_api_key = key_store("Eikon")

Expand Down
55 changes: 55 additions & 0 deletions tests/test_fxmacrodata_vendor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import pandas as pd

from findatapy.market.datavendorfxmacrodata import DataVendorFXMacroData
from findatapy.market.marketdatarequest import MarketDataRequest


class _FakeResponse:
def __init__(self, payload):
self._payload = payload

def raise_for_status(self):
pass

def json(self):
return self._payload


def test_fxmacrodata_vendor_builds_daily_fx_dataframe(monkeypatch):
calls = {}

def fake_get(url, params=None, timeout=None):
calls["url"] = url
calls["params"] = params
calls["timeout"] = timeout

return _FakeResponse({
"data": [
{"date": "2026-01-02", "val": 1.10},
{"date": "2026-01-03", "val": 1.11},
]
})

monkeypatch.setattr(
"findatapy.market.datavendorfxmacrodata.requests.get", fake_get)

md_request = MarketDataRequest(
data_source="fxmacrodata",
category="fx",
start_date="2026-01-01",
finish_date="2026-01-04",
tickers=["EURUSD"],
fields=["open", "high", "low", "close"],
fxmacrodata_api_key="test-key",
)

data_frame = DataVendorFXMacroData().load_ticker(md_request)

assert isinstance(data_frame, pd.DataFrame)
assert list(data_frame.columns) == [
"EURUSD.open", "EURUSD.high", "EURUSD.low", "EURUSD.close"]
assert data_frame.loc[pd.Timestamp("2026-01-02"), "EURUSD.close"] == 1.10
assert calls["url"] == "https://fxmacrodata.com/api/v1/forex/eur/usd"
assert calls["params"]["api_key"] == "test-key"
assert calls["params"]["start_date"] == "2026-01-01"
assert calls["params"]["end_date"] == "2026-01-04"