Coverage for src/pyensae/finance/astock.py: 74%

345 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-03 02:16 +0200

1""" 

2@file 

3@brief Downloads stock prices (from Yahoo website) and other prices. 

4""" 

5import os 

6import urllib.request 

7import urllib.error 

8import datetime 

9from io import StringIO 

10import pandas 

11import numpy 

12import requests 

13from pyquickhelper.filehelper import is_file_string 

14 

15 

16class StockPricesException(Exception): 

17 """ 

18 Raised by StockPrices classes. 

19 """ 

20 pass 

21 

22 

23class StockPricesHTTPException(StockPricesException): 

24 """ 

25 Raised by StockPrices classes. 

26 """ 

27 pass 

28 

29 

30class StockPrices: 

31 

32 """ 

33 Defines a class containing stock prices, provides basic functions, 

34 the class uses :epkg:`pandas` to load the data. 

35 

36 .. exref:: 

37 :title: Retrieve stock prices from the Yahoo source 

38 

39 :: 

40 

41 from pyensae.finance import StockPrices 

42 prices = StockPrices(tick="NASDAQ:MSFT") 

43 print(prices.dataframe.head()) 

44 

45 The class loads a stock price from either a url or a folder 

46 where the data was cached. If a filename 

47 ``<folder>/<tick>.<day1>.<day2>.txt`` already exists, 

48 it takes it from here. Otherwise, it downloads it. 

49 

50 A couple of providers have been implemented but it is not 

51 easy to keep them up to date as policies from website 

52 change on a regular basis. 

53 If *url* is ``'yahoo'``, the data will be download using 

54 `CAC 40 <http://finance.yahoo.com/q/cp?s=^FCHI+Components>`_. 

55 The CAC40 composition is described by 

56 `Wikipedia CAC 40 <http://fr.wikipedia.org/wiki/CAC_40>`_. 

57 However `Yahoo Finance <https://fr.finance.yahoo.com/>`_ 

58 introduced the use of cookies in May 2017 

59 and it is not so easy to automate. 

60 The default provider could be 

61 *Google Finance* which has now been integrated into the 

62 search engine. 

63 Tick names depends on the data prodiver. More details: 

64 `European Markets Information <https://www.stockmarketeye.com/users-guide/ticker-symbols-and-data-providers/euro-stocks.html>`_. 

65 You can also go to `quandl <https://www.quandl.com/data/EURONEXT/BNP-Bnp-Paribas-Act-A-BNP>`_ 

66 and get the tick for the module `quandl <https://www.quandl.com/tools/python>`_. 

67 As of May 14th, the following error appears when using 

68 ``url='yahoo'`` which comes from an error in 

69 :epkg:`pandas_reader`:: 

70 

71 ImmediateDeprecationError(DEP_ERROR_MSG.format('Yahoo Daily')) 

72 pandas_datareader.exceptions.ImmediateDeprecationError: 

73 Yahoo Daily has been immediately deprecated due to large breaks in the API without the 

74 introduction of a stable replacement. Pull Requests to re-enable these data 

75 connectors are welcome. 

76 

77 See https://github.com/pydata/pandas-datareader/issues 

78 

79 ``url='yahoo_new'`` should solve the issue. 

80 It relies on :epkg:`yahoo_historial`. 

81 Data can be downloaded for a specific period of time. 

82 If not specified, it takes the largest available. 

83 

84 .. exref:: 

85 :title: Compute the average returns and correlation matrix 

86 

87 :: 

88 

89 import pyensae, pandas 

90 from pyensae.finance import StockPrices 

91 from pyensae.datasource import download_data 

92 

93 # download the CAC 40 composition from my website (for Yahoo) 

94 download_data('cac40_2013_11_11.txt', website='xd') 

95 

96 # download all the prices (if not already done) and store them into files 

97 actions = pandas.read_csv("cac40_2013_11_11.txt", sep="\\t") 

98 

99 # we remove stocks with not enough historical data 

100 stocks = { k:StockPrices(tick = k) for k,v in actions.values } 

101 dates = StockPrices.available_dates(stocks.values()) 

102 stocks = {k:v for k,v in stocks.items() if len(v.missing(dates)) <= 10} 

103 print("nb left", len(stocks)) 

104 

105 # we remove dates with missing prices 

106 dates = StockPrices.available_dates(stocks.values()) 

107 ok = dates[dates["missing"] == 0] 

108 print("all dates before", len(dates), " after:" , len(ok)) 

109 for k in stocks: 

110 stocks[k] = stocks[k].keep_dates(ok) 

111 

112 # we compute correlation matrix and returns 

113 ret, cor = StockPrices.covariance(stocks.values(), cov = False, ret = True) 

114 

115 You should also look at 

116 `pyensae et notebook <http://www.xavierdupre.fr/blog/notebooks/example%20pyensae.html>`_. 

117 If you use `Google Finance <https://www.google.com/finance>`_ 

118 as a provider, the tick name is usually 

119 prefixed by the market places (NASDAQ for example). The export 

120 does not work for all markets places. 

121 Another provider was added, ``yahoo_new`` which delegates the task 

122 of getting data from `Yahoo Finance <https://finance.yahoo.com/>`_ to module 

123 `yahoo-historical <https://github.com/AndrewRPorter/yahoo-historical>`_. 

124 """ 

