Skip to content

Commit 92b9482

Browse files
committed
Added MOEX data source (Moscow Exchange)
1 parent 331e389 commit 92b9482

File tree

3 files changed

+183
-1
lines changed

3 files changed

+183
-1
lines changed

pandas_datareader/data.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66

77
from pandas_datareader.google.daily import GoogleDailyReader
88
from pandas_datareader.google.quotes import GoogleQuotesReader
9+
from pandas_datareader.google.options import Options as GoogleOptions
910

1011
from pandas_datareader.yahoo.daily import YahooDailyReader
1112
from pandas_datareader.yahoo.quotes import YahooQuotesReader
1213
from pandas_datareader.yahoo.actions import (YahooActionReader, YahooDivReader)
1314
from pandas_datareader.yahoo.components import _get_data as get_components_yahoo # noqa
1415
from pandas_datareader.yahoo.options import Options as YahooOptions
15-
from pandas_datareader.google.options import Options as GoogleOptions
1616

1717
from pandas_datareader.eurostat import EurostatReader
1818
from pandas_datareader.fred import FredReader
@@ -22,6 +22,7 @@
2222
from pandas_datareader.enigma import EnigmaReader
2323
from pandas_datareader.nasdaq_trader import get_nasdaq_symbols
2424
from pandas_datareader.quandl import QuandlReader
25+
from pandas_datareader.moex import MoexReader
2526

2627

2728
def get_data_fred(*args, **kwargs):
@@ -59,6 +60,8 @@ def get_quote_google(*args, **kwargs):
5960
def get_data_quandl(*args, **kwargs):
6061
return QuandlReader(*args, **kwargs).read()
6162

63+
def get_data_moex(*args, **kwargs):
64+
return MoexReader(*args, **kwargs).read()
6265

