@@ -85,14 +85,65 @@ def lightness(color, lightness=.94):
85
85
return color .to_rgb ()
86
86
87
87
88
+ _MAX_CANDLES = 10000
89
+
90
+
91
+ def _maybe_resample_data (resample_rule , df , indicators , equity_data , trades ):
92
+ if not resample_rule or len (df ) < _MAX_CANDLES :
93
+ return df , indicators , equity_data , trades
94
+
95
+ if isinstance (resample_rule , str ):
96
+ freq = resample_rule
97
+ else :
98
+ from_index = dict (day = - 2 , hour = - 6 , minute = 1 , second = 0 , millisecond = 0 ,
99
+ microsecond = 0 , nanosecond = 0 )[df .index .resolution ]
100
+ FREQS = ('1T' , '5T' , '10T' , '15T' , '30T' , '1H' , '2H' , '4H' , '8H' , '1D' , '1W' , '1M' )
101
+ freq = next ((f for f in FREQS [from_index :]
102
+ if len (df .resample (f )) <= _MAX_CANDLES ), FREQS [- 1 ])
103
+ warnings .warn ("Data contains too many candlesticks to plot; downsampling to {!r}. "
104
+ "See `Backtest.plot(resample=...)`" .format (freq ))
105
+
106
+ from .lib import OHLCV_AGG , TRADES_AGG , _EQUITY_AGG
107
+ df = df .resample (freq , label = 'right' ).agg (OHLCV_AGG ).dropna ()
108
+
109
+ # XXX: copy(True) pandas bug https://github.com/pandas-dev/pandas/issues/31710
110
+ indicators = [_Indicator (i .s .copy (True ).resample (freq ).mean ().dropna ().reindex (df .index ),
111
+ ** dict (i ._opts ,
112
+ # HACK: override `data` for its index
113
+ data = pd .Series (np .nan , index = df .index )))
114
+ for i in indicators ]
115
+ assert not indicators or indicators [0 ].s .index .equals (df .index )
116
+
117
+ equity_data = equity_data .resample (freq , label = 'right' ).agg (_EQUITY_AGG ).dropna (how = 'all' )
118
+ assert equity_data .index .equals (df .index )
119
+
120
+ def _weighted_returns (s , trades = trades ):
121
+ df = trades .loc [s .index ]
122
+ return ((df ['Size' ].abs () * df ['ReturnPct' ]) / df ['Size' ].abs ().sum ()).sum ()
123
+
124
+ trades = trades .assign (count = 1 ).resample (freq , on = 'ExitTime' , label = 'right' ).agg (dict (
125
+ TRADES_AGG ,
126
+ ReturnPct = _weighted_returns ,
127
+ count = 'sum' ,
128
+ # XXX: Can this prettier?
129
+ EntryBar = (lambda s , trades = trades , index = df .index :
130
+ index .get_loc (trades .loc [s .index ]['EntryTime' ].mean (), method = 'nearest' )),
131
+ ExitBar = (lambda s , trades = trades , index = df .index :
132
+ index .get_loc (trades .loc [s .index ]['ExitTime' ].mean (), method = 'nearest' )),
133
+ )).dropna ()
134
+
135
+ return df , indicators , equity_data , trades
136
+
137
+
88
138
def plot (* , results : pd .Series ,
89
139
df : pd .DataFrame ,
90
140
indicators : List [_Indicator ],
91
141
filename = '' , plot_width = None ,
92
142
plot_equity = True , plot_pl = True ,
93
143
plot_volume = True , plot_drawdown = False ,
94
144
smooth_equity = False , relative_equity = True ,
95
- superimpose = True , show_legend = True , open_browser = True ):
145
+ superimpose = True , resample = True ,
146
+ show_legend = True , open_browser = True ):
96
147
"""
97
148
Like much of GUI code everywhere, this is a mess.
98
149
"""
@@ -111,15 +162,19 @@ def plot(*, results: pd.Series,
111
162
trades = results ['_trades' ]
112
163
113
164
plot_volume = plot_volume and not df .Volume .isnull ().all ()
114
- time_resolution = getattr (df .index , 'resolution' , None )
115
165
is_datetime_index = df .index .is_all_dates
116
166
117
167
from .lib import OHLCV_AGG
118
168
# ohlc df may contain many columns. We're only interested in, and pass on to Bokeh, these
119
169
df = df [list (OHLCV_AGG .keys ())].copy (deep = False )
170
+
171
+ # Limit data to max_candles
172
+ if is_datetime_index :
173
+ df , indicators , equity_data , trades = _maybe_resample_data (
174
+ resample , df , indicators , equity_data , trades )
175
+
120
176
df .index .name = None # Provides source name @index
121
177
df ['datetime' ] = df .index # Save original, maybe datetime index
122
-
123
178
df = df .reset_index (drop = True )
124
179
equity_data = equity_data .reset_index (drop = True )
125
180
index = df .index
@@ -224,6 +279,10 @@ def _plot_equity_section():
224
279
x1 , x2 = dd_end - 1 , dd_end
225
280
y , y1 , y2 = equity [dd_start ], equity [x1 ], equity [x2 ]
226
281
dd_end -= (1 - (y - y1 ) / (y2 - y1 )) * (dd_end - x1 ) # y = a x + b
282
+ # If _plot_resample_data() was applied,
283
+ # the agg'd equity might have "stretched" the calculation
284
+ # XXX: test this?
285
+ dd_end = min (dd_end , equity .index [- 1 ])
227
286
228
287
if smooth_equity :
229
288
interest_points = pd .Index ([
@@ -321,11 +380,15 @@ def _plot_pl_section():
321
380
trade_source .add (returns_long , 'returns_long' )
322
381
trade_source .add (returns_short , 'returns_short' )
323
382
trade_source .add (size , 'marker_size' )
383
+ if 'count' in trades :
384
+ trade_source .add (trades ['count' ], 'count' )
324
385
r1 = fig .scatter ('index' , 'returns_long' , source = trade_source , fill_color = cmap ,
325
386
marker = 'triangle' , line_color = 'black' , size = 'marker_size' )
326
387
r2 = fig .scatter ('index' , 'returns_short' , source = trade_source , fill_color = cmap ,
327
388
marker = 'inverted_triangle' , line_color = 'black' , size = 'marker_size' )
328
389
tooltips = [("Size" , "@size{0,0}" )]
390
+ if 'count' in trades :
391
+ tooltips .append (("Count" , "@count{0,0}" ))
329
392
set_tooltips (fig , tooltips + [("P/L" , "@returns_long{+0.[000]%}" )],
330
393
vline = False , renderers = [r1 ])
331
394
set_tooltips (fig , tooltips + [("P/L" , "@returns_short{+0.[000]%}" )],
@@ -346,8 +409,9 @@ def _plot_volume_section():
346
409
347
410
def _plot_superimposed_ohlc ():
348
411
"""Superimposed, downsampled vbars"""
412
+ time_resolution = pd .DatetimeIndex (df ['datetime' ]).resolution
349
413
resample_rule = (superimpose if isinstance (superimpose , str ) else
350
- dict (day = 'W ' ,
414
+ dict (day = 'M ' ,
351
415
hour = 'D' ,
352
416
minute = 'H' ,
353
417
second = 'T' ,
0 commit comments