125 

126 def __init__(self, tick, url="google", folder="cache", 

127 begin=None, end=None, sep=",", 

128 intern=False, use_dtime=False): 

129 """ 

130 @param tick tick name, ex ``NASDAQ:MSFT`` 

131 @param url if yahoo, downloads the data from there if it was not done before 

132 url is possible, ``'google'``, ``'yahoo_new'``, 

133 ``'quandl'`` are predefined values 

134 @param folder cache folder (created if it does not exists 

135 @param begin first day (datetime), see below 

136 @param end last day (datetime), see below 

137 @param sep column separator 

138 @param intern do not use unless you know what to do 

139 (see :meth:`__getitem__ <pyensae.finance.astock.StockPrices.__getitem__>`) 

140 @param use_dtime if True, use DateTime instead of string 

141 """ 

142 if isinstance(url, pandas.DataFrame): 

143 self.datadf = url 

144 self.tickname = tick 

145 if "Date" not in url.columns: 

146 raise StockPricesHTTPException( 

147 "the dataframe does not contain any column 'Date': {0}".format( 

148 ",".join( 

149 _ for _ in url.columns))) 

150 elif isinstance(tick, str) and is_file_string(tick) and os.path.exists(tick): 

151 self.tickname = os.path.split(tick)[-1] 

152 with open(tick, "r") as f: 

153 for line in f.readlines(): 

154 if line.startswith('<!DOCTYPE html PUBLIC'): 

155 raise StockPricesHTTPException( 

156 "pandas cannot parse the file, check your have access to internet: " + str(tick)) 

157 break 

158 try: 

159 self.datadf = pandas.read_csv(tick, sep=sep) 

160 except Exception as e: 

161 with open(tick, "r") as t: 

162 content = t.read() 

163 if "Firewall Authentication" in content: 

164 raise StockPricesException( 

165 "pandas cannot parse the file, check your have access to internet: " + str(tick)) from e 

166 raise 

167 else: 

168 if not os.path.exists(folder): 

169 try: 

170 os.mkdir(folder) 

171 except PermissionError as e: 

172 raise StockPricesException(("PermissionError, unable to create directory '{0}', " + 

173 "check you execute the program in a folder you have " + 

174 "permission to modify ({1})").format(folder, os.getcwd())) from e 

175 self.tickname = tick 

176 

177 if begin is None: 

178 begin = datetime.datetime(2000, 1, 3) 

179 if end is None: 

180 now = datetime.datetime.now() 

181 end = now - datetime.timedelta(1) 

182 

183 sbeg = begin.strftime("%Y-%m-%d") 

184 send = end.strftime("%Y-%m-%d") 

185 name = os.path.join(folder, tick.replace(":", "_").replace("/", "_").replace("\\\\", "_") + 

186 ".{0}.{1}.txt".format(sbeg, send)) 

187 

188 date_format = None 

189 if not os.path.exists(name): 

190 if url == "google": 

191 use_url = True 

192 url_string = "https://finance.google.com/finance/historical?q={0}".format( 

193 self.tickname) 

