@@ -85,14 +85,72 @@ 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 isinstance (resample_rule , str ):
93
+ freq = resample_rule
94
+ else :
95
+ if len (df ) < _MAX_CANDLES :
96
+ return df , indicators , equity_data , trades
97
+
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
+ indicators = [_Indicator (i .df .resample (freq , label = 'right' ).mean ()
110
+ .dropna ().reindex (df .index ).values .T ,
111
+ ** dict (i ._opts , name = i .name ,
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 ].df .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
+ def _group_trades (column ):
125
+ def f (s , new_index = df .index .astype (np .int64 ), bars = trades [column ]):
126
+ if s .size :
127
+ # Via int64 because on pandas recently broken datetime
128
+ mean_time = int (bars .loc [s .index ].view ('i8' ).mean ())
129
+ new_bar_idx = new_index .get_loc (mean_time , method = 'nearest' )
130
+ return new_bar_idx
131
+ return f
132
+
133
+ if len (trades ): # Avoid pandas "resampling on Int64 index" error
134
+ trades = trades .assign (count = 1 ).resample (freq , on = 'ExitTime' , label = 'right' ).agg (dict (
135
+ TRADES_AGG ,
136
+ ReturnPct = _weighted_returns ,
137
+ count = 'sum' ,
138
+ EntryBar = _group_trades ('EntryTime' ),
139
+ ExitBar = _group_trades ('ExitTime' ),
140
+ )).dropna ()
141
+
142
+ return df , indicators , equity_data , trades
143
+
144
+
88
145
def plot (* , results : pd .Series ,
89
146
df : pd .DataFrame ,
90
147
indicators : List [_Indicator ],
91
148
filename = '' , plot_width = None ,
92
149
plot_equity = True , plot_pl = True ,
93
150
plot_volume = True , plot_drawdown = False ,
94
151
smooth_equity = False , relative_equity = True ,
95
- superimpose = True , show_legend = True , open_browser = True ):
152
+ superimpose = True , resample = True ,
153
+ show_legend = True , open_browser = True ):
96
154
"""
97
155
Like much of GUI code everywhere, this is a mess.
98
156
"""
@@ -111,15 +169,19 @@ def plot(*, results: pd.Series,
111
169
trades = results ['_trades' ]
112
170
113
171
plot_volume = plot_volume and not df .Volume .isnull ().all ()
114
- time_resolution = getattr (df .index , 'resolution' , None )
115
172
is_datetime_index = df .index .is_all_dates
116
173
117
174
from .lib import OHLCV_AGG
118
175
# ohlc df may contain many columns. We're only interested in, and pass on to Bokeh, these
119
176
df = df [list (OHLCV_AGG .keys ())].copy (deep = False )
177
+
178
+ # Limit data to max_candles
179
+ if is_datetime_index :
180
+ df , indicators , equity_data , trades = _maybe_resample_data (
181
+ resample , df , indicators , equity_data , trades )
182
+
120
183
df .index .name = None # Provides source name @index
121
184
df ['datetime' ] = df .index # Save original, maybe datetime index
122
-
123
185
df = df .reset_index (drop = True )
124
186
equity_data = equity_data .reset_index (drop = True )
125
187
index = df .index
@@ -224,6 +286,10 @@ def _plot_equity_section():
224
286
x1 , x2 = dd_end - 1 , dd_end
225
287
y , y1 , y2 = equity [dd_start ], equity [x1 ], equity [x2 ]
226
288
dd_end -= (1 - (y - y1 ) / (y2 - y1 )) * (dd_end - x1 ) # y = a x + b
289
+ # If _plot_resample_data() was applied,
290
+ # the agg'd equity might have "stretched" the calculation
291
+ # XXX: test this?
292
+ dd_end = min (dd_end , equity .index [- 1 ])
227
293
228
294
if smooth_equity :
229
295
interest_points = pd .Index ([
@@ -321,11 +387,15 @@ def _plot_pl_section():
321
387
trade_source .add (returns_long , 'returns_long' )
322
388
trade_source .add (returns_short , 'returns_short' )
323
389
trade_source .add (size , 'marker_size' )
390
+ if 'count' in trades :
391
+ trade_source .add (trades ['count' ], 'count' )
324
392
r1 = fig .scatter ('index' , 'returns_long' , source = trade_source , fill_color = cmap ,
325
393
marker = 'triangle' , line_color = 'black' , size = 'marker_size' )
326
394
r2 = fig .scatter ('index' , 'returns_short' , source = trade_source , fill_color = cmap ,
327
395
marker = 'inverted_triangle' , line_color = 'black' , size = 'marker_size' )
328
396
tooltips = [("Size" , "@size{0,0}" )]
397
+ if 'count' in trades :
398
+ tooltips .append (("Count" , "@count{0,0}" ))
329
399
set_tooltips (fig , tooltips + [("P/L" , "@returns_long{+0.[000]%}" )],
330
400
vline = False , renderers = [r1 ])
331
401
set_tooltips (fig , tooltips + [("P/L" , "@returns_short{+0.[000]%}" )],
@@ -346,8 +416,9 @@ def _plot_volume_section():
346
416
347
417
def _plot_superimposed_ohlc ():
348
418
"""Superimposed, downsampled vbars"""
419
+ time_resolution = pd .DatetimeIndex (df ['datetime' ]).resolution
349
420
resample_rule = (superimpose if isinstance (superimpose , str ) else
350
- dict (day = 'W ' ,
421
+ dict (day = 'M ' ,
351
422
hour = 'D' ,
352
423
minute = 'H' ,
353
424
second = 'T' ,
0 commit comments