Skip to content

Commit ca4054c

Browse files
authored
ENH: Oanda currency historical rates (#224)
Closes #222
1 parent 3f5eb14 commit ca4054c

File tree

5 files changed

+167
-2
lines changed

5 files changed

+167
-2
lines changed

docs/source/remote_data.rst

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Currently the following sources are supported:
3131
- :ref:`OECD<remote_data.oecd>`
3232
- :ref:`Eurostat<remote_data.eurostat>`
3333
- :ref:`Thrift Savings Plan<remote_data.tsp>`
34+
- :ref:`Oanda currency historical rate<remote_data.oanda_curr_hist>`
3435

3536
It should be noted, that various sources support different kinds of data, so not all sources implement the same methods and the data elements returned might also differ.
3637

@@ -476,6 +477,7 @@ reconnect after waiting a few minutes.
476477
.. _remote_data.tsp:
477478
478479
TSP Fund Data
480+
=============
479481
480482
Download mutual fund index prices for the TSP.
481483
@@ -485,3 +487,39 @@ Download mutual fund index prices for the TSP.
485487
tspreader = tsp.TSPReader(start='2015-10-1', end='2015-12-31')
486488
tspreader.read()
487489
490+
491+
.. _remote_data.oanda_curr_hist
492+
493+
Oanda currency historical rate
494+
==============================
495+
496+
Download currency historical rate from `Oanda <https://www.oanda.com/>`__.
497+
498+
.. code-block:: python
499+
500+
In [1]: from pandas_datareader.oanda import get_oanda_currency_historical_rates
501+
In [2]: start, end = "2016-01-01", "2016-06-01"
502+
In [3]: quote_currency = "USD"
503+
In [4]: base_currency = ["EUR", "GBP", "JPY"]
504+
In [5]: df_rates = get_oanda_currency_historical_rates(
505+
start, end,
506+
quote_currency=quote_currency,
507+
base_currency=base_currency
508+
)
509+
In [6]: print(df_rates)
510+
511+
EUR/USD GBP/USD JPY/USD
512+
Date
513+
2016-01-01 1.087090 1.473989 0.008320
514+
2016-01-02 1.087090 1.473989 0.008320
515+
2016-01-03 1.087090 1.473989 0.008320
516+
2016-01-04 1.086730 1.473481 0.008370
517+
2016-01-05 1.078760 1.469430 0.008388
518+
... ... ... ...
519+
2016-05-28 1.111669 1.462630 0.009072
520+
2016-05-29 1.111669 1.462630 0.009072
521+
2016-05-30 1.112479 1.461999 0.009006
522+
2016-05-31 1.114269 1.461021 0.009010
523+
2016-06-01 1.115170 1.445410 0.009095
524+
525+
[153 rows x 3 columns]

pandas_datareader/_utils.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
import pandas as pd
2-
from pandas.core.common import PandasError
1+
import datetime as dt
32
from distutils.version import LooseVersion
43

4+
import pandas as pd
5+
from pandas.core.common import PandasError, is_number
6+
from pandas import to_datetime
7+
8+
import requests
9+
from requests_file import FileAdapter
10+
import requests_ftp
11+
requests_ftp.monkeypatch_session()
12+
513

614
if pd.compat.PY3:
715
from urllib.error import HTTPError # noqa
@@ -16,6 +24,36 @@ class SymbolWarning(UserWarning):
1624
class RemoteDataError(PandasError, IOError):
1725
pass
1826

27+
28+
def _sanitize_dates(start, end):
29+
"""
30+
Return (datetime_start, datetime_end) tuple
31+
if start is None - default is 2010/01/01
32+
if end is None - default is today
33+
"""
34+
if is_number(start):
35+
# regard int as year
36+
start = dt.datetime(start, 1, 1)
37+
start = to_datetime(start)
38+
39+
if is_number(end):
40+
end = dt.datetime(end, 1, 1)
41+
end = to_datetime(end)
42+
43+
if start is None:
44+
start = dt.datetime(2010, 1, 1)
45+
if end is None:
46+
end = dt.datetime.today()
47+
return start, end
48+
49+
50+
def _init_session(session, retry_count=3):
51+
if session is None:
52+
session = requests.Session()
53+
session.mount('file://', FileAdapter())
54+
# do not set requests max_retries here to support arbitrary pause
55+
return session
56+
1957
PANDAS_VERSION = LooseVersion(pd.__version__)
2058

2159
if PANDAS_VERSION >= LooseVersion('0.17.0'):

pandas_datareader/data.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from pandas_datareader.famafrench import FamaFrenchReader
2020
from pandas_datareader.oecd import OECDReader
2121
from pandas_datareader.edgar import EdgarIndexReader
22+
from pandas_datareader.oanda import get_oanda_currency_historical_rates
2223

2324

2425
def get_data_fred(*args, **kwargs):
@@ -146,6 +147,12 @@ def DataReader(name, data_source=None, start=None, end=None,
146147
return EdgarIndexReader(symbols=name, start=start, end=end,
147148
retry_count=retry_count, pause=pause,
148149
session=session).read()
150+
elif data_source == "oanda":
151+
return get_oanda_currency_historical_rates(
152+
start, end,
153+
quote_currency="USD", base_currency=name,
154+
reversed=True, session=session
155+
)
149156
else:
150157
msg = "data_source=%r is not implemented" % data_source
151158
raise NotImplementedError(msg)

pandas_datareader/oanda.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import pandas as pd
2+
from pandas.compat import StringIO, string_types
3+
from ._utils import _init_session, _sanitize_dates
4+
5+
6+
def reverse_pair(s, sep="/"):
7+
lst = s.split(sep)
8+
return sep.join([lst[1], lst[0]])
9+
10+
11+
def get_oanda_currency_historical_rates(start, end, quote_currency="USD", base_currency=None, reversed=True, session=None):
12+
session = _init_session(session)
13+
start, end = _sanitize_dates(start, end)
14+
15+
url = "http://www.oanda.com/currency/historical-rates/download"
16+
bam = "bid" # "ask", "mid"
17+
18+
if base_currency is None:
19+
base_currency = ["EUR", "GBP", "AUD", "NZD", "CAD", "CHF"]
20+
elif isinstance(base_currency, string_types):
21+
base_currency = [base_currency]
22+
23+
params = {
24+
"quote_currency": quote_currency,
25+
"start_date": start.strftime("%Y-%m-%d"),
26+
"end_date": end.strftime("%Y-%m-%d"),
27+
"period": "daily",
28+
"display": "absolute",
29+
"rate": 0,
30+
"data_range": "c",
31+
"price": bam,
32+
"view": "table",
33+
"download": "csv"
34+
}
35+
for i, _base_currency in enumerate(base_currency):
36+
params["base_currency_%d" % i] = _base_currency
37+
38+
response = session.get(url, params=params)
39+
skiprows = 4
40+
skipfooter = 4
41+
usecols = range(len(base_currency) + 1)
42+
df = pd.read_csv(StringIO(response.text), parse_dates=[0], skiprows=skiprows, skipfooter=skipfooter, usecols=usecols, engine='python')
43+
df = df.rename(columns={
44+
"End Date": "Date",
45+
})
46+
df = df.set_index("Date")
47+
df = df[::-1]
48+
if reversed:
49+
df.columns = pd.Index(df.columns.map(reverse_pair))
50+
df = 1 / df
51+
if len(base_currency) == 1:
52+
return df.iloc[:, 0]
53+
else:
54+
return df

pandas_datareader/tests/test_oanda.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import pandas as pd
2+
import pandas.util.testing as tm
3+
import pandas_datareader.data as web
4+
5+
from pandas_datareader.oanda import get_oanda_currency_historical_rates
6+
7+
8+
class TestOandaCurrencyHistoricalRate(tm.TestCase):
9+
def test_oanda_currency_historical_rate(self):
10+
start = "2016-01-01"
11+
end = "2016-06-01"
12+
quote_currency = "USD"
13+
base_currency = None
14+
reversed = True
15+
df_rates = get_oanda_currency_historical_rates(
16+
start, end,
17+
quote_currency=quote_currency,
18+
base_currency=base_currency, reversed=reversed
19+
)
20+
self.assertEqual(df_rates.index[0], pd.to_datetime("2016-01-01"))
21+
self.assertEqual(df_rates.index[-1], pd.to_datetime("2016-06-01"))
22+
23+
def test_oanda_currency_historical_rate_datareader(self):
24+
start = "2016-01-01"
25+
end = "2016-06-01"
26+
df_rates = web.DataReader(["EUR", "GBP", "JPY"], "oanda", start, end)
27+
self.assertEqual(df_rates.index[0], pd.to_datetime("2016-01-01"))
28+
self.assertEqual(df_rates.index[-1], pd.to_datetime("2016-06-01"))

0 commit comments

Comments
 (0)