194 url_string += "&startdate={0}&enddate={1}&output=csv".format( 

195 begin.strftime('%b %d, %Y'), end.strftime('%b %d, %Y')) 

196 url = url_string.replace(" ", "+").replace(",", "%2C") 

197 date_format = "%b-%d-%Y" 

198 elif url == "quandl": 

199 import quandl # pylint: disable=C0415 

200 df = quandl.get( 

201 "EURONEXT/BNP", start_date=begin.strftime('%Y-%m-%d'), end_date=end.strftime('%Y-%m-%d')) 

202 df.reset_index(drop=False).to_csv( 

203 name, sep=sep, index=False) 

204 use_url = False 

205 elif url == 'yahoo_new': 

206 from yahoo_historical import Fetcher 

207 

208 dt = datetime.datetime(begin.year, begin.month, begin.day) 

209 ts = dt.timestamp() 

210 try: 

211 data = Fetcher(tick, ts) 

212 except TypeError as e: 

213 raise TypeError( 

214 f"Unable to fetch data with year={begin.year!r}, " 

215 f"month={begin.month!r}, day={begin.day!r}.") from e 

216 df = data.get_historical() 

217 df.to_csv(name, sep=sep, index=False) 

218 use_url = False 

219 elif url in ("yahoo", "google", "fred", "famafrench"): 

220 import pandas_datareader.data as web # pylint: disable=C0415 

221 df = web.DataReader(self.tickname, url, 

222 begin, end).reset_index(drop=False) 

223 df.to_csv(name, sep=sep, index=False) 

224 use_url = False 

225 else: 

226 raise StockPricesHTTPException( 

227 "Unable to download data '{0}' from the following website '{1}'".format(tick, url)) 

228 

229 if use_url: 

230 self.url_ = url 

231 try: 

232 u = urllib.request.urlopen(url) 

233 text = u.read() 

234 u.close() 

235 except urllib.error.HTTPError as e: 

236 raise StockPricesHTTPException( 

237 "HTTPError, unable to load tick '{0}'\nURL: {1}".format(tick, url)) from e 

238 

239 if len(text) < 10: 

240 raise StockPricesHTTPException( 

241 "nothing to download for '{0}' less than 10 downloaded bytes".format(tick)) 

242 

243 try: 

244 f = open(name, "wb") 

245 f.write(text) 

246 f.close() 

247 except PermissionError as e: 

248 raise StockPricesException(("PermissionError, unable to create directory '{0}', " + 

249 "check you execute the program in a folder you have " + 

250 "permission to modify ({1})").format(folder, os.getcwd())) from e 

251 else: 

252 self.url_ = name 

253 

254 try: 

255 self.datadf = pandas.read_csv(name, sep=sep) 

256 except Exception as e: 

257 with open(tick, "r") as t: 

258 content = t.read() 

259 if "Firewall Authentication" in content: 

260 raise StockPricesException( 

261 "pandas cannot parse the file, check your have access to internet '{0}'".format(tick)) from e 

262 raise 

263 

264 if date_format is not None: 

265 self.datadf["Date"] = pandas.to_datetime(self.datadf["Date"]) 

266 self.datadf["Date"] = self.datadf["Date"].apply( 

267 lambda x: x.strftime('%Y-%m-%d')) 

268 self.datadf.to_csv(name, sep=sep, index=False) 

269 

270 if use_dtime: 

271 self.datadf["Date"] = pandas.to_datetime(self.datadf["Date"]) 

272 

273 if not intern: 

274 try: 

275 self.datadf = self.datadf.sort_values("Date") 

276 except ValueError as e: 

277 if "'Date' is both an index level and a column label" in str(e): 

278 vals = self.datadf['Date'] 

279 ind = self.datadf.index 

280 if numpy.array_equal(vals, ind): 

281 self.datadf = self.datadf.sort_index() 

282 else: 

283 raise StockPricesException( 

284 "Columns Date and index are different.") from e 

285 else: 

286 raise 

287 except AttributeError: 

288 self.datadf = self.datadf.sort("Date") 

289 except KeyError as e: 

290 raise StockPricesException("schema: {}".format( 

291 ",".join(self.datadf.columns))) from e 

