Skip to content

Commit f9f778d

Browse files
authored
Merge pull request #469 from bashtage/dtemkin-mstar-new
Rebase Dtemkin Morningstar
2 parents b59a44b + bbc9f7e commit f9f778d

File tree

8 files changed

+405
-12
lines changed

8 files changed

+405
-12
lines changed

docs/source/readers/morningstar.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Morningstar
2+
-----------
3+
4+
.. py:module:: pandas_datareader.mstar.daily
5+
6+
.. autoclass:: MorningstarDailyReader
7+
:members:
8+
:inherited-members:
9+
10+
.. py:module:: pandas_datareader.mstar.financials
11+
12+
.. autoclass:: MorningstarDailyReader
13+
:members:
14+
:inherited-members:

docs/source/whatsnew/v0.6.0.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ Highlights include:
2424
have been removed. PDR would like to restore these features, and pull
2525
requests are welcome.
2626

27+
- A new connector for Morningstart Open, High, Low, Close and Volume was
28+
introduced (:issue:`467`)
29+
2730
- A new connector for IEX daily price data was introduced (:issue:`465`).
2831

2932
- A new connector for IEX the majority of the IEX API was introduced
@@ -50,6 +53,9 @@ Enhancements
5053
- A new data connector for data provided by the Bank of Canada was
5154
introduced. (:issue:`440`)
5255

56+
- A new data connector for stock pricing data provided by Morningstar
57+
was introduced. (:issue:`467`)
58+
5359
.. _whatsnew_060.api_breaking:
5460

5561
Backwards incompatible API changes

pandas_datareader/__init__.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from ._version import get_versions
2-
3-
from .data import (get_components_yahoo, get_data_famafrench, get_data_google,
4-
get_data_yahoo, get_data_enigma, get_data_yahoo_actions,
5-
get_quote_google, get_quote_yahoo, get_tops_iex,
6-
get_last_iex, get_markets_iex, get_summary_iex,
7-
get_records_iex, get_recent_iex, get_iex_symbols,
8-
get_iex_book, DataReader, Options)
2+
from .data import (DataReader, Options, get_components_yahoo,
3+
get_dailysummary_iex, get_data_enigma, get_data_famafrench,
4+
get_data_fred, get_data_google, get_data_moex,
5+
get_data_morningstar, get_data_quandl, get_data_stooq,
6+
get_data_yahoo, get_data_yahoo_actions, get_iex_book,
7+
get_iex_symbols, get_last_iex, get_markets_iex,
8+
get_nasdaq_symbols,
9+
get_quote_google, get_quote_yahoo, get_recent_iex,
10+
get_records_iex, get_summary_iex, get_tops_iex)
911

1012
__version__ = get_versions()['version']
1113
del get_versions
@@ -16,4 +18,6 @@
1618
'get_iex_book', 'get_iex_symbols', 'get_last_iex',
1719
'get_markets_iex', 'get_recent_iex', 'get_records_iex',
1820
'get_summary_iex', 'get_tops_iex',
19-
'DataReader', 'Options']
21+
'get_nasdaq_symbols', 'get_data_quandl', 'get_data_moex',
22+
'get_data_fred', 'get_dailysummary_iex', 'get_data_morningstar',
23+
'get_data_stooq', 'DataReader', 'Options']

pandas_datareader/data.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,19 @@
88
from pandas_datareader.edgar import EdgarIndexReader
99
from pandas_datareader.enigma import EnigmaReader
1010
from pandas_datareader.eurostat import EurostatReader
11-
from pandas_datareader.exceptions import ImmediateDeprecationError, \
12-
DEP_ERROR_MSG
11+
from pandas_datareader.exceptions import DEP_ERROR_MSG, \
12+
ImmediateDeprecationError
1313
from pandas_datareader.famafrench import FamaFrenchReader
1414
from pandas_datareader.fred import FredReader
1515
from pandas_datareader.google.daily import GoogleDailyReader
1616
from pandas_datareader.google.options import Options as GoogleOptions
1717
from pandas_datareader.google.quotes import GoogleQuotesReader
1818
from pandas_datareader.iex.daily import IEXDailyReader
1919
from pandas_datareader.iex.deep import Deep as IEXDeep
20-
from pandas_datareader.iex.tops import LastReader as IEXLasts
21-
from pandas_datareader.iex.tops import TopsReader as IEXTops
20+
from pandas_datareader.iex.tops import LastReader as IEXLasts, \
21+
TopsReader as IEXTops
2222
from pandas_datareader.moex import MoexReader
23+
from pandas_datareader.mstar.daily import MorningstarDailyReader
2324
from pandas_datareader.nasdaq_trader import get_nasdaq_symbols
2425
from pandas_datareader.oecd import OECDReader
2526
from pandas_datareader.quandl import QuandlReader
@@ -38,6 +39,7 @@
3839
'get_tops_iex', 'get_summary_iex', 'get_records_iex',
3940
'get_recent_iex', 'get_markets_iex', 'get_last_iex',
4041
'get_iex_symbols', 'get_iex_book', 'get_dailysummary_iex',
42+
'get_data_morningstar', 'get_data_stooq',
4143
'get_data_stooq', 'DataReader']
4244

