27
27
28
28
from . import utils
29
29
from .utils import APPROX_BDAYS_PER_MONTH , APPROX_BDAYS_PER_YEAR
30
+ from .utils import DAILY , WEEKLY , MONTHLY , YEARLY , ANNUALIZATION_FACTORS
30
31
from .interesting_periods import PERIODS
31
32
32
33
@@ -135,19 +136,21 @@ def aggregate_returns(df_daily_rets, convert_to):
135
136
def cumulate_returns (x ):
136
137
return cum_returns (x )[- 1 ]
137
138
138
- if convert_to == 'weekly' :
139
+ if convert_to == WEEKLY :
139
140
return df_daily_rets .groupby (
140
141
[lambda x : x .year ,
141
142
lambda x : x .month ,
142
143
lambda x : x .isocalendar ()[1 ]]).apply (cumulate_returns )
143
- elif convert_to == 'monthly' :
144
+ elif convert_to == MONTHLY :
144
145
return df_daily_rets .groupby (
145
146
[lambda x : x .year , lambda x : x .month ]).apply (cumulate_returns )
146
- elif convert_to == 'yearly' :
147
+ elif convert_to == YEARLY :
147
148
return df_daily_rets .groupby (
148
149
[lambda x : x .year ]).apply (cumulate_returns )
149
150
else :
150
- ValueError ('convert_to must be weekly, monthly or yearly' )
151
+ ValueError (
152
+ 'convert_to must be {}, {} or {}' .format (WEEKLY , MONTHLY , YEARLY )
153
+ )
151
154
152
155
153
156
def max_drawdown (returns ):
@@ -188,20 +191,24 @@ def max_drawdown(returns):
188
191
return - 1 * MDD
189
192
190
193
191
- def annual_return (returns , style = 'compound' ):
194
+ def annual_return (returns , style = 'compound' , period = DAILY ):
192
195
"""Determines the annual returns of a strategy.
193
196
194
197
Parameters
195
198
----------
196
199
returns : pd.Series
197
- Daily returns of the strategy, noncumulative.
200
+ Periodic returns of the strategy, noncumulative.
198
201
- See full explanation in tears.create_full_tear_sheet.
199
202
style : str, optional
200
203
- If 'compound', then return will be calculated in geometric
201
204
terms: (1+mean(all_daily_returns))^252 - 1.
202
205
- If 'calendar', then return will be calculated as
203
206
((last_value - start_value)/start_value)/num_of_years.
204
207
- Otherwise, return is simply mean(all_daily_returns)*252.
208
+ period : str, optional
209
+ - defines the periodicity of the 'returns' data for purposes of
210
+ annualizing. Can be 'monthly', 'weekly', or 'daily'
211
+ - defaults to 'daily'.
205
212
206
213
Returns
207
214
-------
@@ -213,27 +220,41 @@ def annual_return(returns, style='compound'):
213
220
if returns .size < 1 :
214
221
return np .nan
215
222
223
+ try :
224
+ ann_factor = ANNUALIZATION_FACTORS [period ]
225
+ except KeyError :
226
+ raise ValueError (
227
+ "period cannot be '{}'. "
228
+ "Must be '{}', '{}', or '{}'" .format (
229
+ period , DAILY , WEEKLY , MONTHLY
230
+ )
231
+ )
232
+
216
233
if style == 'calendar' :
217
- num_years = len (returns ) / APPROX_BDAYS_PER_YEAR
234
+ num_years = len (returns ) / ann_factor
218
235
df_cum_rets = cum_returns (returns , starting_value = 100 )
219
236
start_value = df_cum_rets [0 ]
220
237
end_value = df_cum_rets [- 1 ]
221
238
return ((end_value - start_value ) / start_value ) / num_years
222
239
if style == 'compound' :
223
- return pow ((1 + returns .mean ()), APPROX_BDAYS_PER_YEAR ) - 1
240
+ return pow ((1 + returns .mean ()), ann_factor ) - 1
224
241
else :
225
- return returns .mean () * APPROX_BDAYS_PER_YEAR
242
+ return returns .mean () * ann_factor
226
243
227
244
228
- def annual_volatility (returns ):
245
+ def annual_volatility (returns , period = DAILY ):
229
246
"""
230
247
Determines the annual volatility of a strategy.
231
248
232
249
Parameters
233
250
----------
234
251
returns : pd.Series
235
- Daily returns of the strategy, noncumulative.
252
+ Periodic returns of the strategy, noncumulative.
236
253
- See full explanation in tears.create_full_tear_sheet.
254
+ period : str, optional
255
+ - defines the periodicity of the 'returns' data for purposes of
256
+ annualizing volatility. Can be 'monthly' or 'weekly' or 'daily'.
257
+ - defaults to 'daily'
237
258
238
259
Returns
239
260
-------
@@ -244,10 +265,20 @@ def annual_volatility(returns):
244
265
if returns .size < 2 :
245
266
return np .nan
246
267
247
- return returns .std () * np .sqrt (APPROX_BDAYS_PER_YEAR )
268
+ try :
269
+ ann_factor = ANNUALIZATION_FACTORS [period ]
270
+ except KeyError :
271
+ raise ValueError (
272
+ "period cannot be: '{}'."
273
+ " Must be '{}', '{}', or '{}'" .format (
274
+ period , DAILY , WEEKLY , MONTHLY
275
+ )
276
+ )
277
+
278
+ return returns .std () * np .sqrt (ann_factor )
248
279
249
280
250
- def calmar_ratio (returns , returns_style = 'calendar' ):
281
+ def calmar_ratio (returns , returns_style = 'calendar' , period = DAILY ):
251
282
"""
252
283
Determines the Calmar ratio, or drawdown ratio, of a strategy.
253
284
@@ -258,6 +289,11 @@ def calmar_ratio(returns, returns_style='calendar'):
258
289
- See full explanation in tears.create_full_tear_sheet.
259
290
returns_style : str, optional
260
291
See annual_returns' style
292
+ period : str, optional
293
+ - defines the periodicity of the 'returns' data for purposes of
294
+ annualizing. Can be 'monthly', 'weekly', or 'daily'
295
+ - defaults to 'daily'.
296
+
261
297
262
298
Returns
263
299
-------
@@ -273,7 +309,9 @@ def calmar_ratio(returns, returns_style='calendar'):
273
309
if temp_max_dd < 0 :
274
310
temp = annual_return (
275
311
returns = returns ,
276
- style = returns_style ) / abs (max_drawdown (returns = returns ))
312
+ style = returns_style ,
313
+ period = period
314
+ ) / abs (max_drawdown (returns = returns ))
277
315
else :
278
316
return np .nan
279
317
@@ -321,7 +359,7 @@ def omega_ratio(returns, annual_return_threshhold=0.0):
321
359
return np .nan
322
360
323
361
324
- def sortino_ratio (returns , required_return = 0 ):
362
+ def sortino_ratio (returns , required_return = 0 , period = DAILY ):
325
363
326
364
"""
327
365
Determines the Sortino ratio of a strategy.
@@ -331,9 +369,14 @@ def sortino_ratio(returns, required_return=0):
331
369
returns : pd.Series or pd.DataFrame
332
370
Daily returns of the strategy, noncumulative.
333
371
- See full explanation in tears.create_full_tear_sheet.
334
-
372
+ returns_style : str, optional
373
+ See annual_returns' style
335
374
required_return: float / series
336
375
minimum acceptable return
376
+ period : str, optional
377
+ - defines the periodicity of the 'returns' data for purposes of
378
+ annualizing. Can be 'monthly', 'weekly', or 'daily'
379
+ - defaults to 'daily'.
337
380
338
381
Returns
339
382
-------
@@ -344,14 +387,24 @@ def sortino_ratio(returns, required_return=0):
344
387
Annualized Sortino ratio.
345
388
346
389
"""
390
+ try :
391
+ ann_factor = ANNUALIZATION_FACTORS [period ]
392
+ except KeyError :
393
+ raise ValueError (
394
+ "period cannot be: '{}'."
395
+ " Must be '{}', '{}', or '{}'" .format (
396
+ period , DAILY , WEEKLY , MONTHLY
397
+ )
398
+ )
399
+
347
400
mu = np .nanmean (returns - required_return , axis = 0 )
348
401
sortino = mu / downside_risk (returns , required_return )
349
402
if len (returns .shape ) == 2 :
350
403
sortino = pd .Series (sortino , index = returns .columns )
351
- return sortino * APPROX_BDAYS_PER_YEAR
404
+ return sortino * ann_factor
352
405
353
406
354
- def downside_risk (returns , required_return = 0 ):
407
+ def downside_risk (returns , required_return = 0 , period = DAILY ):
355
408
"""
356
409
Determines the downside deviation below a threshold
357
410
@@ -363,6 +416,10 @@ def downside_risk(returns, required_return=0):
363
416
364
417
required_return: float / series
365
418
minimum acceptable return
419
+ period : str, optional
420
+ - defines the periodicity of the 'returns' data for purposes of
421
+ annualizing. Can be 'monthly', 'weekly', or 'daily'
422
+ - defaults to 'daily'.
366
423
367
424
Returns
368
425
-------
@@ -373,18 +430,28 @@ def downside_risk(returns, required_return=0):
373
430
Annualized downside deviation
374
431
375
432
"""
433
+ try :
434
+ ann_factor = ANNUALIZATION_FACTORS [period ]
435
+ except KeyError :
436
+ raise ValueError (
437
+ "period cannot be: '{}'."
438
+ " Must be '{}', '{}', or '{}'" .format (
439
+ period , DAILY , WEEKLY , MONTHLY
440
+ )
441
+ )
442
+
376
443
downside_diff = returns - required_return
377
444
mask = downside_diff > 0
378
445
downside_diff [mask ] = 0.0
379
446
squares = np .square (downside_diff )
380
447
mean_squares = np .nanmean (squares , axis = 0 )
381
- dside_risk = np .sqrt (mean_squares ) * np .sqrt (APPROX_BDAYS_PER_YEAR )
448
+ dside_risk = np .sqrt (mean_squares ) * np .sqrt (ann_factor )
382
449
if len (returns .shape ) == 2 :
383
450
dside_risk = pd .Series (dside_risk , index = returns .columns )
384
451
return dside_risk
385
452
386
453
387
- def sharpe_ratio (returns , returns_style = 'compound' ):
454
+ def sharpe_ratio (returns , returns_style = 'compound' , period = DAILY ):
388
455
"""
389
456
Determines the Sharpe ratio of a strategy.
390
457
@@ -395,6 +462,10 @@ def sharpe_ratio(returns, returns_style='compound'):
395
462
- See full explanation in tears.create_full_tear_sheet.
396
463
returns_style : str, optional
397
464
See annual_returns' style
465
+ period : str, optional
466
+ - defines the periodicity of the 'returns' data for purposes of
467
+ annualizing. Can be 'monthly', 'weekly', or 'daily'
468
+ - defaults to 'daily'.
398
469
399
470
Returns
400
471
-------
@@ -406,8 +477,8 @@ def sharpe_ratio(returns, returns_style='compound'):
406
477
See https://en.wikipedia.org/wiki/Sharpe_ratio for more details.
407
478
"""
408
479
409
- numer = annual_return (returns , style = returns_style )
410
- denom = annual_volatility (returns )
480
+ numer = annual_return (returns , style = returns_style , period = period )
481
+ denom = annual_volatility (returns , period = period )
411
482
412
483
if denom > 0.0 :
413
484
return numer / denom
@@ -661,7 +732,8 @@ def calc_alpha_beta(returns, factor_returns):
661
732
def perf_stats (
662
733
returns ,
663
734
returns_style = 'compound' ,
664
- return_as_dict = False ):
735
+ return_as_dict = False ,
736
+ period = DAILY ):
665
737
"""Calculates various performance metrics of a strategy, for use in
666
738
plotting.show_perf_stats.
667
739
@@ -674,6 +746,10 @@ def perf_stats(
674
746
See annual_returns' style
675
747
return_as_dict : boolean, optional
676
748
If True, returns the computed metrics in a dictionary.
749
+ period : str, optional
750
+ - defines the periodicity of the 'returns' data for purposes of
751
+ annualizing. Can be 'monthly', 'weekly', or 'daily'
752
+ - defaults to 'daily'.
677
753
678
754
Returns
679
755
-------
@@ -685,14 +761,14 @@ def perf_stats(
685
761
all_stats = OrderedDict ()
686
762
all_stats ['annual_return' ] = annual_return (
687
763
returns ,
688
- style = returns_style )
689
- all_stats ['annual_volatility' ] = annual_volatility (returns )
764
+ style = returns_style , period = period )
765
+ all_stats ['annual_volatility' ] = annual_volatility (returns , period = period )
690
766
all_stats ['sharpe_ratio' ] = sharpe_ratio (
691
767
returns ,
692
- returns_style = returns_style )
768
+ returns_style = returns_style , period = period )
693
769
all_stats ['calmar_ratio' ] = calmar_ratio (
694
770
returns ,
695
- returns_style = returns_style )
771
+ returns_style = returns_style , period = period )
696
772
all_stats ['stability' ] = stability_of_timeseries (returns )
697
773
all_stats ['max_drawdown' ] = max_drawdown (returns )
698
774
all_stats ['omega_ratio' ] = omega_ratio (returns )
0 commit comments