292 self.datadf.reset_index(drop=True, inplace=True) 

293 self.datadf.set_index("Date", drop=False, inplace=True) 

294 

295 def __getitem__(self, key): 

296 """ 

297 Overloads the ``getitem`` operator to get a @see cl StockPrice object. 

298 

299 @param key key 

300 @return StockPrice 

301 """ 

302 return StockPrices( 

303 self.tick, self.datadf.__getitem__(key), intern=True) 

304 

305 def __len__(self): 

306 """ 

307 @return number of observations 

308 """ 

309 return len(self.datadf) 

310 

311 @property 

312 def shape(self): 

313 """ 

314 @return number of observations 

315 """ 

316 return self.datadf.shape 

317 

318 @property 

319 def tick(self): 

320 """ 

321 Returns the tick name. 

322 """ 

323 return self.tickname 

324 

325 @property 

326 def dataframe(self): 

327 """ 

328 Returns the dataframe. 

329 """ 

330 return self.datadf 

331 

332 def df(self): 

333 """ 

334 Returns the dataframe. 

335 """ 

336 return self.datadf 

337 

338 def FirstDate(self): 

339 """ 

340 Returns the first date. 

341 """ 

342 return self.datadf["Date"].min() 

343 

344 def LastDate(self): 

345 """ 

346 Returns the first date. 

347 """ 

348 return self.datadf["Date"].max() 

349 

350 def missing(self, trading_dates): 

351 """ 

352 Returnq the list of missing dates from an overset of trading dates. 

353 

354 @param trading_dates trading_dates (DataFrame having the column ``Date`` or in the index) 

355 @return missing dates (or None if issues) 

356 """ 

357 da = self.dataframe["Date"] 

358 da2 = {v: 1 for v in da} 

359 

360 if isinstance(trading_dates, dict): 

361 se = trading_dates 

362 else: 

363 se = trading_dates[ 

364 "Date"] if "Date" in trading_dates.columns else trading_dates.index 

365 

366 tbl = [{"Date": v} for v in se if v not in da2] 

367 if len(tbl) > 0: 

368 df = pandas.DataFrame(tbl) 

369 try: 

370 return df.sort_values("Date") 

371 except AttributeError: 

372 return df.sort("Date") 

373 else: 

374 return None 

375 

376 @staticmethod 

377 def available_dates(listStockPrices, missing=True, field="Close"): 

378 """ 

379 Returns the list of values (Open or High or Low or Close or Volume) from each stock 

380 for all the available_dates for a list of stock prices. 

381 

382 A missing date is a date for which there is at least one stock price and one missing stock price. 

383 

384 if ``missing`` is true a column is added which gives the number of missing stock prices for this dates 

385 

386 @param listStockPrices list of StockPrices 

387 @param missing True or False 

388 @param field which field to use to fill the matrix 

389 @return matrix with the available dates for each stock 

390 """ 

391 if field == "ohlc": 

392 field = ["Open", "High", "Low", "Close"] 

393 dates = [] 

394 if isinstance(field, str): 

395 for st in listStockPrices: 

396 lifi = list(st.dataframe.columns) 

397 index = lifi.index(field) 

398 for row in st.dataframe.values: 

399 date = row[0] 

400 dates.append( 

401 {"Date": date, "tick": st.tick, field: row[index]}) 

402 elif isinstance(field, (tuple, list)): 

403 for st in listStockPrices: 

404 lifi = list(st.dataframe.columns) 

405 indexes = [lifi.index(f) for f in field] 

406 for row in st.dataframe.values: 

407 date = row[0] 

408 r = {"Date": date, "tick": st.tick, } 

409 for i, f in zip(indexes, field): 

410 r[f] = row[i] 

411 dates.append(r) 

412 else: 

413 raise TypeError("field must be a string, a tuple or a list") 

414 

415 df = pandas.DataFrame(dates) 

416 if isinstance(field, str): 

417 piv = df.pivot(index="Date", columns="tick", values=field) 

418 elif isinstance(field, (tuple, list)): 

419 pivs = [df.pivot(index="Date", columns="tick", values=f) for f in field] 

420 for fi, piv in zip(field, pivs): 

421 col = [c + "," + fi for c in piv.columns] 