4345

@@ -97,6 +99,10 @@ def get_last_iex(*args, **kwargs):
9799
return IEXLasts(*args, **kwargs).read()
98100

99101

102+
def get_data_morningstar(*args, **kwargs):
103+
return MorningstarDailyReader(*args, **kwargs).read()
104+
105+
100106
def get_markets_iex(*args, **kwargs):
101107
"""
102108
Returns near-real time volume data across markets segregated by tape
@@ -359,6 +365,11 @@ def DataReader(name, data_source=None, start=None, end=None,
359365
return MoexReader(symbols=name, start=start, end=end,
360366
retry_count=retry_count, pause=pause,
361367
session=session).read()
368+
elif data_source == "morningstar":
369+
return MorningstarDailyReader(symbols=name, start=start, end=end,
370+
retry_count=retry_count, pause=pause,
371+
session=session, interval="d").read()
372+
362373
else:
363374
msg = "data_source=%r is not implemented" % data_source
364375
raise NotImplementedError(msg)

pandas_datareader/mstar/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""
2+
written and developed by Daniel Temkin
3+
please refer to LICENSE for ownership and reference information
4+
"""

pandas_datareader/mstar/daily.py

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import time
2+
from datetime import datetime, timedelta
3+
from warnings import warn
4+
5+
import requests
6+
from pandas import DataFrame
7+
8+
from pandas_datareader._utils import SymbolWarning
9+
from pandas_datareader.base import _BaseReader
10+
11+
12+
class MorningstarDailyReader(_BaseReader):
13+
"""
14+
Read daily data from Morningstar
15+
16+
Parameters
17+
----------
18+
symbols : {str, List[str]}
19+
String symbol of like of symbols
20+
start : string, (defaults to '1/1/2010')
21+
Starting date, timestamp. Parses many different kind of date
22+
representations (e.g., 'JAN-01-2010', '1/1/10', 'Jan, 1, 1980')
23+
end : string, (defaults to today)
24+
Ending date, timestamp. Same format as starting date.
25+
retry_count : int, default 3
26+
Number of times to retry query request.
27+
pause : float, default 0.1
28+
Time, in seconds, of the pause between retries.
29+
session : Session, default None
30+
requests.sessions.Session instance to be used
31+
freq : {str, None}
32+
Frequency to use in select readers
33+
incl_splits : bool, optional
34+
Include splits in data
35+
incl_dividends : bool,, optional
36+
Include divdends in data
37+
incl_volume : bool, optional
38+
Include volume in data
39+
currency : str, optional
40+
Currency to use for data
41+
interval : str, optional
42+
Sampling interval to use for downloaded data
43+
"""
44+
45+
def __init__(self, symbols, start=None, end=None, retry_count=3,
46+
pause=0.1, timeout=30, session=None, freq=None,
47+
incl_splits=False, incl_dividends=False, incl_volume=True,
48+
currency='usd', interval='d'):
49+
super(MorningstarDailyReader, self).__init__(symbols, start, end,
50+
retry_count, pause,
51+
timeout, session, freq)
52+
53+
self.incl_splits = incl_splits
54+
self.incl_dividends = incl_dividends
55+
self.incl_vol = incl_volume
56+
self.currency = currency
57+
self.interval = interval
58+
59+
self._symbol_data_cache = []
60+
61+
def _url_params(self):
62+
if self.interval not in ['d', 'wk', 'mo', 'm', 'w']:
63+
raise ValueError("Invalid interval: valid values are 'd', 'wk' "
64+
"and 'mo'. 'm' and 'w' have been implemented for "
65+
"backward compatibility")
66+
elif self.interval in ['m', 'mo']:
67+
self.interval = 'm'
68+
elif self.interval in ['w', 'wk']:
69+
self.interval = 'w'
70+
71+
if self.currency != "usd":
72+
warn("Caution! There is no explicit check for a valid currency "
73+
"acronym\nIf an error is encountered consider changing this "
74+
"value.")
75+
76+
p = {"range": "|".join(
77+
[self.start.strftime("%Y-%m-%d"), self.end.strftime("%Y-%m-%d")]),
78+
"f": self.interval, "curry": self.currency,
79+
"dtype": "his", "showVol": "true",
80+
"hasF": "true", "isD": "true", "isS": "true",
81+
"ProdCode": "DIRECT"}
82+
83+
return p
84+
85+
@property
86+
def url(self):
87+
"""API URL"""
88+
return "http://globalquote.morningstar.com/globalcomponent/" \
89+
"RealtimeHistoricalStockData.ashx"
90+
91+
def _get_crumb(self, *args):
92+
"""Not required """
93+
pass
94+
95+
def _dl_mult_symbols(self, symbols):
96+
failed = []
97+
symbol_data = []
98+
for symbol in symbols:
99+
100+
params = self._url_params()
101+
params.update({"ticker": symbol})
102+
103+
try:
104+
resp = requests.get(self.url, params=params)
105+
except Exception:
106+
if symbol not in failed:
107+
if self.retry_count == 0:
108+
warn("skipping symbol %s: number of retries "
109+
"exceeded." % symbol)
110+
pass
111+
else:
112+
print("adding %s to retry list" % symbol)
113+
failed.append(symbol)
114+
else:
115+
if resp.status_code == requests.codes.ok:
116+
jsondata = resp.json()
117+
if jsondata is None:
118+
failed.append(symbol)
119+
continue
120+
jsdata = self._restruct_json(symbol=symbol,
121+
jsondata=jsondata)
122+
symbol_data.extend(jsdata)
123+
else:
124+
raise Exception("Request Error!: %s : %s" % (
125+
resp.status_code, resp.reason))
126+
127+
time.sleep(self.pause)
128+
129+
if len(failed) > 0 and self.retry_count > 0:
130+
# TODO: This appears to do nothing since
131+
# TODO: successful symbols are not added to
132+
self._dl_mult_symbols(symbols=failed)
133+
self.retry_count -= 1
134+
else:
135+
self.retry_count = 0
136+
137+
if not symbol_data:
138+
raise ValueError('All symbols were invalid')
139+
elif self.retry_count == 0 and len(failed) > 0:
140+
warn("The following symbols were excluded do to http "
141+
"request errors: \n %s" % failed, SymbolWarning)
142+
143+
symbols_df = DataFrame(data=symbol_data)
144+
dfx = symbols_df.set_index(["Symbol", "Date"])
145+
return dfx
146+
147+
@staticmethod
148+
def _convert_index2date(enddate, indexvals):
149+
i = 0
150+
while i < len(indexvals):
151+
days = indexvals[len(indexvals) - 1] - indexvals[i]
152+
d = enddate - timedelta(days=days)
153+
i += 1
154+
yield d.strftime("%Y-%m-%d")
155+
156+
def _restruct_json(self, symbol, jsondata):
157+
if jsondata is None:
158+
return
159+
divdata = jsondata["DividendData"]
160+
161+
pricedata = jsondata["PriceDataList"][0]["Datapoints"]
162+
dateidx = jsondata["PriceDataList"][0]["DateIndexs"]
163+
volumes = jsondata["VolumeList"]["Datapoints"]
164+
165+
date_ = self._convert_index2date(enddate=self.end, indexvals=dateidx)
166+
barss = []
167+
for p in range(len(pricedata)):
168+
bar = pricedata[p]
169+
d = next(date_)
170+
bardict = {
171+
"Symbol": symbol, "Date": d, "Open": bar[0], "High": bar[1],
172+
"Low": bar[2],
173+
"Close": bar[3]
174+
}
175+
if len(divdata) == 0:
176+
pass
177+
else:
178+
events = []
179+
for x in divdata:
180+
delta = (datetime.strptime(x["Date"], "%Y-%m-%d") -
181+
datetime.strptime(d, "%Y-%m-%d"))
182+
if delta.days == 0:
183+
events.append(x)
184+
for e in events:
185+
if self.incl_dividends and e["Type"].find("Div") > -1:
186+
val = e["Desc"].replace(e["Type"], "")
187+
bardict.update({"isDividend": val})
188+
elif (self.incl_splits is True and
189+
e["Type"].find("Split") > -1):
190+
val = e["Desc"].replace(e["Type"], "")
191+
bardict.update({"isSplit": val})
192+
else:
193+
pass
194+
if self.incl_vol is True:
195+
bardict.update({"Volume": int(volumes[p] * 1000000)})
196+
else:
197+
pass
198+
199+
barss.append(bardict)
200+
return barss
201+
202+
def read(self):
203+
"""Read data"""
204+
if isinstance(self.symbols, str):
205+
symbols = [self.symbols]
206+
else:
207+
symbols = self.symbols
208+
209+
is_str = False
210+
try:
211+
is_str = all(map(lambda v: isinstance(v, str), symbols))
212+
except Exception:
213+
pass
214+
215+
if not is_str:
216+
raise TypeError("symbols must be iterable or string and not "
217+
"type %s" % type(self.symbols))
218+
219+
df = self._dl_mult_symbols(symbols=symbols)
220+
if len(df.index.levels[0]) == 0:
221+
raise ValueError("None of the provided symbols were valid")
222+
else:
223+
return df

pandas_datareader/tests/mstar/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)