1#!/usr/bin/env python 2# -*- coding: utf-8; py-indent-offset:4 -*- 3############################################################################### 4# 5# Copyright (C) 2015, 2016, 2017 Daniel Rodriguez 6# 7# This program is free software: you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program. If not, see <http://www.gnu.org/licenses/>. 19# 20############################################################################### 21from __future__ import (absolute_import, division, print_function, 22 unicode_literals) 23 24import bisect 25import collections 26import datetime 27import itertools 28import math 29import operator 30import sys 31 32import matplotlib 33import numpy as np # guaranteed by matplotlib 34import matplotlib.dates as mdates 35import matplotlib.font_manager as mfontmgr 36import matplotlib.legend as mlegend 37import matplotlib.ticker as mticker 38 39from ..utils.py3 import range, with_metaclass, string_types, integer_types 40from .. import AutoInfoClass, MetaParams, TimeFrame, date2num 41 42from .finance import plot_candlestick, plot_ohlc, plot_volume, plot_lineonclose 43from .formatters import (MyVolFormatter, MyDateFormatter, getlocator) 44from . import locator as loc 45from .multicursor import MultiCursor 46from .scheme import PlotScheme 47from .utils import tag_box_style 48 49 50class PInfo(object): 51 def __init__(self, sch): 52 self.sch = sch 53 self.nrows = 0 54 self.row = 0 55 self.clock = None 56 self.x = None 57 self.xlen = 0 58 self.sharex = None 59 self.figs = list() 60 self.cursors = list() 61 self.daxis = collections.OrderedDict() 62 self.vaxis = list() 63 self.zorder = dict() 64 self.coloridx = collections.defaultdict(lambda: -1) 65 self.handles = collections.defaultdict(list) 66 self.labels = collections.defaultdict(list) 67 self.legpos = collections.defaultdict(int) 68 69 self.prop = mfontmgr.FontProperties(size=self.sch.subtxtsize) 70 71 def newfig(self, figid, numfig, mpyplot): 72 fig = mpyplot.figure(figid + numfig) 73 self.figs.append(fig) 74 self.daxis = collections.OrderedDict() 75 self.vaxis = list() 76 self.row = 0 77 self.sharex = None 78 return fig 79 80 def nextcolor(self, ax): 81 self.coloridx[ax] += 1 82 return self.coloridx[ax] 83 84 def color(self, ax): 85 return self.sch.color(self.coloridx[ax]) 86 87 def zordernext(self, ax): 88 z = self.zorder[ax] 89 if self.sch.zdown: 90 return z * 0.9999 91 return z * 1.0001 92 93 def zordercur(self, ax): 94 return self.zorder[ax] 95 96 97class Plot_OldSync(with_metaclass(MetaParams, object)): 98 params = (('scheme', PlotScheme()),) 99 100 def __init__(self, **kwargs): 101 for pname, pvalue in kwargs.items(): 102 setattr(self.p.scheme, pname, pvalue) 103 104 def drawtag(self, ax, x, y, facecolor, edgecolor, alpha=0.9, **kwargs): 105 106 txt = ax.text(x, y, '%.2f' % y, va='center', ha='left', 107 fontsize=self.pinf.sch.subtxtsize, 108 bbox=dict(boxstyle=tag_box_style, 109 facecolor=facecolor, 110 edgecolor=edgecolor, 111 alpha=alpha), 112 # 3.0 is the minimum default for text 113 zorder=self.pinf.zorder[ax] + 3.0, 114 **kwargs) 115 116 def plot(self, strategy, figid=0, numfigs=1, iplot=True, 117 start=None, end=None, **kwargs): 118 # pfillers={}): 119 if not strategy.datas: 120 return 121 122 if not len(strategy): 123 return 124 125 if iplot: 126 if 'ipykernel' in sys.modules: 127 matplotlib.use('nbagg') 128 129 # this import must not happen before matplotlib.use 130 import matplotlib.pyplot as mpyplot 131 self.mpyplot = mpyplot 132 133 self.pinf = PInfo(self.p.scheme) 134 self.sortdataindicators(strategy) 135 self.calcrows(strategy) 136 137 st_dtime = strategy.lines.datetime.plot() 138 if start is None: 139 start = 0 140 if end is None: 141 end = len(st_dtime) 142 143 if isinstance(start, datetime.date): 144 start = bisect.bisect_left(st_dtime, date2num(start)) 145 146 if isinstance(end, datetime.date): 147 end = bisect.bisect_right(st_dtime, date2num(end)) 148 149 if end < 0: 150 end = len(st_dtime) + 1 + end # -1 = len() -2 = len() - 1 151 152 slen = len(st_dtime[start:end]) 153 d, m = divmod(slen, numfigs) 154 pranges = list() 155 for i in range(numfigs): 156 a = d * i + start 157 if i == (numfigs - 1): 158 d += m # add remainder to last stint 159 b = a + d 160 161 pranges.append([a, b, d]) 162 163 figs = [] 164 165 for numfig in range(numfigs): 166 # prepare a figure 167 fig = self.pinf.newfig(figid, numfig, self.mpyplot) 168 figs.append(fig) 169 170 self.pinf.pstart, self.pinf.pend, self.pinf.psize = pranges[numfig] 171 self.pinf.xstart = self.pinf.pstart 172 self.pinf.xend = self.pinf.pend 173 174 self.pinf.clock = strategy 175 self.pinf.xreal = self.pinf.clock.datetime.plot( 176 self.pinf.pstart, self.pinf.psize) 177 self.pinf.xlen = len(self.pinf.xreal) 178 self.pinf.x = list(range(self.pinf.xlen)) 179 # self.pinf.pfillers = {None: []} 180 # for key, val in pfillers.items(): 181 # pfstart = bisect.bisect_left(val, self.pinf.pstart) 182 # pfend = bisect.bisect_right(val, self.pinf.pend) 183 # self.pinf.pfillers[key] = val[pfstart:pfend] 184 185 # Do the plotting 186 # Things that go always at the top (observers) 187 self.pinf.xdata = self.pinf.x 188 for ptop in self.dplotstop: 189 self.plotind(None, ptop, subinds=self.dplotsover[ptop]) 190 191 # Create the rest on a per data basis 192 dt0, dt1 = self.pinf.xreal[0], self.pinf.xreal[-1] 193 for data in strategy.datas: 194 if not data.plotinfo.plot: 195 continue 196 197 self.pinf.xdata = self.pinf.x 198 xd = data.datetime.plotrange(self.pinf.xstart, self.pinf.xend) 199 if len(xd) < self.pinf.xlen: 200 self.pinf.xdata = xdata = [] 201 xreal = self.pinf.xreal 202 dts = data.datetime.plot() 203 xtemp = list() 204 for dt in (x for x in dts if dt0 <= x <= dt1): 205 dtidx = bisect.bisect_left(xreal, dt) 206 xdata.append(dtidx) 207 xtemp.append(dt) 208 209 self.pinf.xstart = bisect.bisect_left(dts, xtemp[0]) 210 self.pinf.xend = bisect.bisect_right(dts, xtemp[-1]) 211 212 for ind in self.dplotsup[data]: 213 self.plotind( 214 data, 215 ind, 216 subinds=self.dplotsover[ind], 217 upinds=self.dplotsup[ind], 218 downinds=self.dplotsdown[ind]) 219 220 self.plotdata(data, self.dplotsover[data]) 221 222 for ind in self.dplotsdown[data]: 223 self.plotind( 224 data, 225 ind, 226 subinds=self.dplotsover[ind], 227 upinds=self.dplotsup[ind], 228 downinds=self.dplotsdown[ind]) 229 230 cursor = MultiCursor( 231 fig.canvas, list(self.pinf.daxis.values()), 232 useblit=True, 233 horizOn=True, vertOn=True, 234 horizMulti=False, vertMulti=True, 235 horizShared=True, vertShared=False, 236 color='black', lw=1, ls=':') 237 238 self.pinf.cursors.append(cursor) 239 240 # Put the subplots as indicated by hspace 241 fig.subplots_adjust(hspace=self.pinf.sch.plotdist, 242 top=0.98, left=0.05, bottom=0.05, right=0.95) 243 244 laxis = list(self.pinf.daxis.values()) 245 246 # Find last axis which is not a twinx (date locator fails there) 247 i = -1 248 while True: 249 lastax = laxis[i] 250 if lastax not in self.pinf.vaxis: 251 break 252 253 i -= 1 254 255 self.setlocators(lastax) # place the locators/fmts 256 257 # Applying fig.autofmt_xdate if the data axis is the last one 258 # breaks the presentation of the date labels. why? 259 # Applying the manual rotation with setp cures the problem 260 # but the labels from all axis but the last have to be hidden 261 for ax in laxis: 262 self.mpyplot.setp(ax.get_xticklabels(), visible=False) 263 264 self.mpyplot.setp(lastax.get_xticklabels(), visible=True, 265 rotation=self.pinf.sch.tickrotation) 266 267 # Things must be tight along the x axis (to fill both ends) 268 axtight = 'x' if not self.pinf.sch.ytight else 'both' 269 self.mpyplot.autoscale(enable=True, axis=axtight, tight=True) 270 271 return figs 272 273 def setlocators(self, ax): 274 comp = getattr(self.pinf.clock, '_compression', 1) 275 tframe = getattr(self.pinf.clock, '_timeframe', TimeFrame.Days) 276 277 if self.pinf.sch.fmt_x_data is None: 278 if tframe == TimeFrame.Years: 279 fmtdata = '%Y' 280 elif tframe == TimeFrame.Months: 281 fmtdata = '%Y-%m' 282 elif tframe == TimeFrame.Weeks: 283 fmtdata = '%Y-%m-%d' 284 elif tframe == TimeFrame.Days: 285 fmtdata = '%Y-%m-%d' 286 elif tframe == TimeFrame.Minutes: 287 fmtdata = '%Y-%m-%d %H:%M' 288 elif tframe == TimeFrame.Seconds: 289 fmtdata = '%Y-%m-%d %H:%M:%S' 290 elif tframe == TimeFrame.MicroSeconds: 291 fmtdata = '%Y-%m-%d %H:%M:%S.%f' 292 elif tframe == TimeFrame.Ticks: 293 fmtdata = '%Y-%m-%d %H:%M:%S.%f' 294 else: 295 fmtdata = self.pinf.sch.fmt_x_data 296 297 fordata = MyDateFormatter(self.pinf.xreal, fmt=fmtdata) 298 for dax in self.pinf.daxis.values(): 299 dax.fmt_xdata = fordata 300 301 # Major locator / formatter 302 locmajor = loc.AutoDateLocator(self.pinf.xreal) 303 ax.xaxis.set_major_locator(locmajor) 304 if self.pinf.sch.fmt_x_ticks is None: 305 autofmt = loc.AutoDateFormatter(self.pinf.xreal, locmajor) 306 else: 307 autofmt = MyDateFormatter(self.pinf.xreal, 308 fmt=self.pinf.sch.fmt_x_ticks) 309 ax.xaxis.set_major_formatter(autofmt) 310 311 def calcrows(self, strategy): 312 # Calculate the total number of rows 313 rowsmajor = self.pinf.sch.rowsmajor 314 rowsminor = self.pinf.sch.rowsminor 315 nrows = 0 316 317 datasnoplot = 0 318 for data in strategy.datas: 319 if not data.plotinfo.plot: 320 # neither data nor indicators nor volume add rows 321 datasnoplot += 1 322 self.dplotsup.pop(data, None) 323 self.dplotsdown.pop(data, None) 324 self.dplotsover.pop(data, None) 325 326 else: 327 pmaster = data.plotinfo.plotmaster 328 if pmaster is data: 329 pmaster = None 330 if pmaster is not None: 331 # data doesn't add a row, but volume may 332 if self.pinf.sch.volume: 333 nrows += rowsminor 334 else: 335 # data adds rows, volume may 336 nrows += rowsmajor 337 if self.pinf.sch.volume and not self.pinf.sch.voloverlay: 338 nrows += rowsminor 339 340 if False: 341 # Datas and volumes 342 nrows += (len(strategy.datas) - datasnoplot) * rowsmajor 343 if self.pinf.sch.volume and not self.pinf.sch.voloverlay: 344 nrows += (len(strategy.datas) - datasnoplot) * rowsminor 345 346 # top indicators/observers 347 nrows += len(self.dplotstop) * rowsminor 348 349 # indicators above datas 350 nrows += sum(len(v) for v in self.dplotsup.values()) 351 nrows += sum(len(v) for v in self.dplotsdown.values()) 352 353 self.pinf.nrows = nrows 354 355 def newaxis(self, obj, rowspan): 356 ax = self.mpyplot.subplot2grid( 357 (self.pinf.nrows, 1), (self.pinf.row, 0), 358 rowspan=rowspan, sharex=self.pinf.sharex) 359 360 # update the sharex information if not available 361 if self.pinf.sharex is None: 362 self.pinf.sharex = ax 363 364 # update the row index with the taken rows 365 self.pinf.row += rowspan 366 367 # save the mapping indicator - axis and return 368 self.pinf.daxis[obj] = ax 369 370 # Activate grid in all axes if requested 371 ax.yaxis.tick_right() 372 ax.grid(self.pinf.sch.grid, which='both') 373 374 return ax 375 376 def plotind(self, iref, ind, 377 subinds=None, upinds=None, downinds=None, 378 masterax=None): 379 380 sch = self.p.scheme 381 382 # check subind 383 subinds = subinds or [] 384 upinds = upinds or [] 385 downinds = downinds or [] 386 387 # plot subindicators on self with independent axis above 388 for upind in upinds: 389 self.plotind(iref, upind) 390 391 # Get an axis for this plot 392 ax = masterax or self.newaxis(ind, rowspan=self.pinf.sch.rowsminor) 393 394 indlabel = ind.plotlabel() 395 396 # Scan lines quickly to find out if some lines have to be skipped for 397 # legend (because matplotlib reorders the legend) 398 toskip = 0 399 for lineidx in range(ind.size()): 400 line = ind.lines[lineidx] 401 linealias = ind.lines._getlinealias(lineidx) 402 lineplotinfo = getattr(ind.plotlines, '_%d' % lineidx, None) 403 if not lineplotinfo: 404 lineplotinfo = getattr(ind.plotlines, linealias, None) 405 if not lineplotinfo: 406 lineplotinfo = AutoInfoClass() 407 pltmethod = lineplotinfo._get('_method', 'plot') 408 if pltmethod != 'plot': 409 toskip += 1 - lineplotinfo._get('_plotskip', False) 410 411 if toskip >= ind.size(): 412 toskip = 0 413 414 for lineidx in range(ind.size()): 415 line = ind.lines[lineidx] 416 linealias = ind.lines._getlinealias(lineidx) 417 418 lineplotinfo = getattr(ind.plotlines, '_%d' % lineidx, None) 419 if not lineplotinfo: 420 lineplotinfo = getattr(ind.plotlines, linealias, None) 421 422 if not lineplotinfo: 423 lineplotinfo = AutoInfoClass() 424 425 if lineplotinfo._get('_plotskip', False): 426 continue 427 428 # Legend label only when plotting 1st line 429 if masterax and not ind.plotinfo.plotlinelabels: 430 label = indlabel * (not toskip) or '_nolegend' 431 else: 432 label = (indlabel + '\n') * (not toskip) 433 label += lineplotinfo._get('_name', '') or linealias 434 435 toskip -= 1 # one line less until legend can be added 436 437 # plot data 438 lplot = line.plotrange(self.pinf.xstart, self.pinf.xend) 439 440 # Global and generic for indicator 441 if self.pinf.sch.linevalues and ind.plotinfo.plotlinevalues: 442 plotlinevalue = lineplotinfo._get('_plotvalue', True) 443 if plotlinevalue and not math.isnan(lplot[-1]): 444 label += ' %.2f' % lplot[-1] 445 446 plotkwargs = dict() 447 linekwargs = lineplotinfo._getkwargs(skip_=True) 448 449 if linekwargs.get('color', None) is None: 450 if not lineplotinfo._get('_samecolor', False): 451 self.pinf.nextcolor(ax) 452 plotkwargs['color'] = self.pinf.color(ax) 453 454 plotkwargs.update(dict(aa=True, label=label)) 455 plotkwargs.update(**linekwargs) 456 457 if ax in self.pinf.zorder: 458 plotkwargs['zorder'] = self.pinf.zordernext(ax) 459 460 pltmethod = getattr(ax, lineplotinfo._get('_method', 'plot')) 461 462 xdata, lplotarray = self.pinf.xdata, lplot 463 if lineplotinfo._get('_skipnan', False): 464 # Get the full array and a mask to skipnan 465 lplotarray = np.array(lplot) 466 lplotmask = np.isfinite(lplotarray) 467 468 # Get both the axis and the data masked 469 lplotarray = lplotarray[lplotmask] 470 xdata = np.array(xdata)[lplotmask] 471 472 plottedline = pltmethod(xdata, lplotarray, **plotkwargs) 473 try: 474 plottedline = plottedline[0] 475 except: 476 # Possibly a container of artists (when plotting bars) 477 pass 478 479 self.pinf.zorder[ax] = plottedline.get_zorder() 480 481 vtags = lineplotinfo._get('plotvaluetags', True) 482 if self.pinf.sch.valuetags and vtags: 483 linetag = lineplotinfo._get('_plotvaluetag', True) 484 if linetag and not math.isnan(lplot[-1]): 485 # line has valid values, plot a tag for the last value 486 self.drawtag(ax, len(self.pinf.xreal), lplot[-1], 487 facecolor='white', 488 edgecolor=self.pinf.color(ax)) 489 490 farts = (('_gt', operator.gt), ('_lt', operator.lt), ('', None),) 491 for fcmp, fop in farts: 492 fattr = '_fill' + fcmp 493 fref, fcol = lineplotinfo._get(fattr, (None, None)) 494 if fref is not None: 495 y1 = np.array(lplot) 496 if isinstance(fref, integer_types): 497 y2 = np.full_like(y1, fref) 498 else: # string, naming a line, nothing else is supported 499 l2 = getattr(ind, fref) 500 prl2 = l2.plotrange(self.pinf.xstart, self.pinf.xend) 501 y2 = np.array(prl2) 502 kwargs = dict() 503 if fop is not None: 504 kwargs['where'] = fop(y1, y2) 505 506 falpha = self.pinf.sch.fillalpha 507 if isinstance(fcol, (list, tuple)): 508 fcol, falpha = fcol 509 510 ax.fill_between(self.pinf.xdata, y1, y2, 511 facecolor=fcol, 512 alpha=falpha, 513 interpolate=True, 514 **kwargs) 515 516 # plot subindicators that were created on self 517 for subind in subinds: 518 self.plotind(iref, subind, subinds=self.dplotsover[subind], 519 masterax=ax) 520 521 if not masterax: 522 # adjust margin if requested ... general of particular 523 ymargin = ind.plotinfo._get('plotymargin', 0.0) 524 ymargin = max(ymargin, self.pinf.sch.yadjust) 525 if ymargin: 526 ax.margins(y=ymargin) 527 528 # Set specific or generic ticks 529 yticks = ind.plotinfo._get('plotyticks', []) 530 if not yticks: 531 yticks = ind.plotinfo._get('plotyhlines', []) 532 533 if yticks: 534 ax.set_yticks(yticks) 535 else: 536 locator = mticker.MaxNLocator(nbins=4, prune='both') 537 ax.yaxis.set_major_locator(locator) 538 539 # Set specific hlines if asked to 540 hlines = ind.plotinfo._get('plothlines', []) 541 if not hlines: 542 hlines = ind.plotinfo._get('plotyhlines', []) 543 for hline in hlines: 544 ax.axhline(hline, color=self.pinf.sch.hlinescolor, 545 ls=self.pinf.sch.hlinesstyle, 546 lw=self.pinf.sch.hlineswidth) 547 548 if self.pinf.sch.legendind and \ 549 ind.plotinfo._get('plotlegend', True): 550 551 handles, labels = ax.get_legend_handles_labels() 552 # Ensure that we have something to show 553 if labels: 554 # location can come from the user 555 loc = ind.plotinfo.legendloc or self.pinf.sch.legendindloc 556 557 # Legend done here to ensure it includes all plots 558 legend = ax.legend(loc=loc, 559 numpoints=1, frameon=False, 560 shadow=False, fancybox=False, 561 prop=self.pinf.prop) 562 563 # legend.set_title(indlabel, prop=self.pinf.prop) 564 # hack: if title is set. legend has a Vbox for the labels 565 # which has a default "center" set 566 legend._legend_box.align = 'left' 567 568 # plot subindicators on self with independent axis below 569 for downind in downinds: 570 self.plotind(iref, downind) 571 572 def plotvolume(self, data, opens, highs, lows, closes, volumes, label): 573 pmaster = data.plotinfo.plotmaster 574 if pmaster is data: 575 pmaster = None 576 voloverlay = (self.pinf.sch.voloverlay and pmaster is None) 577 578 # if sefl.pinf.sch.voloverlay: 579 if voloverlay: 580 rowspan = self.pinf.sch.rowsmajor 581 else: 582 rowspan = self.pinf.sch.rowsminor 583 584 ax = self.newaxis(data.volume, rowspan=rowspan) 585 586 # if self.pinf.sch.voloverlay: 587 if voloverlay: 588 volalpha = self.pinf.sch.voltrans 589 else: 590 volalpha = 1.0 591 592 maxvol = volylim = max(volumes) 593 if maxvol: 594 595 # Plot the volume (no matter if as overlay or standalone) 596 vollabel = label 597 volplot, = plot_volume(ax, self.pinf.xdata, opens, closes, volumes, 598 colorup=self.pinf.sch.volup, 599 colordown=self.pinf.sch.voldown, 600 alpha=volalpha, label=vollabel) 601 602 nbins = 6 603 prune = 'both' 604 # if self.pinf.sch.voloverlay: 605 if voloverlay: 606 # store for a potential plot over it 607 nbins = int(nbins / self.pinf.sch.volscaling) 608 prune = None 609 610 volylim /= self.pinf.sch.volscaling 611 ax.set_ylim(0, volylim, auto=True) 612 else: 613 # plot a legend 614 handles, labels = ax.get_legend_handles_labels() 615 if handles: 616 617 # location can come from the user 618 loc = data.plotinfo.legendloc or self.pinf.sch.legendindloc 619 620 # Legend done here to ensure it includes all plots 621 legend = ax.legend(loc=loc, 622 numpoints=1, frameon=False, 623 shadow=False, fancybox=False, 624 prop=self.pinf.prop) 625 626 locator = mticker.MaxNLocator(nbins=nbins, prune=prune) 627 ax.yaxis.set_major_locator(locator) 628 ax.yaxis.set_major_formatter(MyVolFormatter(maxvol)) 629 630 if not maxvol: 631 ax.set_yticks([]) 632 return None 633 634 return volplot 635 636 def plotdata(self, data, indicators): 637 for ind in indicators: 638 upinds = self.dplotsup[ind] 639 for upind in upinds: 640 self.plotind(data, upind, 641 subinds=self.dplotsover[upind], 642 upinds=self.dplotsup[upind], 643 downinds=self.dplotsdown[upind]) 644 645 opens = data.open.plotrange(self.pinf.xstart, self.pinf.xend) 646 highs = data.high.plotrange(self.pinf.xstart, self.pinf.xend) 647 lows = data.low.plotrange(self.pinf.xstart, self.pinf.xend) 648 closes = data.close.plotrange(self.pinf.xstart, self.pinf.xend) 649 volumes = data.volume.plotrange(self.pinf.xstart, self.pinf.xend) 650 651 vollabel = 'Volume' 652 pmaster = data.plotinfo.plotmaster 653 if pmaster is data: 654 pmaster = None 655 656 datalabel = '' 657 if hasattr(data, '_name') and data._name: 658 datalabel += data._name 659 660 voloverlay = (self.pinf.sch.voloverlay and pmaster is None) 661 662 if not voloverlay: 663 vollabel += ' ({})'.format(datalabel) 664 665 # if self.pinf.sch.volume and self.pinf.sch.voloverlay: 666 axdatamaster = None 667 if self.pinf.sch.volume and voloverlay: 668 volplot = self.plotvolume( 669 data, opens, highs, lows, closes, volumes, vollabel) 670 axvol = self.pinf.daxis[data.volume] 671 ax = axvol.twinx() 672 self.pinf.daxis[data] = ax 673 self.pinf.vaxis.append(ax) 674 else: 675 if pmaster is None: 676 ax = self.newaxis(data, rowspan=self.pinf.sch.rowsmajor) 677 elif getattr(data.plotinfo, 'sameaxis', False): 678 axdatamaster = self.pinf.daxis[pmaster] 679 ax = axdatamaster 680 else: 681 axdatamaster = self.pinf.daxis[pmaster] 682 ax = axdatamaster.twinx() 683 self.pinf.vaxis.append(ax) 684 685 if hasattr(data, '_compression') and \ 686 hasattr(data, '_timeframe'): 687 tfname = TimeFrame.getname(data._timeframe, data._compression) 688 datalabel += ' (%d %s)' % (data._compression, tfname) 689 690 plinevalues = getattr(data.plotinfo, 'plotlinevalues', True) 691 if self.pinf.sch.style.startswith('line'): 692 if self.pinf.sch.linevalues and plinevalues: 693 datalabel += ' C:%.2f' % closes[-1] 694 695 if axdatamaster is None: 696 color = self.pinf.sch.loc 697 else: 698 self.pinf.nextcolor(axdatamaster) 699 color = self.pinf.color(axdatamaster) 700 701 plotted = plot_lineonclose( 702 ax, self.pinf.xdata, closes, 703 color=color, label=datalabel) 704 else: 705 if self.pinf.sch.linevalues and plinevalues: 706 datalabel += ' O:%.2f H:%.2f L:%.2f C:%.2f' % \ 707 (opens[-1], highs[-1], lows[-1], closes[-1]) 708 if self.pinf.sch.style.startswith('candle'): 709 plotted = plot_candlestick( 710 ax, self.pinf.xdata, opens, highs, lows, closes, 711 colorup=self.pinf.sch.barup, 712 colordown=self.pinf.sch.bardown, 713 label=datalabel, 714 alpha=self.pinf.sch.baralpha, 715 fillup=self.pinf.sch.barupfill, 716 filldown=self.pinf.sch.bardownfill) 717 718 elif self.pinf.sch.style.startswith('bar') or True: 719 # final default option -- should be "else" 720 plotted = plot_ohlc( 721 ax, self.pinf.xdata, opens, highs, lows, closes, 722 colorup=self.pinf.sch.barup, 723 colordown=self.pinf.sch.bardown, 724 label=datalabel) 725 726 self.pinf.zorder[ax] = plotted[0].get_zorder() 727 728 # Code to place a label at the right hand side with the last value 729 vtags = data.plotinfo._get('plotvaluetags', True) 730 if self.pinf.sch.valuetags and vtags: 731 self.drawtag(ax, len(self.pinf.xreal), closes[-1], 732 facecolor='white', edgecolor=self.pinf.sch.loc) 733 734 ax.yaxis.set_major_locator(mticker.MaxNLocator(prune='both')) 735 # make sure "over" indicators do not change our scale 736 if data.plotinfo._get('plotylimited', True): 737 if axdatamaster is None: 738 ax.set_ylim(ax.get_ylim()) 739 740 if self.pinf.sch.volume: 741 # if not self.pinf.sch.voloverlay: 742 if not voloverlay: 743 self.plotvolume( 744 data, opens, highs, lows, closes, volumes, vollabel) 745 else: 746 # Prepare overlay scaling/pushup or manage own axis 747 if self.pinf.sch.volpushup: 748 # push up overlaid axis by lowering the bottom limit 749 axbot, axtop = ax.get_ylim() 750 axbot *= (1.0 - self.pinf.sch.volpushup) 751 ax.set_ylim(axbot, axtop) 752 753 for ind in indicators: 754 self.plotind(data, ind, subinds=self.dplotsover[ind], masterax=ax) 755 756 handles, labels = ax.get_legend_handles_labels() 757 a = axdatamaster or ax 758 if handles: 759 # put data and volume legend entries in the 1st positions 760 # because they are "collections" they are considered after Line2D 761 # for the legend entries, which is not our desire 762 # if self.pinf.sch.volume and self.pinf.sch.voloverlay: 763 764 ai = self.pinf.legpos[a] 765 if self.pinf.sch.volume and voloverlay: 766 if volplot: 767 # even if volume plot was requested, there may be no volume 768 labels.insert(ai, vollabel) 769 handles.insert(ai, volplot) 770 771 didx = labels.index(datalabel) 772 labels.insert(ai, labels.pop(didx)) 773 handles.insert(ai, handles.pop(didx)) 774 775 if axdatamaster is None: 776 self.pinf.handles[ax] = handles 777 self.pinf.labels[ax] = labels 778 else: 779 self.pinf.handles[axdatamaster] = handles 780 self.pinf.labels[axdatamaster] = labels 781 # self.pinf.handles[axdatamaster].extend(handles) 782 # self.pinf.labels[axdatamaster].extend(labels) 783 784 h = self.pinf.handles[a] 785 l = self.pinf.labels[a] 786 787 axlegend = a 788 loc = data.plotinfo.legendloc or self.pinf.sch.legenddataloc 789 legend = axlegend.legend(h, l, 790 loc=loc, 791 frameon=False, shadow=False, 792 fancybox=False, prop=self.pinf.prop, 793 numpoints=1, ncol=1) 794 795 # hack: if title is set. legend has a Vbox for the labels 796 # which has a default "center" set 797 legend._legend_box.align = 'left' 798 799 for ind in indicators: 800 downinds = self.dplotsdown[ind] 801 for downind in downinds: 802 self.plotind(data, downind, 803 subinds=self.dplotsover[downind], 804 upinds=self.dplotsup[downind], 805 downinds=self.dplotsdown[downind]) 806 807 self.pinf.legpos[a] = len(self.pinf.handles[a]) 808 809 if data.plotinfo._get('plotlog', False): 810 a = axdatamaster or ax 811 a.set_yscale('log') 812 813 def show(self): 814 self.mpyplot.show() 815 816 def savefig(self, fig, filename, width=16, height=9, dpi=300, tight=True): 817 fig.set_size_inches(width, height) 818 bbox_inches = 'tight' * tight or None 819 fig.savefig(filename, dpi=dpi, bbox_inches=bbox_inches) 820 821 def sortdataindicators(self, strategy): 822 # These lists/dictionaries hold the subplots that go above each data 823 self.dplotstop = list() 824 self.dplotsup = collections.defaultdict(list) 825 self.dplotsdown = collections.defaultdict(list) 826 self.dplotsover = collections.defaultdict(list) 827 828 # Sort observers in the different lists/dictionaries 829 for x in strategy.getobservers(): 830 if not x.plotinfo.plot or x.plotinfo.plotskip: 831 continue 832 833 if x.plotinfo.subplot: 834 self.dplotstop.append(x) 835 else: 836 key = getattr(x._clock, 'owner', x._clock) 837 self.dplotsover[key].append(x) 838 839 # Sort indicators in the different lists/dictionaries 840 for x in strategy.getindicators(): 841 if not hasattr(x, 'plotinfo'): 842 # no plotting support - so far LineSingle derived classes 843 continue 844 845 if not x.plotinfo.plot or x.plotinfo.plotskip: 846 continue 847 848 x._plotinit() # will be plotted ... call its init function 849 850 # support LineSeriesStub which has "owner" to point to the data 851 key = getattr(x._clock, 'owner', x._clock) 852 if key is strategy: # a LinesCoupler 853 key = strategy.data 854 855 if getattr(x.plotinfo, 'plotforce', False): 856 if key not in strategy.datas: 857 datas = strategy.datas 858 while True: 859 if key not in strategy.datas: 860 key = key._clock 861 else: 862 break 863 864 xpmaster = x.plotinfo.plotmaster 865 if xpmaster is x: 866 xpmaster = None 867 if xpmaster is not None: 868 key = xpmaster 869 870 if x.plotinfo.subplot and xpmaster is None: 871 if x.plotinfo.plotabove: 872 self.dplotsup[key].append(x) 873 else: 874 self.dplotsdown[key].append(x) 875 else: 876 self.dplotsover[key].append(x) 877 878 879Plot = Plot_OldSync 880