422 piv.columns = col 

423 if len(pivs) == 1: 

424 piv = pivs[0] 

425 else: 

426 piv = pivs[0].merge(pivs[1], how="outer", 

427 left_index=True, right_index=True) 

428 for p in pivs[2:]: 

429 piv = piv.merge( 

430 p, how="outer", left_index=True, right_index=True) 

431 else: 

432 raise TypeError("field must be a string, a tuple or a list") 

433 

434 if missing: 

435 def count_nan(row): 

436 "count nans" 

437 n = 0 

438 for k, v in row.items(): 

439 if k == "Date": 

440 continue 

441 if numpy.isnan(v): 

442 n += 1 

443 return n 

444 piv["missing"] = piv.apply(lambda row: count_nan(row), axis=1) 

445 

446 try: 

447 piv = piv.sort_index() 

448 except AttributeError: 

449 piv = piv.sort() 

450 return piv 

451 

452 def head(self): 

453 """ 

454 usual 

455 """ 

456 return self.dataframe.head() 

457 

458 def tail(self): 

459 """ 

460 usual 

461 """ 

462 return self.dataframe.tail() 

463 

464 def keep_dates(self, trading_dates): 

465 """ 

466 removes undesired dates 

467 

468 @param trading_dates dates 

469 @return new series 

470 """ 

471 da = self.dataframe["Date"] 

472 da2 = {v: 1 for v in da} 

473 

474 if isinstance(trading_dates, dict): 

475 se = trading_dates 

476 else: 

477 se = trading_dates[ 

478 "Date"] if "Date" in trading_dates.columns else trading_dates.index 

479 

480 tbl = {v: 1 for v in se if v in da2} 

481 if len(tbl) > 0: 

482 ave = self.dataframe.apply(lambda row: row["Date"] in tbl, axis=1) 

483 return StockPrices(self.tickname, self.dataframe.loc[ave, :]) 

484 else: 

485 raise StockPricesException("no trading dates left") 

486 

487 def returns(self): 

488 """ 

489 Builds the series of returns. 

490 

491 @param col column to use to compute the returns 

492 @return StockPrices 

493 """ 

494 df = self.dataframe 

495 fd = self.FirstDate() 

496 ld = self.LastDate() 

497 

498 plus = df["Date"] > fd # dates from FirstDate+1 to LastDate 

499 moins = df["Date"] < ld # dates from FirstDate to LastDate-1 

500 

501 res = df.loc[plus, ["Date", "Volume"]] 

502 

503 for k in df.columns: 

504 if k in ["Date", "Volume"]: 

505 continue 

506 m = numpy.array(df.loc[moins, k]) 

507 p = numpy.array(df.loc[plus, k]) 

508 res[k] = (p - m) / m 

509 

510 return StockPrices(self.tickname, res) 

511 

512 @staticmethod 

513 def covariance( 

514 listStockPrices, missing=True, field="Close", cov=True, ret=False): 

515 """ 

516 Computes the covariances matrix (of returns). 

517 

518 @param listStockPrices list of StockPrices 

519 @param field which field to use to fill the matrix 

520 @param cov if True, returns the covariance, otherwise, the correlations 

521 @param ret if True, also add the returns 

522 @return square dataframe or 2 dataframe (returns, correlation) 

523 """ 

524 listStockPrices = [v.returns() for v in listStockPrices] 

525 mat = StockPrices.available_dates(listStockPrices, False, field) 

526 

527 npmat = numpy.array(mat) 

528 cov = numpy.cov( 

529 npmat.transpose()) if cov else numpy.corrcoef( 

530 npmat.transpose()) 

531 names = [v.tick for v in listStockPrices] 

532 ret_mat = pandas.DataFrame(cov, columns=names, index=names) 

533 

534 if ret: 

535 rows = [{"tick": v.tick, "return": v.dataframe[field].mean()} 

536 for v in listStockPrices] 

537 ret = pandas.DataFrame(rows) 

538 ret.set_index("tick", drop=True, inplace=True) 

539 return ret, ret_mat 

540 else: 

541 return ret_mat 

542 

543 def plot(self, begin=None, end=None, 

544 field="Close", date_format=None, 

545 existing=None, axis=1, ax=None, 

546 label_prefix=None, color=None, **args): 