6366
def DataReader(name, data_source=None, start=None, end=None,
6467
retry_count=3, pause=0.001, session=None, access_key=None):
@@ -170,6 +173,10 @@ def DataReader(name, data_source=None, start=None, end=None,
170173
return QuandlReader(symbols=name, start=start, end=end,
171174
retry_count=retry_count, pause=pause,
172175
session=session).read()
176+
elif data_source == "moex":
177+
return MoexReader(symbols=name, start=start, end=end,
178+
retry_count=retry_count, pause=pause,
179+
session=session).read()
173180
else:
174181
msg = "data_source=%r is not implemented" % data_source
175182
raise NotImplementedError(msg)

pandas_datareader/moex.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
from pandas_datareader.base import _DailyBaseReader
2+
from pandas import read_csv, compat
3+
from pandas.compat import StringIO, bytes_to_str
4+
import datetime as dt
5+
6+
class MoexReader(_DailyBaseReader):
7+
8+
"""
9+
Returns DataFrame of historical stock prices from symbols, over date
10+
range, start to end. To avoid being penalized by Moex servers,
11+
pauses between downloading 'chunks' of symbols can be specified.
12+
13+
Parameters
14+
----------
15+
symbols : string, array-like object (list, tuple, Series), or DataFrame
16+
Single stock symbol (ticker), array-like object of symbols or
17+
DataFrame with index containing stock symbols.
18+
start : string, (defaults to '1/1/2010')
19+
Starting date, timestamp. Parses many different kind of date
20+
representations (e.g., 'JAN-01-2010', '1/1/10', 'Jan, 1, 1980')
21+
end : string, (defaults to today)
22+
Ending date, timestamp. Same format as starting date.
23+
retry_count : int, default 3
24+
Number of times to retry query request.
25+
pause : int, default 0
26+
Time, in seconds, to pause between consecutive queries of chunks. If
27+
single value given for symbol, represents the pause between retries.
28+
chunksize : int, default 25
29+
Number of symbols to download consecutively before intiating pause.
30+
session : Session, default None
31+
requests.sessions.Session instance to be used
32+
"""
33+
34+
def __init__(self, *args, **kwargs):
35+
super(MoexReader, self).__init__(*args, **kwargs)
36+
self.start = self.start.date()
37+
self.end_dt = self.end
38+
self.end = self.end.date()
39+
if not isinstance(self.symbols, compat.string_types):
40+
raise ValueError("Support for multiple symbols is not yet implemented.")
41+
42+
__url_metadata = "https://iss.moex.com/iss/securities/{symbol}.csv"
43+
__url_data = "https://iss.moex.com/iss/history/engines/{engine}/markets/{market}/securities/{symbol}.csv"
44+
45+
@property
46+
def url(self):
47+
return self.__url_data.format(
48+
engine = self.__engine,
49+
market = self.__market,
50+
symbol = self.symbols
51+
)
52+
53+
def _get_params(self, start):
54+
params = {
55+
'iss.only': 'history',
56+
'iss.dp': 'point',
57+
'iss.df': '%Y-%m-%d',
58+
'iss.tf': '%H:%M:%S',
59+
'iss.dft': '%Y-%m-%d %H:%M:%S',
60+
'iss.json': 'extended',
61+
'callback': 'JSON_CALLBACK',
62+
'from': start,
63+
'till': self.end_dt.strftime('%Y-%m-%d'),
64+
'limit': 100,
65+
'start': 1,
66+
'sort_order': 'TRADEDATE',
67+
'sort_order_desc': 'asc'
68+
}
69+
return params
70+
71+
def _get_metadata(self):
72+
""" get a market and an engine for a given symbol """
73+
response = self._get_response(self.__url_metadata.format(symbol=self.symbols))
74+
text = self._sanitize_response(response)
75+
if len(text) == 0:
76+
service = self.__class__.__name__
77+
raise IOError("{} request returned no data; check URL for invalid "
78+
"inputs: {}".format(service, self.__url_metadata))
79+
if isinstance(text, compat.binary_type):
80+
text = text.decode('windows-1251')
81+
else:
82+
text = text
83+
84+
header_str = 'secid;boardid;'
85+
get_data = False
86+
for s in text.splitlines():
87+
if s.startswith(header_str):
88+
get_data = True
89+
continue
90+
if get_data and s!='':
91+
fields = s.split(';')
92+
return fields[5], fields[7]
93+
service = self.__class__.__name__
94+
raise IOError("{} request returned no metadata"
95+
": {}\nTypo in security symbol `{}`?".format(service, self.__url_metadata.format(symbol=self.symbols), self.symbols))
96+
97+
def read(self):
98+
""" read data """
99+
try:
100+
self.__market, self.__engine = self._get_metadata()
101+
102+
end = self.end.strftime('%Y-%m-%d')
103+
out_list = []
104+
date_column = None
105+
while True: # read in loop with small date intervals
106+
if len(out_list)>0:
107+
if date_column is None:
108+
date_column = out_list[0].split(';').index('TRADEDATE')
109+
110+
start_str = out_list[-1].split(';', 4)[date_column] # get the last downloaded date
111+
start = dt.datetime.strptime(start_str, '%Y-%m-%d').date()
112+
else:
113+
start_str = self.start.strftime('%Y-%m-%d')
114+
start = self.start
115+
116+
if start >= self.end or start>=dt.date.today():
117+
break
118+
119+
params = self._get_params(start_str)
120+
strings_out = self._read_url_as_String(self.url, params).splitlines()[2:]
121+
strings_out = list(filter(lambda x: x.strip(), strings_out))
122+
123+
if len(out_list) == 0:
124+
out_list = strings_out
125+
if len(strings_out) < 101:
126+
break
127+
else:
128+
out_list += strings_out[1:] # remove CSV head line
129+
if len(strings_out) < 100:
130+
break
131+
str_io = StringIO('\r\n'.join(out_list))
132+
df = self._read_lines(str_io)
133+
return df
134+
finally:
135+
self.close()
136+
137+
def _read_url_as_String(self, url, params=None):
138+
""" Open url (and retry) """
139+
response = self._get_response(url, params=params)
140+
text = self._sanitize_response(response)
141+
if len(text) == 0:
142+
service = self.__class__.__name__
143+
raise IOError("{} request returned no data; check URL for invalid "
144+
"inputs: {}".format(service, self.url))
145+
if isinstance(text, compat.binary_type):
146+
out = text.decode('windows-1251')
147+
else:
148+
out = text
149+
return out
150+
151+
def _read_lines(self, out):
152+
rs = read_csv(out, index_col='TRADEDATE', parse_dates=True, sep=';',
153+
na_values=('-', 'null'))
154+
# Get rid of unicode characters in index name.
155+
try:
156+
rs.index.name = rs.index.name.decode(
157+
'unicode_escape').encode('ascii', 'ignore')
158+
except AttributeError:
159+
# Python 3 string has no decode method.
160+
rs.index.name = rs.index.name.encode('ascii', 'ignore').decode()
161+
return rs

pandas_datareader/tests/test_moex.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import os
2+
import pytest
3+
4+
from requests.exceptions import HTTPError
5+
import pandas_datareader as pdr
6+
import pandas_datareader.data as web
7+
8+
class TestMoex(object):
9+
def test_moex_datareader(self):
10+
try:
11+
df = web.DataReader("USD000UTSTOM", 'moex', start="2017-07-01", end="2017-07-31")
12+
assert 'SECID' in df.columns
13+
except HTTPError as e:
14+
pytest.skip(e)

0 commit comments

Comments
 (0)