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
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
exclude: 'Stock_NeurIPS2018.py'
ci:
skip: [flake8] # remove this eventually
skip: [flake8, reorder-python-imports] # remove this eventually
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
Expand All @@ -16,7 +16,7 @@ repos:
- id: reorder-python-imports
args: [--py37-plus, --add-import, "from __future__ import annotations"]
- repo: https://github.com/asottile/pyupgrade
rev: v3.20.0
rev: v3.21.2
hooks:
- id: pyupgrade
args: [--py37-plus]
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ FinRL
|[Binance](https://binance-docs.github.io/apidocs/spot/en/#public-api-definitions)| Cryptocurrency| API-specific, 1s, 1min| API-specific| Tick-level daily aggregated trades, OHLCV| Prices&Indicators|
|[CCXT](https://docs.ccxt.com/en/latest/manual.html)| Cryptocurrency| API-specific, 1min| API-specific| OHLCV| Prices&Indicators|
|[EODhistoricaldata](https://eodhistoricaldata.com/financial-apis/)| US Securities| Frequency-specific, 1min| API-specific | OHLCV | Prices&Indicators|
|[FXMacroData](https://fxmacrodata.com/)| FX spot rates and macro announcements| Daily and event-time | Account-specific | FX rates, release calendars, forecasts, official macro announcements | OHLCV-shaped FX rates and macro event features |
|[IEXCloud](https://iexcloud.io/docs/api/)| NMS US securities|1970-now, 1 day|100 per second per IP|OHLCV| Prices&Indicators|
|[JoinQuant](https://www.joinquant.com/)| CN Securities| 2005-now, 1min| 3 requests each time| OHLCV| Prices&Indicators|
|[QuantConnect](https://www.quantconnect.com/docs/v2)| US Securities| 1998-now, 1s| NA| OHLCV| Prices&Indicators|
Expand Down
7 changes: 7 additions & 0 deletions docs/source/start/quick_start.rst
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,10 @@ Run the library:
python main.py --mode=trade # if trade. Users should input your alpaca parameters in config.py

Choices for ``--mode``: start mode, train, download_data, backtest

FXMacroData can be used as a daily FX spot data source by setting
``data_source="fxmacrodata"`` and passing pairs such as ``["EURUSD"]`` as the
ticker list. It also exposes macro announcement, release-calendar, and forecast
data through the FXMacroData processor so event features can be joined into an
RL state frame. API keys can be passed in ``kwargs`` with ``api_key`` or supplied
through the ``FXMACRODATA_API_KEY`` or ``FXMD_API_KEY`` environment variables.
25 changes: 25 additions & 0 deletions docs/source/tutorial/stocktrading/1-data.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,31 @@ In FinRL's YahooDownloader, we modified the data frame to the form that convenie
end_date = '2020-01-31',
ticker_list = ['aapl']).fetch_data()

**using FXMacroData for daily FX spot data and macro events**

FXMacroData provides daily FX spot rates for currency pairs such as EUR/USD.
The downloader maps each daily rate to FinRL's OHLCV shape, with open, high,
low, and close set to the FX spot rate and volume set to 0. FXMacroData also
provides official macro announcements, release-calendar rows, and forecast
groups that can be joined into a trading state.

.. code-block:: python
from finrl.meta.preprocessor.fxmacrodatadownloader import FXMacroDataDownloader
from finrl.meta.data_processors.processor_fxmacrodata import FXMacroDataProcessor

eurusd_df_finrl = FXMacroDataDownloader(start_date = '2020-01-01',
end_date = '2020-01-31',
ticker_list = ['EURUSD']).fetch_data()

processor = FXMacroDataProcessor()
macro_df = processor.download_macro_data(currency = 'usd',
indicator_list = ['inflation',
'policy_rate'],
start_date = '2020-01-01',
end_date = '2020-01-31')
eurusd_with_macro = processor.add_macro_features(eurusd_df_finrl, macro_df,
date_column = 'date')

Data for the chosen ticker
----------------------------------------
.. code-block:: python
Expand Down
11 changes: 11 additions & 0 deletions finrl/meta/data_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import pandas as pd

from finrl.meta.data_processors.processor_alpaca import AlpacaProcessor as Alpaca
from finrl.meta.data_processors.processor_fxmacrodata import (
FXMacroDataProcessor as FXMacroData,
)
from finrl.meta.data_processors.processor_wrds import WrdsProcessor as Wrds
from finrl.meta.data_processors.processor_yahoofinance import (
YahooFinanceProcessor as YahooFinance,
Expand All @@ -28,6 +31,14 @@ def __init__(self, data_source, tech_indicator=None, vix=None, **kwargs):
elif data_source == "yahoofinance":
self.processor = YahooFinance()

elif data_source == "fxmacrodata":
self.processor = FXMacroData(
api_key=kwargs.get("API_KEY") or kwargs.get("api_key"),
base_url=kwargs.get("BASE_URL")
or kwargs.get("base_url")
or "https://api.fxmacrodata.com/v1",
)

else:
raise ValueError("Data source input is NOT supported yet.")

Expand Down
217 changes: 217 additions & 0 deletions finrl/meta/data_processors/processor_fxmacrodata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
from __future__ import annotations

from typing import List

import numpy as np
import pandas as pd
from stockstats import StockDataFrame as Sdf

from finrl.meta.preprocessor.fxmacrodatadownloader import FXMacroDataDownloader
from finrl.meta.preprocessor.fxmacrodatadownloader import FXMacroDataMacroDownloader


class FXMacroDataProcessor:
"""Provides daily FX spot data from FXMacroData for FinRL processors."""

def __init__(self, api_key=None, base_url="https://api.fxmacrodata.com/v1"):
self.api_key = api_key
self.base_url = base_url

def download_data(
self,
ticker_list: list[str],
start_date: str,
end_date: str,
time_interval: str,
) -> pd.DataFrame:
if time_interval.lower() not in {"1d", "1day"}:
raise ValueError("FXMacroDataProcessor supports daily data only.")

self.start = start_date
self.end = end_date
self.time_interval = "1d"
data_df = FXMacroDataDownloader(
start_date=start_date,
end_date=end_date,
ticker_list=ticker_list,
api_key=self.api_key,
base_url=self.base_url,
).fetch_data()
data_df["timestamp"] = pd.to_datetime(data_df["date"])
return data_df[["timestamp", "open", "high", "low", "close", "volume", "tic"]]

def download_macro_data(
self,
currency: str,
indicator_list: list[str],
start_date: str = None,
end_date: str = None,
dataset: str = "announcements",
) -> pd.DataFrame:
downloader = FXMacroDataMacroDownloader(
currency=currency,
indicator_list=indicator_list,
start_date=start_date,
end_date=end_date,
api_key=self.api_key,
base_url=self.base_url,
)
if dataset == "announcements":
return downloader.fetch_announcements()
if dataset == "calendar":
return downloader.fetch_calendar()
if dataset == "predictions":
return downloader.fetch_predictions()
raise ValueError("dataset must be announcements, calendar, or predictions")

def download_release_calendar(
self,
currency: str,
indicator_list: list[str] = None,
start_date: str = None,
end_date: str = None,
) -> pd.DataFrame:
return FXMacroDataMacroDownloader(
currency=currency,
indicator_list=indicator_list,
start_date=start_date,
end_date=end_date,
api_key=self.api_key,
base_url=self.base_url,
).fetch_calendar()

def download_predictions(
self,
currency: str,
indicator_list: list[str],
start_date: str = None,
end_date: str = None,
) -> pd.DataFrame:
return FXMacroDataMacroDownloader(
currency=currency,
indicator_list=indicator_list,
start_date=start_date,
end_date=end_date,
api_key=self.api_key,
base_url=self.base_url,
).fetch_predictions()

def add_macro_features(
self,
data: pd.DataFrame,
macro_data: pd.DataFrame,
date_column: str = "timestamp",
) -> pd.DataFrame:
if macro_data.empty:
return data.copy()

df = data.copy()
macro = macro_data.copy()
df["_fxmacrodata_date"] = pd.to_datetime(df[date_column]).dt.normalize()
macro["_fxmacrodata_date"] = pd.to_datetime(macro["date"]).dt.normalize()

for (currency, indicator), group in macro.groupby(["currency", "indicator"]):
prefix = f"macro_{currency}_{indicator}"
feature_cols = [
"value",
"actual",
"consensus",
"forecast",
"surprise",
"prediction",
"announcement_datetime",
]
selected = group[["_fxmacrodata_date"] + feature_cols].copy()
selected = selected.sort_values("_fxmacrodata_date")
selected = selected.drop_duplicates("_fxmacrodata_date", keep="last")
selected[f"{prefix}_event"] = 1.0
selected = selected.rename(
columns={col: f"{prefix}_{col}" for col in feature_cols}
)
df = df.merge(selected, on="_fxmacrodata_date", how="left")
event_col = f"{prefix}_event"
df[event_col] = df[event_col].fillna(0.0)
fill_cols = [col for col in selected.columns if col.startswith(prefix)]
fill_cols = [col for col in fill_cols if col != event_col]
if "tic" in df.columns:
df[fill_cols] = df.groupby("tic", group_keys=False)[fill_cols].ffill()
else:
df[fill_cols] = df[fill_cols].ffill()

return df.drop(columns=["_fxmacrodata_date"])

def clean_data(self, df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
df["timestamp"] = pd.to_datetime(df["timestamp"])
df = df.dropna()
df = df.sort_values(["timestamp", "tic"]).reset_index(drop=True)
return df

def add_technical_indicator(
self, data: pd.DataFrame, tech_indicator_list: list[str]
) -> pd.DataFrame:
df = data.copy()
df = df.sort_values(by=["tic", "timestamp"])
stock = Sdf.retype(df.copy())
unique_ticker = stock.tic.unique()

for indicator in tech_indicator_list:
indicator_df = pd.DataFrame()
for tic in unique_ticker:
try:
temp_indicator = stock[stock.tic == tic][indicator]
temp_indicator = pd.DataFrame(temp_indicator)
temp_indicator["tic"] = tic
temp_indicator["timestamp"] = df[df.tic == tic][
"timestamp"
].to_list()
indicator_df = pd.concat(
[indicator_df, temp_indicator], ignore_index=True
)
except Exception as exc:
print(exc)
df = df.merge(
indicator_df[["tic", "timestamp", indicator]],
on=["tic", "timestamp"],
how="left",
)
df = df.sort_values(by=["timestamp", "tic"]).reset_index(drop=True)
return df

def add_turbulence(self, df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
df["turbulence"] = 0
return df

def add_vix(self, df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
df["VIXY"] = 0
return df

def add_vixor(self, df: pd.DataFrame) -> pd.DataFrame:
return self.add_vix(df)

def df_to_array(
self, df: pd.DataFrame, tech_indicator_list: list[str], if_vix: bool
) -> list[np.ndarray]:
df = df.copy()
unique_ticker = df.tic.unique()
if_first_time = True
turbulence_array = None
for tic in unique_ticker:
if if_first_time:
price_array = df[df.tic == tic][["close"]].values
tech_array = df[df.tic == tic][tech_indicator_list].values
if if_vix:
turbulence_array = df[df.tic == tic]["VIXY"].values
else:
turbulence_array = df[df.tic == tic]["turbulence"].values
if_first_time = False
else:
price_array = np.hstack(
[price_array, df[df.tic == tic][["close"]].values]
)
tech_array = np.hstack(
[tech_array, df[df.tic == tic][tech_indicator_list].values]
)
return price_array, tech_array, turbulence_array
Loading