547 """ 

548 See :meth:`draw <pyensae.finance.astock.StockPrices.draw>`. 

549 """ 

550 return StockPrices.draw(self, begin=begin, end=end, 

551 field=field, date_format=date_format, 

552 existing=existing, axis=axis, ax=ax, 

553 label_prefix=label_prefix, color=color, 

554 **args) 

555 

556 @staticmethod 

557 def draw(listStockPrices, begin=None, end=None, 

558 field="Close", date_format=None, 

559 existing=None, axis=1, ax=None, 

560 label_prefix=None, color=None, **args): 

561 """ 

562 Draws a graph showing one or several time series. 

563 The example was taken 

564 `date_demo.py <https://matplotlib.org/examples/api/date_demo.html>`_. 

565 

566 @param listStockPrices list of @see cl StockPrices (or one @see cl StockPrices if it is the only one) 

567 @param begin first date (datetime) or None to take the first one 

568 @param end last included date (datetime) or None to take the last one 

569 @param field Open, High, Low, Close, Adj Close, Volume 

570 @param date_format ``%Y`` or ``%Y-%m`` or ``%Y-%m-%d`` or None if you prefer the function to choose 

571 @param args other arguments to send to ``plt.subplots`` 

572 @param axis 1 or 2, it only works if existing is not None. 

573 If axis is 2, the function draws the curves on the second axis. 

574 @param label_prefix to prefix curve label 

575 @param color curve color 

576 @param args other parameters to give method ``plt.subplots`` 

577 @param ax use existing `axes <http://matplotlib.org/api/axes_api.html>`_ 

578 @return `axes <http://matplotlib.org/api/axes_api.html>`_ 

579 

580 The parameter ``figsize`` of the method 

581 `subplots <https://matplotlib.org/api/pyplot_api.html?highlight=subplots#matplotlib.pyplot.subplots>`_ 

582 can change the graph size (see the example below). 

583 

584 .. exref:: 

585 :title: graph of a financial series 

586 

587 :: 

588 

589 from pyensae.finance import StockPrices 

590 stocks = [ StockPrices("NASDAQ:MSFT", folder = cache), 

591 StockPrices("NASDAQ:GOOGL", folder = cache), 

592 StockPrices("NASDAQ:AAPL", folder = cache)] 

593 fig, ax, plt = StockPrices.draw(stocks) 

594 fig.savefig("image.png") 

595 fig, ax, plt = StockPrices.draw(stocks, begin="2010-01-01", figsize=(16,8)) 

596 plt.show() 

597 

598 You can also chain the graphs and add a series on a second graph: 

599 

600 :: 

601 

602 from pyensae.finance import StockPrices 

603 stock = StockPrices("NASDAQ:MSFT", folder = cache) 

604 stock2 = StockPrices "NASDAQ:GOOGL", folder = cache) 

605 fig, ax, plt = stock.plot(figsize=(16,8)) 

606 fig, ax, plt = stock2.plot(existing=(fig,ax), axis=2) 

607 plt.show() 

608 

609 .. versionchanged:: 1.1 

610 Parameter *existing* was removed and parameter *ax* was added. 

611 If the date overlaps, the method 

612 `autofmt_xdate <https://matplotlib.org/api/figure_api.html#matplotlib.figure.Figure.autofmt_xdate>`_ 

613 should be called. 

614 """ 

615 if isinstance(listStockPrices, StockPrices): 

616 listStockPrices = [listStockPrices] 

617 

618 data = StockPrices.available_dates( 

619 listStockPrices, missing=False, field=field) 

620 if begin is None: 

621 if end is not None: 

622 data = data[data.index <= end] 

623 else: 

624 if end is not None: 

625 data = data[(data.index >= begin) & (data.index <= end)] 

626 else: 

627 data = data[data.index >= begin] 

628 

629 dates = [datetime.datetime.strptime(_, '%Y-%m-%d') for _ in data.index] 

630 begin = dates[0] 

631 end = dates[-1] 

632 

633 def price(x): 

634 "local formatting" 

635 return '%1.2f' % x 

636 

637 import matplotlib.pyplot as plt # pylint: disable=C0415 

