Module pinkfish.pfstatistics
Calculate trading statistics.
The stats() function returns the following metrics in a pd.Series.
-
start
: str The date when trading begins formatted as YY-MM-DD. -
end
: str The date when trading ends formatted as YY-MM-DD. -
beginning_balance
: int The initial capital. -
ending_balance
: float The ending capital. -
total_net_profit
: float Total value of all profitable trades minus all losing trades. -
gross_profit
: float Total value of all profitable trades. -
gross_loss
: float Total value of all losing trades. -
profit_factor
: float The Ratio of the total profits from profitable trades divided by the total loses from losing trades. A break-even system has a profit factor of 1. -
return_on_initial_capital
: float The ratio of gross profit divided by the initial capital and multiplied by 100. -
annual_return_rate
: float The compound annual growth rate of the strategy. -
trading_period
: str The trading time frame expressed as years, monthe, and days. -
pct_time_in_market
: float The percentage of days in which the strategy is not completely holding cash. -
margin
: float The buying power in dollars divided by the capital. For example, if the margin is 2 and the capital is $10,000, then the buying power is $20,000. -
avg_leverage
: float Leverage is the total value of securities held plus any cash, divided by the total value of securities held plus cash minus loans. The average leverage is just the average daily leverage over the life of the strategy. -
max_leverage
: float The maximum daily leverage over the life of the strategy. -
min_leverage
: floatpppppppppppppppppppppppppp The minimum daily leverage over the life of the strategy. -
total_num_trades
: int The number of closed trades. -
trades_per_year
: float The average number of closed trades per year. -
num_winning_trades
: int The number of profitable trades. -
num_losing_trades
: int The number of losing trades. -
num_even_trades
: int The number of break even trades. -
pct_profitable_trades
: float The number of winning trades divided by the total number of closed trades and multiplied by 100. -
avg_profit_per_trade
: float The total net profit divided by the total number of closed trades and multiplied by 100. -
avg_profit_per_winning_trade
: float The gross profit divided by the number of winning trades. -
avg_loss_per_losing_trade
: float The gross loss divided by the number of losing trades. This quantity is negative. -
ratio_avg_profit_win_loss
: float The absolute value of the average profit per winning trade divided by the average loss per losing trade. -
largest_profit_winning_trade
: float The single largest profit for all winning trades. -
largest_loss_losing_trade
: float The single largest loss for all losing trades. -
num_winning_points
: float The sum of the increase in points from all winning trades. -
num_losing_points
: float The sum of the decrease in points from all losing trades. This quantity is negative. -
total_net_points
: float The mathematical difference between winning points and losing points. -
avg_points
: float The total net points divided by the total number of trades. -
largest_points_winning_trade
: float The single largest point increase for all winning trades. -
largest_points_losing_trade
: float The single largest point decrease for all losing trades. -
avg_pct_gain_per_trade
: float The average percentage gain for all trades. -
largest_pct_winning_trade
: float The single largest percent increase for all winning trades. -
largest_pct_losing_trade
: float The single largest percent decrease for all losing trades. -
expected_shortfall
: float The expected shortfall is calculated by taking the average of returns in the worst 5% of cases. In other words, it is the average percent loss of the worst 5% of losing trades. -
max_consecutive_winning_trades
: int The longest winning streak in trades. -
max_consecutive_losing_trades
: int The longest losing streak in trades. -
avg_bars_winning_trades
: float On average, how long a winning trade takes in market days. -
avg_bars_losing_trades
: float On average, how long a losing trade takes in market days. -
max_closed_out_drawdown
: float Worst peak minus trough balance based on closing prices. -
max_closed_out_drawdown_peak_date
: str The beginning and peak date of the largest drawdown formatted as YY-MM-DD. The balance hit it's highest point on this date. -
max_closed_out_drawdown_trough_date
: str The trough date of the largest drawdown. The balance hit it's lowest point on this date. -
max_closed_out_drawdown_recovery_date
: str The end date of the largest drawdown. The date in which the balance has equaled the peak value again. -
drawdown_loss_period
: int The number of calendar days from peak to trough. -
drawdown_recovery_period
: int The number of calendar days from trough to recovery. -
annualized_return_over_max_drawdown
: float Annual return rate divided by the max drawdown. -
max_intra_day_drawdown
: float Worst peak minus trough balance based on intraday values. -
avg_yearly_closed_out_drawdown
:float The average yearly drawdown calculated using every available market year period. In other words, every rollowing window of 252 market days is taken as a different year in the calculation. -
max_yearly_closed_out_drawdown
: float Worst peak minus trough balance based on closing prices during any 252 market day period. -
avg_monthly_closed_out_drawdown
: float The average monthly drawdown calculated using every available market month period. In other words, every rollowing window of 20 market days is taken as a different month in the calculation. -
max_monthly_closed_out_drawdown
: float Worst peak minus trough balance based on closing prices during any 20 market day period. -
avg_weekly_closed_out_drawdown
: float The average weekly drawdown calculated using every available market week period. In other words, every rollowing window of 5 market days is taken as a different week in the calculation. -
max_weekly_closed_out_drawdown
: float Worst peak minus trough balance based on closing prices during any 5 market day period. -
avg_yearly_closed_out_runup
: float The average yearly runup calculated using every available market year period. In other words, every rollowing window of 252 market days is taken as a different year in the calculation. -
max_yearly_closed_out_runup
: float Best peak minus trough balance based on closing prices during any 252 market day period. -
avg_monthly_closed_out_runup
: float The average monthly runup calculated using every available market month period. In other words, every rollowing window of 20 market days is taken as a different month in the calculation. -
max_monthly_closed_out_runup
: float Best peak minus trough balance based on closing prices during any 20 market day period. -
avg_weekly_closed_out_runup
: float The average weekly runup calculated using every available market week period. In other words, every rollowing window of 5 market days is taken as a different week in the calculation. -
max_weekly_closed_out_runup
: float Best peak minus trough balance based on closing prices during any 5 market day period. -
pct_profitable_years
: float The percentage of all years that were profitable. In other words, the percentage of 252 market day periods that were profitable. -
best_year
: float The percentage increase in balance of the best year. -
worst_year
: float The percentage decrease in balance of the worst year. -
avg_year
: float The percentage change per year on average. -
annual_std
: float The yearly standard deviation over the entire trading period. -
pct_profitable_months
: float The percentage of all months that were profitable. In other words, the percentage of 20 market day periods that were profitable. -
best_month
: float The percentage increase in balance of the best month. -
worst_month
: float The percentage decrease in balance of the worst month. -
avg_month
: float The percentage change per month on average. -
monthly_std
: float The monthly standard deviation over the entire trading period. -
pct_profitable_weeks
: float The percentage of all weeks that were profitable. In other words, the percentage of 5 market day periods that were profitable. -
best_week
: float The percentage increase in balance of the best week. -
worst_week
: float The percentage decrease in balance of the worst week. -
avg_week
: float The percentage change per week on average. -
weekly_std
: float The weekly standard deviation over the entire trading period. -
pct_profitable_weeks
: float The percentage of all weeks that were profitable. In other words, the percentage of 5 market day periods that were profitable. -
weekly_std
: float The weekly standard deviation over the entire trading period. -
pct_profitable_days
: float The percentage of all days that were profitable. -
best_day
: float The percentage increase in balance of the best day. -
worst_day
: float The percentage decrease in balance of the worst day. -
avg_day
: float The percentage change per day on average. -
daily_std
: float The daily standard deviation over the entire trading period. -
sharpe_ratio
: float A measure of risk adjusted return. The ratio is the average return per unit of volatility, i.e. standard deviation. -
sharpe_ratio_max
: float The maximum expected sharpe ratio. It is the sharpe ratio plus 3 standard deviations of the sharpe ratio. 99.73% of sharpe ratios are theoretically below this value. -
sharpe_ratio_min
: float The mimimum expected sharpe ratio. It is the sharpe ratio minus 3 standard deviations of the sharpe ratio. 99.73% of sharpe ratios are theoretically above this value. -
sortino_ratio
: float A variation of the Sharpe ratio that differentiates harmful volatility from overall volatility by using the asset's standard deviation of negative portfolio returns (downside deviation) instead of the total standard deviation.
Expand source code
"""
Calculate trading statistics.
The stats() function returns the following metrics in a pd.Series.
- `start` : str
The date when trading begins formatted as YY-MM-DD.
- `end` : str
The date when trading ends formatted as YY-MM-DD.
- `beginning_balance` : int
The initial capital.
- `ending_balance` : float
The ending capital.
- `total_net_profit` : float
Total value of all profitable trades minus all losing trades.
- `gross_profit` : float
Total value of all profitable trades.
- `gross_loss` : float
Total value of all losing trades.
- `profit_factor` : float
The Ratio of the total profits from profitable trades divided by
the total loses from losing trades. A break-even system has a
profit factor of 1.
- `return_on_initial_capital` : float
The ratio of gross profit divided by the initial capital and
multiplied by 100.
- `annual_return_rate` : float
The compound annual growth rate of the strategy.
- `trading_period` : str
The trading time frame expressed as years, monthe, and days.
- `pct_time_in_market` : float
The percentage of days in which the strategy is not completely
holding cash.
- `margin` : float
The buying power in dollars divided by the capital. For example,
if the margin is 2 and the capital is $10,000, then the buying
power is $20,000.
- `avg_leverage` : float
Leverage is the total value of securities held plus any cash,
divided by the total value of securities held plus cash minus
loans. The average leverage is just the average daily leverage
over the life of the strategy.
- `max_leverage` : float
The maximum daily leverage over the life of the strategy.
- `min_leverage` : floatpppppppppppppppppppppppppp
The minimum daily leverage over the life of the strategy.
- `total_num_trades` : int
The number of closed trades.
- `trades_per_year` : float
The average number of closed trades per year.
- `num_winning_trades` : int
The number of profitable trades.
- `num_losing_trades` : int
The number of losing trades.
- `num_even_trades` : int
The number of break even trades.
- `pct_profitable_trades` : float
The number of winning trades divided by the total number of closed
trades and multiplied by 100.
- `avg_profit_per_trade` : float
The total net profit divided by the total number of closed trades
and multiplied by 100.
- `avg_profit_per_winning_trade` : float
The gross profit divided by the number of winning trades.
- `avg_loss_per_losing_trade` : float
The gross loss divided by the number of losing trades. This
quantity is negative.
- `ratio_avg_profit_win_loss` : float
The absolute value of the average profit per winning trade divided
by the average loss per losing trade.
- `largest_profit_winning_trade` : float
The single largest profit for all winning trades.
- `largest_loss_losing_trade` : float
The single largest loss for all losing trades.
- `num_winning_points` : float
The sum of the increase in points from all winning trades.
- `num_losing_points` : float
The sum of the decrease in points from all losing trades. This
quantity is negative.
- `total_net_points` : float
The mathematical difference between winning points and
losing points.
- `avg_points` : float
The total net points divided by the total number of trades.
- `largest_points_winning_trade` : float
The single largest point increase for all winning trades.
- `largest_points_losing_trade` : float
The single largest point decrease for all losing trades.
- `avg_pct_gain_per_trade` : float
The average percentage gain for all trades.
- `largest_pct_winning_trade` : float
The single largest percent increase for all winning trades.
- `largest_pct_losing_trade` : float
The single largest percent decrease for all losing trades.
- `expected_shortfall` : float
The expected shortfall is calculated by taking the average of
returns in the worst 5% of cases. In other words, it is the
average percent loss of the worst 5% of losing trades.
- `max_consecutive_winning_trades` : int
The longest winning streak in trades.
- `max_consecutive_losing_trades` : int
The longest losing streak in trades.
- `avg_bars_winning_trades` : float
On average, how long a winning trade takes in market days.
- `avg_bars_losing_trades` : float
On average, how long a losing trade takes in market days.
- `max_closed_out_drawdown` : float
Worst peak minus trough balance based on closing prices.
- `max_closed_out_drawdown_peak_date` : str
The beginning and peak date of the largest drawdown formatted
as YY-MM-DD. The balance hit it's highest point on this date.
- `max_closed_out_drawdown_trough_date` : str
The trough date of the largest drawdown. The balance hit it's
lowest point on this date.
- `max_closed_out_drawdown_recovery_date` : str
The end date of the largest drawdown. The date in which the
balance has equaled the peak value again.
- `drawdown_loss_period` : int
The number of calendar days from peak to trough.
- `drawdown_recovery_period` : int
The number of calendar days from trough to recovery.
- `annualized_return_over_max_drawdown` : float
Annual return rate divided by the max drawdown.
- `max_intra_day_drawdown` : float
Worst peak minus trough balance based on intraday values.
- `avg_yearly_closed_out_drawdown` :float
The average yearly drawdown calculated using every available
market year period. In other words, every rollowing window of 252
market days is taken as a different year in the calculation.
- `max_yearly_closed_out_drawdown` : float
Worst peak minus trough balance based on closing prices during any
252 market day period.
- `avg_monthly_closed_out_drawdown` : float
The average monthly drawdown calculated using every available
market month period. In other words, every rollowing window of 20
market days is taken as a different month in the calculation.
- `max_monthly_closed_out_drawdown` : float
Worst peak minus trough balance based on closing prices during any
20 market day period.
- `avg_weekly_closed_out_drawdown` : float
The average weekly drawdown calculated using every available
market week period. In other words, every rollowing window of 5
market days is taken as a different week in the calculation.
- `max_weekly_closed_out_drawdown` : float
Worst peak minus trough balance based on closing prices during any
5 market day period.
- `avg_yearly_closed_out_runup` : float
The average yearly runup calculated using every available
market year period. In other words, every rollowing window of 252
market days is taken as a different year in the calculation.
- `max_yearly_closed_out_runup` : float
Best peak minus trough balance based on closing prices during any
252 market day period.
- `avg_monthly_closed_out_runup` : float
The average monthly runup calculated using every available
market month period. In other words, every rollowing window of 20
market days is taken as a different month in the calculation.
- `max_monthly_closed_out_runup` : float
Best peak minus trough balance based on closing prices during any
20 market day period.
- `avg_weekly_closed_out_runup` : float
The average weekly runup calculated using every available
market week period. In other words, every rollowing window of 5
market days is taken as a different week in the calculation.
- `max_weekly_closed_out_runup` : float
Best peak minus trough balance based on closing prices during any
5 market day period.
- `pct_profitable_years` : float
The percentage of all years that were profitable. In other words,
the percentage of 252 market day periods that were profitable.
- `best_year` : float
The percentage increase in balance of the best year.
- `worst_year` : float
The percentage decrease in balance of the worst year.
- `avg_year` : float
The percentage change per year on average.
- `annual_std` : float
The yearly standard deviation over the entire trading period.
- `pct_profitable_months` : float
The percentage of all months that were profitable. In other words,
the percentage of 20 market day periods that were profitable.
- `best_month` : float
The percentage increase in balance of the best month.
- `worst_month` : float
The percentage decrease in balance of the worst month.
- `avg_month` : float
The percentage change per month on average.
- `monthly_std` : float
The monthly standard deviation over the entire trading period.
- `pct_profitable_weeks` : float
The percentage of all weeks that were profitable. In other words,
the percentage of 5 market day periods that were profitable.
- `best_week` : float
The percentage increase in balance of the best week.
- `worst_week` : float
The percentage decrease in balance of the worst week.
- `avg_week` : float
The percentage change per week on average.
- `weekly_std` : float
The weekly standard deviation over the entire trading period.
- `pct_profitable_weeks` : float
The percentage of all weeks that were profitable. In other words,
the percentage of 5 market day periods that were profitable.
- `weekly_std` : float
The weekly standard deviation over the entire trading period.
- `pct_profitable_days` : float
The percentage of all days that were profitable.
- `best_day` : float
The percentage increase in balance of the best day.
- `worst_day` : float
The percentage decrease in balance of the worst day.
- `avg_day` : float
The percentage change per day on average.
- `daily_std` : float
The daily standard deviation over the entire trading period.
- `sharpe_ratio` : float
A measure of risk adjusted return. The ratio is the average return
per unit of volatility, i.e. standard deviation.
- `sharpe_ratio_max` : float
The maximum expected sharpe ratio. It is the sharpe ratio plus
3 standard deviations of the sharpe ratio. 99.73% of sharpe ratios
are theoretically below this value.
- `sharpe_ratio_min` : float
The mimimum expected sharpe ratio. It is the sharpe ratio minus
3 standard deviations of the sharpe ratio. 99.73% of sharpe ratios
are theoretically above this value.
- `sortino_ratio` : float
A variation of the Sharpe ratio that differentiates harmful
volatility from overall volatility by using the asset's standard
deviation of negative portfolio returns (downside deviation)
instead of the total standard deviation.
"""
from datetime import datetime
from dateutil.relativedelta import relativedelta
import math
import operator
import sys
import numpy as np
from numpy.lib.stride_tricks import as_strided
import pandas as pd
import pinkfish.trade as trade
import pinkfish.utility as utility
# This is a reference to the module object instance itself.
__m = sys.modules[__name__]
########################################################################
# CONSTANTS
TRADING_DAYS_PER_YEAR = 252
"""
int : The number of trading days per year.
"""
TRADING_DAYS_PER_MONTH = 20
"""
int : The number of trading days per month.
"""
TRADING_DAYS_PER_WEEK = 5
"""
int : The number of trading days per week.
"""
ALPHA_BEGIN = (1900, 1, 1)
"""
tuple : Use with `select_timeseries`, beginning data for any timeseries.
"""
SP500_BEGIN = (1957, 3, 4)
"""
tuple : Use with `select_timeseries`, date the S&P500 began.
"""
########################################################################
# TRADING DAYS
def select_trading_days(use_stock_market_calendar):
"""
Select between continuous and standard stock market days.
Set use_stock_market_calendar=False if your timeseries is 7 days
a week, e.g. cryptocurrencies.
Parameters
----------
use_stock_market_calendar : bool
True for standard stock market calendar. False for trading
7 days a week.
Returns
-------
None
"""
if use_stock_market_calendar:
__m.TRADING_DAYS_PER_YEAR = 252
__m.TRADING_DAYS_PER_MONTH = 20
__m.TRADING_DAYS_PER_WEEK = 5
else:
__m.TRADING_DAYS_PER_YEAR = 365
__m.TRADING_DAYS_PER_MONTH = 30
__m.TRADING_DAYS_PER_WEEK = 7
def get_trading_days():
"""
Returns the number of trading days per year, month, and week.
"""
return (__m.TRADING_DAYS_PER_YEAR,
__m.TRADING_DAYS_PER_MONTH,
__m.TRADING_DAYS_PER_WEEK)
########################################################################
# OVERALL RESULTS
def _beginning_balance(capital):
return capital
def _ending_balance(dbal):
return dbal.iloc[-1]['close']
@utility.no_empty_container('tlog', 0)
def _total_net_profit(tlog):
return tlog.iloc[-1]['cumul_total']
@utility.no_empty_container('tlog', 0)
def _gross_profit(tlog):
return tlog[tlog['pl_cash'] > 0]['pl_cash'].sum()
@utility.no_empty_container('tlog', 0)
def _gross_loss(tlog):
return tlog[tlog['pl_cash'] < 0]['pl_cash'].sum()
def _profit_factor(tlog):
if _gross_profit(tlog) == 0: return 0
if _gross_loss(tlog) == 0: return 1000
return _gross_profit(tlog) / _gross_loss(tlog) * -1
def _return_on_initial_capital(tlog, capital):
return _total_net_profit(tlog) / capital * 100
def _difference_in_years(start, end):
diff = abs(start - end)
diff_in_years = (diff.days + diff.seconds/86400)/365.2425
return diff_in_years
def _cagr(B, A, n):
if B < 0: B = 0
return (math.pow(B / A, 1 / n) - 1) * 100
def _annual_return_rate(end_balance, capital, start, end):
B = end_balance
A = capital
n = _difference_in_years(start, end)
return _cagr(B, A, n)
def _trading_period(start, end):
diff = relativedelta(end, start)
return f'{diff.years} years {diff.months} months {diff.days} days'
def _total_days_in_market(dbal):
n = (dbal['shares'] > 0).sum()
if dbal.iloc[-2]['shares'] > 0:
n += 1
return n
def _pct_time_in_market(dbal):
return _total_days_in_market(dbal) / len(dbal) * 100
########################################################################
# LEVERAGE
def _margin():
return trade.TradeLog.margin
def _avg_leverage(dbal):
return dbal['leverage'].mean()
def _max_leverage(dbal):
return dbal['leverage'].max()
def _min_leverage(dbal):
return dbal['leverage'].min()
########################################################################
# SUMS
def _total_num_trades(tlog):
return len(tlog)
def _trades_per_year(tlog, start, end):
diff = relativedelta(end, start)
years = diff.years + diff.months/12 + diff.days/365
return _total_num_trades(tlog) / years
@utility.no_empty_container('tlog', 0)
def _num_winning_trades(tlog):
return (tlog['pl_cash'] > 0).sum()
@utility.no_empty_container('tlog', 0)
def _num_losing_trades(tlog):
return (tlog['pl_cash'] < 0).sum()
@utility.no_empty_container('tlog', 0)
def _num_even_trades(tlog):
return (tlog['pl_cash'] == 0).sum()
def _pct_profitable_trades(tlog):
if _total_num_trades(tlog) == 0: return 0
return _num_winning_trades(tlog) / _total_num_trades(tlog) * 100
########################################################################
# CASH PROFITS AND LOSSES
def _avg_profit_per_trade(tlog):
if _total_num_trades(tlog) == 0: return 0
return _total_net_profit(tlog) / _total_num_trades(tlog)
def _avg_profit_per_winning_trade(tlog):
if _num_winning_trades(tlog) == 0: return 0
return _gross_profit(tlog) / _num_winning_trades(tlog)
def _avg_loss_per_losing_trade(tlog):
if _num_losing_trades(tlog) == 0: return 0
return _gross_loss(tlog) / _num_losing_trades(tlog)
def _ratio_avg_profit_win_loss(tlog):
if _avg_profit_per_winning_trade(tlog) == 0: return 0
if _avg_loss_per_losing_trade(tlog) == 0: return 1000
return (_avg_profit_per_winning_trade(tlog) /
_avg_loss_per_losing_trade(tlog) * -1)
def _largest_profit_winning_trade(tlog):
if _num_winning_trades(tlog) == 0: return 0
return tlog[tlog['pl_cash'] > 0].max()['pl_cash']
def _largest_loss_losing_trade(tlog):
if _num_losing_trades(tlog) == 0: return 0
return tlog[tlog['pl_cash'] < 0].min()['pl_cash']
########################################################################
# POINTS
def _num_winning_points(tlog):
if _num_winning_trades(tlog) == 0: return 0
return tlog[tlog['pl_points'] > 0]['pl_points'].sum()
def _num_losing_points(tlog):
if _num_losing_trades(tlog) == 0: return 0
return tlog[tlog['pl_points'] < 0]['pl_points'].sum()
def _total_net_points(tlog):
return _num_winning_points(tlog) + _num_losing_points(tlog)
@utility.no_empty_container('tlog', 0)
def _avg_points(tlog):
return tlog['pl_points'].sum() / len(tlog.index)
def _largest_points_winning_trade(tlog):
if _num_winning_trades(tlog) == 0: return 0
return tlog[tlog['pl_points'] > 0].max()['pl_points']
def _largest_points_losing_trade(tlog):
if _num_losing_trades(tlog) == 0: return 0
return tlog[tlog['pl_points'] < 0].min()['pl_points']
@utility.no_empty_container('tlog', 0)
def _avg_pct_gain_per_trade(tlog):
s = tlog['pl_points'] / tlog['entry_price']
return np.average(s) * 100
def _largest_pct_winning_trade(tlog):
if _num_winning_trades(tlog) == 0: return 0
df = tlog[tlog['pl_points'] > 0]
s = df['pl_points'] / df['entry_price']
return s.max() * 100
def _largest_pct_losing_trade(tlog):
if _num_losing_trades(tlog) == 0: return 0
df = tlog[tlog['pl_points'] < 0]
s = df['pl_points'] / df['entry_price']
return s.min() * 100
@utility.no_empty_container('tlog', 0)
def _expected_shortfall(tlog):
df = tlog[tlog['pl_points'] < 0]
s = df['pl_points'] / df['entry_price']
l = sorted(s)
end = int(len(l) * .05)
avg = np.mean(l[:end]) *100 if end > 0 else 0
return avg
########################################################################
# STREAKS
def _subsequence(s, c):
"""
Calculate the length of a subsequence
Takes as parameter list like object `s` and returns the length of
the longest subsequence of `s` constituted only by consecutive
character `c`s.
Example: If the string passed as parameter is "001000111100",
and `c` is '0', then the longest subsequence of only '0's has
length 3.
"""
count = 0
maxlen = 0
for bit in s:
if bit == c:
count += 1
if count > maxlen:
maxlen = count
else:
count = 0
return maxlen
def _max_consecutive_winning_trades(tlog):
if _num_winning_trades(tlog) == 0: return 0
return _subsequence(tlog['pl_cash'] > 0, True)
def _max_consecutive_losing_trades(tlog):
if _num_losing_trades(tlog) == 0: return 0
return _subsequence(tlog['pl_cash'] > 0, False)
@utility.no_empty_container('tlog', [])
def _get_trade_bars(ts, tlog, op):
l = []
for row in tlog.itertuples():
if op(row.pl_cash, 0):
l.append(len(ts[row.entry_date:row.exit_date].index))
return l
def _avg_bars_winning_trades(ts, tlog):
if _num_winning_trades(tlog) == 0: return 0
return np.average(_get_trade_bars(ts, tlog, operator.gt))
def _avg_bars_losing_trades(ts, tlog):
if _num_losing_trades(tlog) == 0: return 0
return np.average(_get_trade_bars(ts, tlog, operator.lt))
########################################################################
# DRAWDOWN AND RUNUP
def _max_closed_out_drawdown(close):
"""
Only compare each point to the previous running peak O(N).
"""
running_max = pd.Series(close).expanding(min_periods=1).max()
cur_dd = (close - running_max) / running_max * 100
dd_max = min(0, cur_dd.min())
idx = cur_dd.idxmin()
dd = pd.Series(dtype='object')
dd['max'] = dd_max
dd['peak'] = running_max[idx]
dd['trough'] = close[idx]
dd['peak_date'] = close[close == dd['peak']].index[0].strftime('%Y-%m-%d')
dd['trough_date'] = idx.strftime('%Y-%m-%d')
close = close[close.index > idx]
rd_mask = close > dd['peak']
if rd_mask.any():
dd['recovery_date'] = close[rd_mask].index[0].strftime('%Y-%m-%d')
else:
dd['recovery_date'] = 'Not Recovered Yet'
return dd
def _max_intra_day_drawdown(high, low):
"""
Only compare each point to the previous running peak O(N).
"""
running_max = pd.Series(high).expanding(min_periods=1).max()
cur_dd = (low - running_max) / running_max * 100
dd_max = min(0, cur_dd.min())
idx = cur_dd.idxmin()
dd = pd.Series(dtype='object')
dd['max'] = dd_max
dd['peak'] = running_max[idx]
dd['trough'] = low[idx]
dd['peak_date'] = high[high == dd['peak']].index[0].strftime('%Y-%m-%d')
dd['trough_date'] = idx.strftime('%Y-%m-%d')
high = high[high.index > idx]
rd_mask = high > dd['peak']
if rd_mask.any():
dd['recovery_date'] = high[rd_mask].index[0].strftime('%Y-%m-%d')
else:
dd['recovery_date'] = 'Not Recovered Yet'
return dd
def _drawdown_loss_recovery_period(peak_date, trough_date , recovery_date):
if recovery_date == 'Not Recovered Yet':
loss_period = recovery_period = recovery_date
else:
peak_date = datetime.strptime(peak_date, '%Y-%m-%d')
trough_date = datetime.strptime(trough_date, '%Y-%m-%d')
recovery_date = datetime.strptime(recovery_date, '%Y-%m-%d')
loss_period = abs(peak_date - trough_date).days
recovery_period = abs(trough_date-recovery_date).days
return loss_period, recovery_period
def _windowed_view(x, window_size):
"""
Create a 2d windowed view of a 1d array.
`x` must be a 1d numpy array.
`numpy.lib.stride_tricks.as_strided` is used to create the view.
The data is not copied.
Example:
>>> x = np.array([1, 2, 3, 4, 5, 6])
>>> _windowed_view(x, 3)
array([[1, 2, 3],
[2, 3, 4],
[3, 4, 5],
[4, 5, 6]])
"""
y = as_strided(x, shape=(x.size - window_size + 1, window_size),
strides=(x.strides[0], x.strides[0]))
return y
def _rolling_max_dd(ser, period, min_periods=1):
"""
Compute the rolling maximum drawdown of `ser`.
`ser` must be a Series.
`min_periods` should satisfy 1 <= min_periods <= window_size.
Returns an 1d array with length len(x) - min_periods + 1.
"""
window_size = period + 1
x = ser.values
if min_periods < window_size:
pad = np.empty(window_size - min_periods)
pad.fill(x[0])
x = np.concatenate((pad, x))
y = _windowed_view(x, window_size)
running_max_y = np.maximum.accumulate(y, axis=1)
dd = (y - running_max_y) / running_max_y * 100
rmdd = dd.min(axis=1)
return pd.Series(data=rmdd, index=ser.index, name=ser.name)
def _rolling_max_ru(ser, period, min_periods=1):
"""
Compute the rolling maximum runup of `ser`.
`ser` must be a Series.
`min_periods` should satisfy 1 <= min_periods <= window_size.
Returns an 1d array with length len(x) - min_periods + 1.
"""
window_size = period + 1
x = ser.values
if min_periods < window_size:
pad = np.empty(window_size - min_periods)
pad.fill(x[0])
x = np.concatenate((pad, x))
y = _windowed_view(x, window_size)
running_min_y = np.minimum.accumulate(y, axis=1)
ru = (y - running_min_y) / running_min_y * 100
rmru = ru.max(axis=1)
return pd.Series(data=rmru, index=ser.index, name=ser.name)
########################################################################
# PERCENT CHANGE - used to compute several stastics
def _pct_change(close, period):
diff = (close.shift(-period) - close) / close * 100
diff.dropna(inplace=True)
return diff
########################################################################
# RATIOS
def _sharpe_ratio(rets, risk_free=0.00, period=TRADING_DAYS_PER_YEAR):
dev = np.std(rets, axis=0)
mean = np.mean(rets, axis=0)
if math.isclose(dev, 0):
sharpe = 0
else:
sharpe = (mean*period - risk_free) / (dev * np.sqrt(period))
return sharpe
def _sortino_ratio(rets, risk_free=0.00, period=TRADING_DAYS_PER_YEAR):
mean = np.mean(rets, axis=0)
negative_rets = rets[rets < 0]
if len(negative_rets) == 0:
dev = 0
else:
dev = np.std(negative_rets, axis=0)
if math.isclose(dev, 0):
sortino = 0
else:
sortino = (mean*period - risk_free) / (dev * np.sqrt(period))
return sortino
########################################################################
# STATS - this is the primary call used to generate the results
def stats(ts, tlog, dbal, capital):
"""
Compute trading stats.
Parameters
----------
ts : pd.DataFrame
The timeseries of a symbol.
tlog : pd.DataFrame
The trade log.
dbal : pd.DataFrame
The daily balance.
capital : int
The amount of money available for trading.
Examples
--------
>>> stats = pf.stats(ts, tlog, dbal, capital)
Returns
-------
stats : pd.Series
The statistics for the strategy.
"""
start = ts.index[0]
end = ts.index[-1]
stats = pd.Series(dtype='object')
# OVERALL RESULTS
stats['start'] = start.strftime('%Y-%m-%d')
stats['end'] = end.strftime('%Y-%m-%d')
stats['beginning_balance'] = _beginning_balance(capital)
stats['ending_balance'] = _ending_balance(dbal)
stats['total_net_profit'] = _total_net_profit(tlog)
stats['gross_profit'] = _gross_profit(tlog)
stats['gross_loss'] = _gross_loss(tlog)
stats['profit_factor'] = _profit_factor(tlog)
stats['return_on_initial_capital'] = _return_on_initial_capital(tlog, capital)
cagr = _annual_return_rate(dbal['close'].iloc[-1], capital, start, end)
stats['annual_return_rate'] = cagr
stats['trading_period'] = _trading_period(start, end)
stats['pct_time_in_market'] = _pct_time_in_market(dbal)
# LEVERAGE
stats['margin'] = _margin()
stats['avg_leverage'] = _avg_leverage(dbal)
stats['max_leverage'] = _max_leverage(dbal)
stats['min_leverage'] = _min_leverage(dbal)
# SUMS
stats['total_num_trades'] = _total_num_trades(tlog)
stats['trades_per_year'] = _trades_per_year(tlog, start, end)
stats['num_winning_trades'] = _num_winning_trades(tlog)
stats['num_losing_trades'] = _num_losing_trades(tlog)
stats['num_even_trades'] = _num_even_trades(tlog)
stats['pct_profitable_trades'] = _pct_profitable_trades(tlog)
# CASH PROFITS AND LOSSES
stats['avg_profit_per_trade'] = _avg_profit_per_trade(tlog)
stats['avg_profit_per_winning_trade'] = _avg_profit_per_winning_trade(tlog)
stats['avg_loss_per_losing_trade'] = _avg_loss_per_losing_trade(tlog)
stats['ratio_avg_profit_win_loss'] = _ratio_avg_profit_win_loss(tlog)
stats['largest_profit_winning_trade'] = _largest_profit_winning_trade(tlog)
stats['largest_loss_losing_trade'] = _largest_loss_losing_trade(tlog)
# POINTS
stats['num_winning_points'] = _num_winning_points(tlog)
stats['num_losing_points'] = _num_losing_points(tlog)
stats['total_net_points'] = _total_net_points(tlog)
stats['avg_points'] = _avg_points(tlog)
stats['largest_points_winning_trade'] = _largest_points_winning_trade(tlog)
stats['largest_points_losing_trade'] = _largest_points_losing_trade(tlog)
stats['avg_pct_gain_per_trade'] = _avg_pct_gain_per_trade(tlog)
stats['largest_pct_winning_trade'] = _largest_pct_winning_trade(tlog)
stats['largest_pct_losing_trade'] = _largest_pct_losing_trade(tlog)
stats['expected_shortfall'] = _expected_shortfall(tlog)
# STREAKS
stats['max_consecutive_winning_trades'] = _max_consecutive_winning_trades(tlog)
stats['max_consecutive_losing_trades'] = _max_consecutive_losing_trades(tlog)
stats['avg_bars_winning_trades'] = _avg_bars_winning_trades(ts, tlog)
stats['avg_bars_losing_trades'] = _avg_bars_losing_trades(ts, tlog)
# DRAWDOWN
dd = _max_closed_out_drawdown(dbal['close'])
stats['max_closed_out_drawdown'] = dd['max']
stats['max_closed_out_drawdown_peak_date'] = dd['peak_date']
stats['max_closed_out_drawdown_trough_date'] = dd['trough_date']
stats['max_closed_out_drawdown_recovery_date'] = dd['recovery_date']
stats['drawdown_loss_period'], stats['drawdown_recovery_period'] = \
_drawdown_loss_recovery_period(dd['peak_date'], dd['trough_date'],
dd['recovery_date'])
if dd['max'] == 0:
stats['annualized_return_over_max_drawdown'] = 0
else:
stats['annualized_return_over_max_drawdown'] = abs(cagr / dd['max'])
dd = _max_intra_day_drawdown(dbal['high'], dbal['low'])
stats['max_intra_day_drawdown'] = dd['max']
dd = _rolling_max_dd(dbal['close'], TRADING_DAYS_PER_YEAR)
stats['avg_yearly_closed_out_drawdown'] = np.average(dd)
stats['max_yearly_closed_out_drawdown'] = min(dd)
dd = _rolling_max_dd(dbal['close'], TRADING_DAYS_PER_MONTH)
stats['avg_monthly_closed_out_drawdown'] = np.average(dd)
stats['max_monthly_closed_out_drawdown'] = min(dd)
dd = _rolling_max_dd(dbal['close'], TRADING_DAYS_PER_WEEK)
stats['avg_weekly_closed_out_drawdown'] = np.average(dd)
stats['max_weekly_closed_out_drawdown'] = min(dd)
# RUNUP
ru = _rolling_max_ru(dbal['close'], TRADING_DAYS_PER_YEAR)
stats['avg_yearly_closed_out_runup'] = np.average(ru)
stats['max_yearly_closed_out_runup'] = ru.max()
ru = _rolling_max_ru(dbal['close'], TRADING_DAYS_PER_MONTH)
stats['avg_monthly_closed_out_runup'] = np.average(ru)
stats['max_monthly_closed_out_runup'] = max(ru)
ru = _rolling_max_ru(dbal['close'], TRADING_DAYS_PER_WEEK)
stats['avg_weekly_closed_out_runup'] = np.average(ru)
stats['max_weekly_closed_out_runup'] = max(ru)
# PERCENT CHANGE
pc = _pct_change(dbal['close'], TRADING_DAYS_PER_YEAR)
if len(pc) > 0:
stats['pct_profitable_years'] = (pc > 0).sum() / len(pc) * 100
stats['best_year'] = pc.max()
stats['worst_year'] = pc.min()
stats['avg_year'] = np.average(pc)
stats['annual_std'] = pc.std()
pc = _pct_change(dbal['close'], TRADING_DAYS_PER_MONTH)
if len(pc) > 0:
stats['pct_profitable_months'] = (pc > 0).sum() / len(pc) * 100
stats['best_month'] = pc.max()
stats['worst_month'] = pc.min()
stats['avg_month'] = np.average(pc)
stats['monthly_std'] = pc.std()
pc = _pct_change(dbal['close'], TRADING_DAYS_PER_WEEK)
if len(pc) > 0:
stats['pct_profitable_weeks'] = (pc > 0).sum() / len(pc) * 100
stats['best_week'] = pc.max()
stats['worst_week'] = pc.min()
stats['avg_week'] = np.average(pc)
stats['weekly_std'] = pc.std()
pc = _pct_change(dbal['close'], 1)
if len(pc) > 0:
stats['pct_profitable_days'] = (pc > 0).sum() / len(pc) * 100
stats['best_day'] = pc.max()
stats['worst_day'] = pc.min()
stats['avg_day'] = np.average(pc)
stats['daily_std'] = pc.std()
# RATIOS
sr = _sharpe_ratio(dbal['close'].pct_change())
sr_std = math.sqrt((1 + 0.5*sr**2) / len(dbal))
stats['sharpe_ratio'] = sr
stats['sharpe_ratio_max'] = sr + 3*sr_std #3 std=>99.73%
stats['sharpe_ratio_min'] = sr - 3*sr_std
stats['sortino_ratio'] = _sortino_ratio(dbal['close'].pct_change())
return stats
########################################################################
# SUMMARY - stats() must be called before calling summary()
def currency(amount):
"""
Returns the dollar amount in US currency format.
Parameters
----------
amount : float
The dollar amount to convert.
Returns
-------
str
the dollar amount in US currency format.
"""
if amount >= 0:
return f'${amount:,.2f}'
else:
return f'-${-amount:,.2f}'
default_metrics = (
'annual_return_rate',
'max_closed_out_drawdown',
'best_month',
'worst_month',
'sharpe_ratio',
'sortino_ratio',
'monthly_std',
'annual_std')
"""
tuple : Default metrics for summary().
The metrics are:
'annual_return_rate'
'max_closed_out_drawdown'
'best_month'
'worst_month'
'sharpe_ratio'
'sortino_ratio'
'monthly_std'
'annual_std'
"""
currency_metrics = (
'beginning_balance',
'ending_balance',
'total_net_profit',
'gross_profit',
'gross_loss')
"""
tuple : Currency metrics for summary().
The metrics are:
'beginning_balance'
'ending_balance'
'total_net_profit'
'gross_profit'
'gross_loss'
"""
def _get_metric_value(s, metric):
"""
Returns a metric in either currency or raw format.
"""
if metric in currency_metrics:
return currency(s[metric])
else:
return s[metric]
def summary(stats, benchmark_stats=None, metrics=default_metrics, extras=None):
"""
Returns stats summary.
`stats()` must be called before calling this function.
Parameters
----------
stats : pd.Series
Statistics for the strategy.
benchmark_stats : pd.Series, optimal
Statistics for the benchmark (default is None, which implies
that a benchmark is not being used).
metrics : tuple, optional
The metrics to be used in the summary (default is
`default_metrics`).
extras : tuple, optional
The extra metrics to be used in the summary (default is None,
which imples that no extra metrics are being used).
"""
if extras is None:
extras = ()
metrics += extras
# Columns.
columns = ['strategy']
if benchmark_stats is not None:
columns.append('benchmark')
# Index & data.
index = []
data = []
for metric in metrics:
index.append(metric)
if benchmark_stats is not None:
data.append((_get_metric_value(stats, metric),
_get_metric_value(benchmark_stats, metric)))
else:
data.append(_get_metric_value(stats, metric))
df = pd.DataFrame(data, columns=columns, index=index)
return df
def optimizer_summary(strategies, metrics):
"""
Generate summary dataframe of a set of strategies vs metrics.
This function is designed to be used in analysis of an
optimization of some parameter. `stats()` must be called for
each strategy before calling this function.
Parameters
----------
strategies : pd.Series
Series of strategy objects that have the `stats` attribute.
metrics : tuple
The metrics to be used in the summary.
Returns
-------
df : pf.DataFrame
Summary of strategies vs metrics.
"""
index = []
columns = strategies.index
data = []
# Add metrics.
for metric in metrics:
index.append(metric)
data.append([_get_metric_value(strategy.stats, metric) for strategy in strategies])
df = pd.DataFrame(data, columns=columns, index=index)
return df
Global variables
var ALPHA_BEGIN
-
tuple : Use with
select_timeseries
, beginning data for any timeseries. var SP500_BEGIN
-
tuple : Use with
select_timeseries
, date the S&P500 began. var TRADING_DAYS_PER_MONTH
-
int : The number of trading days per month.
var TRADING_DAYS_PER_WEEK
-
int : The number of trading days per week.
var TRADING_DAYS_PER_YEAR
-
int : The number of trading days per year.
var currency_metrics
-
tuple : Currency metrics for summary().
The metrics are:
'beginning_balance' 'ending_balance' 'total_net_profit' 'gross_profit' 'gross_loss'
var default_metrics
-
tuple : Default metrics for summary().
The metrics are:
'annual_return_rate' 'max_closed_out_drawdown' 'best_month' 'worst_month' 'sharpe_ratio' 'sortino_ratio' 'monthly_std' 'annual_std'
Functions
def currency(amount)
-
Returns the dollar amount in US currency format.
Parameters
amount
:float
- The dollar amount to convert.
Returns
str
- the dollar amount in US currency format.
Expand source code
def currency(amount): """ Returns the dollar amount in US currency format. Parameters ---------- amount : float The dollar amount to convert. Returns ------- str the dollar amount in US currency format. """ if amount >= 0: return f'${amount:,.2f}' else: return f'-${-amount:,.2f}'
def get_trading_days()
-
Returns the number of trading days per year, month, and week.
Expand source code
def get_trading_days(): """ Returns the number of trading days per year, month, and week. """ return (__m.TRADING_DAYS_PER_YEAR, __m.TRADING_DAYS_PER_MONTH, __m.TRADING_DAYS_PER_WEEK)
def optimizer_summary(strategies, metrics)
-
Generate summary dataframe of a set of strategies vs metrics.
This function is designed to be used in analysis of an optimization of some parameter.
stats()
must be called for each strategy before calling this function.Parameters
strategies
:pd.Series
- Series of strategy objects that have the
stats()
attribute. metrics
:tuple
- The metrics to be used in the summary.
Returns
df
:pf.DataFrame
- Summary of strategies vs metrics.
Expand source code
def optimizer_summary(strategies, metrics): """ Generate summary dataframe of a set of strategies vs metrics. This function is designed to be used in analysis of an optimization of some parameter. `stats()` must be called for each strategy before calling this function. Parameters ---------- strategies : pd.Series Series of strategy objects that have the `stats` attribute. metrics : tuple The metrics to be used in the summary. Returns ------- df : pf.DataFrame Summary of strategies vs metrics. """ index = [] columns = strategies.index data = [] # Add metrics. for metric in metrics: index.append(metric) data.append([_get_metric_value(strategy.stats, metric) for strategy in strategies]) df = pd.DataFrame(data, columns=columns, index=index) return df
def select_trading_days(use_stock_market_calendar)
-
Select between continuous and standard stock market days.
Set use_stock_market_calendar=False if your timeseries is 7 days a week, e.g. cryptocurrencies.
Parameters
use_stock_market_calendar
:bool
- True for standard stock market calendar. False for trading 7 days a week.
Returns
None
Expand source code
def select_trading_days(use_stock_market_calendar): """ Select between continuous and standard stock market days. Set use_stock_market_calendar=False if your timeseries is 7 days a week, e.g. cryptocurrencies. Parameters ---------- use_stock_market_calendar : bool True for standard stock market calendar. False for trading 7 days a week. Returns ------- None """ if use_stock_market_calendar: __m.TRADING_DAYS_PER_YEAR = 252 __m.TRADING_DAYS_PER_MONTH = 20 __m.TRADING_DAYS_PER_WEEK = 5 else: __m.TRADING_DAYS_PER_YEAR = 365 __m.TRADING_DAYS_PER_MONTH = 30 __m.TRADING_DAYS_PER_WEEK = 7
def stats(ts, tlog, dbal, capital)
-
Compute trading stats.
Parameters
ts
:pd.DataFrame
- The timeseries of a symbol.
tlog
:pd.DataFrame
- The trade log.
dbal
:pd.DataFrame
- The daily balance.
capital
:int
- The amount of money available for trading.
Examples
>>> stats = pf.stats(ts, tlog, dbal, capital)
Returns
stats
:pd.Series
- The statistics for the strategy.
Expand source code
def stats(ts, tlog, dbal, capital): """ Compute trading stats. Parameters ---------- ts : pd.DataFrame The timeseries of a symbol. tlog : pd.DataFrame The trade log. dbal : pd.DataFrame The daily balance. capital : int The amount of money available for trading. Examples -------- >>> stats = pf.stats(ts, tlog, dbal, capital) Returns ------- stats : pd.Series The statistics for the strategy. """ start = ts.index[0] end = ts.index[-1] stats = pd.Series(dtype='object') # OVERALL RESULTS stats['start'] = start.strftime('%Y-%m-%d') stats['end'] = end.strftime('%Y-%m-%d') stats['beginning_balance'] = _beginning_balance(capital) stats['ending_balance'] = _ending_balance(dbal) stats['total_net_profit'] = _total_net_profit(tlog) stats['gross_profit'] = _gross_profit(tlog) stats['gross_loss'] = _gross_loss(tlog) stats['profit_factor'] = _profit_factor(tlog) stats['return_on_initial_capital'] = _return_on_initial_capital(tlog, capital) cagr = _annual_return_rate(dbal['close'].iloc[-1], capital, start, end) stats['annual_return_rate'] = cagr stats['trading_period'] = _trading_period(start, end) stats['pct_time_in_market'] = _pct_time_in_market(dbal) # LEVERAGE stats['margin'] = _margin() stats['avg_leverage'] = _avg_leverage(dbal) stats['max_leverage'] = _max_leverage(dbal) stats['min_leverage'] = _min_leverage(dbal) # SUMS stats['total_num_trades'] = _total_num_trades(tlog) stats['trades_per_year'] = _trades_per_year(tlog, start, end) stats['num_winning_trades'] = _num_winning_trades(tlog) stats['num_losing_trades'] = _num_losing_trades(tlog) stats['num_even_trades'] = _num_even_trades(tlog) stats['pct_profitable_trades'] = _pct_profitable_trades(tlog) # CASH PROFITS AND LOSSES stats['avg_profit_per_trade'] = _avg_profit_per_trade(tlog) stats['avg_profit_per_winning_trade'] = _avg_profit_per_winning_trade(tlog) stats['avg_loss_per_losing_trade'] = _avg_loss_per_losing_trade(tlog) stats['ratio_avg_profit_win_loss'] = _ratio_avg_profit_win_loss(tlog) stats['largest_profit_winning_trade'] = _largest_profit_winning_trade(tlog) stats['largest_loss_losing_trade'] = _largest_loss_losing_trade(tlog) # POINTS stats['num_winning_points'] = _num_winning_points(tlog) stats['num_losing_points'] = _num_losing_points(tlog) stats['total_net_points'] = _total_net_points(tlog) stats['avg_points'] = _avg_points(tlog) stats['largest_points_winning_trade'] = _largest_points_winning_trade(tlog) stats['largest_points_losing_trade'] = _largest_points_losing_trade(tlog) stats['avg_pct_gain_per_trade'] = _avg_pct_gain_per_trade(tlog) stats['largest_pct_winning_trade'] = _largest_pct_winning_trade(tlog) stats['largest_pct_losing_trade'] = _largest_pct_losing_trade(tlog) stats['expected_shortfall'] = _expected_shortfall(tlog) # STREAKS stats['max_consecutive_winning_trades'] = _max_consecutive_winning_trades(tlog) stats['max_consecutive_losing_trades'] = _max_consecutive_losing_trades(tlog) stats['avg_bars_winning_trades'] = _avg_bars_winning_trades(ts, tlog) stats['avg_bars_losing_trades'] = _avg_bars_losing_trades(ts, tlog) # DRAWDOWN dd = _max_closed_out_drawdown(dbal['close']) stats['max_closed_out_drawdown'] = dd['max'] stats['max_closed_out_drawdown_peak_date'] = dd['peak_date'] stats['max_closed_out_drawdown_trough_date'] = dd['trough_date'] stats['max_closed_out_drawdown_recovery_date'] = dd['recovery_date'] stats['drawdown_loss_period'], stats['drawdown_recovery_period'] = \ _drawdown_loss_recovery_period(dd['peak_date'], dd['trough_date'], dd['recovery_date']) if dd['max'] == 0: stats['annualized_return_over_max_drawdown'] = 0 else: stats['annualized_return_over_max_drawdown'] = abs(cagr / dd['max']) dd = _max_intra_day_drawdown(dbal['high'], dbal['low']) stats['max_intra_day_drawdown'] = dd['max'] dd = _rolling_max_dd(dbal['close'], TRADING_DAYS_PER_YEAR) stats['avg_yearly_closed_out_drawdown'] = np.average(dd) stats['max_yearly_closed_out_drawdown'] = min(dd) dd = _rolling_max_dd(dbal['close'], TRADING_DAYS_PER_MONTH) stats['avg_monthly_closed_out_drawdown'] = np.average(dd) stats['max_monthly_closed_out_drawdown'] = min(dd) dd = _rolling_max_dd(dbal['close'], TRADING_DAYS_PER_WEEK) stats['avg_weekly_closed_out_drawdown'] = np.average(dd) stats['max_weekly_closed_out_drawdown'] = min(dd) # RUNUP ru = _rolling_max_ru(dbal['close'], TRADING_DAYS_PER_YEAR) stats['avg_yearly_closed_out_runup'] = np.average(ru) stats['max_yearly_closed_out_runup'] = ru.max() ru = _rolling_max_ru(dbal['close'], TRADING_DAYS_PER_MONTH) stats['avg_monthly_closed_out_runup'] = np.average(ru) stats['max_monthly_closed_out_runup'] = max(ru) ru = _rolling_max_ru(dbal['close'], TRADING_DAYS_PER_WEEK) stats['avg_weekly_closed_out_runup'] = np.average(ru) stats['max_weekly_closed_out_runup'] = max(ru) # PERCENT CHANGE pc = _pct_change(dbal['close'], TRADING_DAYS_PER_YEAR) if len(pc) > 0: stats['pct_profitable_years'] = (pc > 0).sum() / len(pc) * 100 stats['best_year'] = pc.max() stats['worst_year'] = pc.min() stats['avg_year'] = np.average(pc) stats['annual_std'] = pc.std() pc = _pct_change(dbal['close'], TRADING_DAYS_PER_MONTH) if len(pc) > 0: stats['pct_profitable_months'] = (pc > 0).sum() / len(pc) * 100 stats['best_month'] = pc.max() stats['worst_month'] = pc.min() stats['avg_month'] = np.average(pc) stats['monthly_std'] = pc.std() pc = _pct_change(dbal['close'], TRADING_DAYS_PER_WEEK) if len(pc) > 0: stats['pct_profitable_weeks'] = (pc > 0).sum() / len(pc) * 100 stats['best_week'] = pc.max() stats['worst_week'] = pc.min() stats['avg_week'] = np.average(pc) stats['weekly_std'] = pc.std() pc = _pct_change(dbal['close'], 1) if len(pc) > 0: stats['pct_profitable_days'] = (pc > 0).sum() / len(pc) * 100 stats['best_day'] = pc.max() stats['worst_day'] = pc.min() stats['avg_day'] = np.average(pc) stats['daily_std'] = pc.std() # RATIOS sr = _sharpe_ratio(dbal['close'].pct_change()) sr_std = math.sqrt((1 + 0.5*sr**2) / len(dbal)) stats['sharpe_ratio'] = sr stats['sharpe_ratio_max'] = sr + 3*sr_std #3 std=>99.73% stats['sharpe_ratio_min'] = sr - 3*sr_std stats['sortino_ratio'] = _sortino_ratio(dbal['close'].pct_change()) return stats
def summary(stats, benchmark_stats=None, metrics=('annual_return_rate', 'max_closed_out_drawdown', 'best_month', 'worst_month', 'sharpe_ratio', 'sortino_ratio', 'monthly_std', 'annual_std'), extras=None)
-
Returns stats summary.
stats()
must be called before calling this function.Parameters
stats
:pd.Series
- Statistics for the strategy.
benchmark_stats
:pd.Series, optimal
- Statistics for the benchmark (default is None, which implies that a benchmark is not being used).
metrics
:tuple
, optional- The metrics to be used in the summary (default is
default_metrics
). extras
:tuple
, optional- The extra metrics to be used in the summary (default is None, which imples that no extra metrics are being used).
Expand source code
def summary(stats, benchmark_stats=None, metrics=default_metrics, extras=None): """ Returns stats summary. `stats()` must be called before calling this function. Parameters ---------- stats : pd.Series Statistics for the strategy. benchmark_stats : pd.Series, optimal Statistics for the benchmark (default is None, which implies that a benchmark is not being used). metrics : tuple, optional The metrics to be used in the summary (default is `default_metrics`). extras : tuple, optional The extra metrics to be used in the summary (default is None, which imples that no extra metrics are being used). """ if extras is None: extras = () metrics += extras # Columns. columns = ['strategy'] if benchmark_stats is not None: columns.append('benchmark') # Index & data. index = [] data = [] for metric in metrics: index.append(metric) if benchmark_stats is not None: data.append((_get_metric_value(stats, metric), _get_metric_value(benchmark_stats, metric))) else: data.append(_get_metric_value(stats, metric)) df = pd.DataFrame(data, columns=columns, index=index) return df