638 import matplotlib.dates as mdates # pylint: disable=C0415 

639 

640 if ax is not None: 

641 ex_h, ex_l = ax.get_legend_handles_labels() 

642 ex_l = tuple(ex_l) 

643 ex_h = tuple(ex_h) 

644 if axis == 2: 

645 ax = ax.twinx() 

646 fig = None 

647 else: 

648 if 'label' in args: 

649 args_ = {k: v for k, v in args.items() if k not in ('label', )} 

650 else: 

651 args_ = args 

652 fig, ax = plt.subplots(**args_) 

653 ex_h, ex_l = tuple(), tuple() 

654 

655 curve = [] 

656 if field == "ohlc": 

657 from mplfinance.original_flavor import candlestick_ohlc # pylint: disable=E0401 

658 ohlc = list(list(data.iloc[i, :4]) 

659 for i in range(0, data.shape[0])) 

660 ohlc = [[mdates.date2num(t)] + v for t, v in zip(dates, ohlc)] 

661 candlestick_ohlc(ax, ohlc, colorup="g") 

662 else: 

663 if label_prefix is None: 

664 label_prefix = "" 

665 add_args = {} 

666 if color: 

667 add_args['c'] = color 

668 for stock in data.columns: 

669 if axis == 2: 

670 curve.append( 

671 ax.plot(dates, data[stock], "r", linestyle='solid', 

672 label=label_prefix + str(stock), **add_args)) 

673 else: 

674 curve.append( 

675 ax.plot(dates, data[stock], linestyle='solid', c=color, 

676 label=label_prefix + str(stock), **add_args)) 

677 

678 if existing is None: 

679 ax.format_xdata = mdates.DateFormatter('%Y-%m-%d') 

680 if len(dates) < 30: 

681 days = mdates.DayLocator() 

682 ax.xaxis.set_major_locator(days) 

683 ax.xaxis.set_minor_locator(days) 

684 if date_format is not None: 

685 fmt = mdates.DateFormatter(date_format) 

686 ax.xaxis.set_major_formatter(fmt) 

687 else: 

688 ax.xaxis.set_major_formatter( 

689 mdates.DateFormatter("%Y-%m-%d")) 

690 elif len(dates) < 500: 

691 months = mdates.MonthLocator() 

692 days = mdates.DayLocator() 

693 ax.xaxis.set_major_locator(months) 

694 ax.xaxis.set_minor_locator(days) 

695 ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m")) 

696 if date_format is not None: 

697 fmt = mdates.DateFormatter(date_format) 

698 ax.xaxis.set_major_formatter(fmt) 

699 else: 

700 ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m")) 

701 else: 

702 years = mdates.YearLocator() 

703 months = mdates.MonthLocator() 

704 ax.xaxis.set_major_locator(years) 

705 ax.xaxis.set_minor_locator(months) 

706 if date_format is not None: 

707 fmt = mdates.DateFormatter(date_format) 

708 ax.xaxis.set_major_formatter(fmt) 

709 else: 

710 ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y")) 

711 

712 ax.set_xlim(begin, end) 

713 ax.format_ydata = price 

714 if fig is not None: 

715 fig.autofmt_xdate() 

716 

717 if axis == 2: 

718 if isinstance(curve, list): 

719 curve = [_[0] for _ in curve] 

720 ax.legend(ex_h + tuple(curve), ex_l + tuple(data.columns)) 

721 else: 

722 ax.grid(True) 

723 ax.legend(ex_l + tuple(data.columns)) 

724 

725 return ax 

726 

727 def to_csv(self, filename, sep="\t", index=False, **params): 

728 """ 

729 Saves the file in text format, 

730 see `to_csv <https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.to_csv.html>`_ 

731 

732 @param filename filename 

733 @param sep separator 

734 @param index to keep or drop the index 

735 @param params other parameters 

736 """ 

737 self.dataframe.to_csv(filename, sep=sep, index=index, **params) 

738 

739 def to_excel(self, excel_writer, **params): 

740 """ 

741 Saves the file in Excel format, 

742 see `to_excel <https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.to_excel.html>`_ 

743 """ 

744 self.dataframe.to_excel(excel_writer, **params)