def get_pl_ratio(start_date, end_date, holdings, df_price): assert check_time(start_date=start_date, end_date=end_date) periods = list(holdings.holdings.keys()) # trim log to fit within the time period periods = [ date for date in periods if start_date <= date.start_time <= end_date ][1:] profit, loss = [], [] for period in periods: portfolio = holdings.holdings[period] # get portfolio value at start and end time start_val = portfolio.get_net_liquidation(period.start_time, df_price) end_val = portfolio.get_net_liquidation(period.end_time, df_price) # if the rebalance made a profit, increase gains by 1 if end_val > start_val: profit.append(end_val - start_val) elif end_val < start_val: loss.append(start_val - end_val) # calculate average profits and losses avg_profit = np.mean(profit) avg_loss = np.mean(loss) # calculate P/L ratio pl_ratio = avg_profit / avg_loss return pl_ratio
def get_win_rate(start_date, end_date, holdings, df_price) -> float: assert check_time(start_date=start_date, end_date=end_date) periods = list(holdings.holdings.keys()) # trim log to fit within the time period periods = [ date for date in periods if start_date <= date.start_time <= end_date ][1:] # how many periods are there? n_periods = len(periods) winning_periods = 0 for period in periods: portfolio = holdings.holdings[period] # get portfolio value at start and end time start_val = portfolio.get_net_liquidation(period.start_time, df_price) end_val = portfolio.get_net_liquidation(period.end_time, df_price) # if the rebalance made a profit, increase gains by 1 if end_val > start_val: winning_periods += 1 # calculate win rate win_rate = winning_periods / n_periods return win_rate
def contains_time(self, time:Union[datetime.datetime, pd.Timestamp]) -> bool: '''checks if a time is within a period''' assert check_time(time=time) if self.start_time <= time <= self.end_time: return True else: return False
def get_stock_liquidation(self, date: Union[pd.Timestamp, datetime], df_prices: pd.DataFrame) -> float: '''calculates the value of all long/short positions in a portfolio at a given time''' assert check_time(date=date) df_prices = df_prices.loc[:date] # trim df to be within valid dates agg_stock_value = 0 # first, consider long positions for stock, shares in self.long.items(): try: long_price = df_prices.at[date, stock] except KeyError: # when was this stock last traded? last_traded = df_prices[stock].last_valid_index() long_price = df_prices.at[last_traded, stock] long_stock_value = long_price * shares agg_stock_value += long_stock_value # then, consider short positions for stock, shares in self.short.items(): try: short_price = df_prices.at[date, stock] except KeyError: # when was this stock last traded? last_traded = df_prices[stock].last_valid_index() short_price = df_prices.at[last_traded, stock] short_stock_value = short_price * shares agg_stock_value -= short_stock_value return agg_stock_value
def get_tracking_error(strategy, benchmark, start_date, end_date) -> float: '''calculates the tracking error of a strategy compared to a benchmark''' assert check_prices(strategy=strategy, benchmark=benchmark) assert check_time(start_date=start_date, end_date=end_date) ann_ex_r = get_annualized_excess_return(strategy, benchmark, start_date, end_date) ir = get_information_ratio(strategy, benchmark, start_date, end_date) tracking_error = ann_ex_r / ir return tracking_error
def get_risk_adjusted_return(prices, start_date, end_date) -> float: '''calculates the risk-adjusted return of a prices time-series between two dates''' assert check_prices(prices=prices) assert check_time(start_date=start_date, end_date=end_date) prices = prices.loc[start_date:end_date] start_date, end_date = prices.index[0], prices.index[-1] ann_rtn = get_annualized_return(prices, start_date, end_date) vo = get_strategy_volatility(prices, start_date, end_date) risk_adj_rtn = ann_rtn / vo risk_adj_rt = round(risk_adj_rtn, 4) return risk_adj_rt
def get_cumulative_return(prices, start_date, end_date) -> float: '''calculates the cumulative return between two dates''' assert check_prices(prices=prices) assert check_time(start_date=start_date, end_date=end_date) prices.sort_index(inplace=True) # make sure dates is in ascending order prices = prices.loc[start_date:end_date] start_date, end_date = prices.index[0], prices.index[-1] cost = prices[start_date] revenue = prices[end_date] cum_rtn = (revenue - cost) / cost cum_rtn = round(cum_rtn, 4) return cum_rtn
def get_strategy_volatility(prices, start_date, end_date) -> float: '''calculates the volatility of a prices time-series between two dates''' assert check_prices(prices=prices) assert check_time(start_date=start_date, end_date=end_date) change = prices.pct_change().dropna() change = change[change != 0] change_avg = np.mean(change) daily_sum = np.sum([(c - change_avg)**2 for c in change]) vo = np.sqrt((250 / ((end_date - start_date).days - 1)) * daily_sum) vo = round(vo, 4) return vo
def get_excess_return(strategy, benchmark, start_date, end_date) -> float: '''calculates the excess return of a strategy over a benchmark between two dates''' assert check_prices(strategy=strategy, benchmark=benchmark) assert check_time(start_date=start_date, end_date=end_date) strategy = strategy.loc[start_date:end_date] benchmark = benchmark.loc[start_date:end_date] start_date, end_date = strategy.index[0], strategy.index[-1] r_strategy = get_daily_returns(strategy) r_benchmark = get_daily_returns(benchmark) excess_return = (r_strategy - r_benchmark).cumsum() excess_return = round(excess_return, 4) return excess_return
def get_annualized_excess_return(strategy, benchmark, start_date, end_date) -> float: '''calculate the annuliazed return of a strategy compared to a benchmark''' assert check_prices(strategy=strategy, benchmark=benchmark) assert check_time(start_date=start_date, end_date=end_date) strategy = strategy.loc[start_date:end_date] benchmark = benchmark.loc[start_date:end_date] start_date, end_date = strategy.index[0], strategy.index[-1] strategy_return = get_annualized_return(strategy, start_date, end_date) market_return = get_annualized_return(benchmark, start_date, end_date) ann_ex_rtn = strategy_return - market_return ann_ex_rtn = round(ann_ex_rtn, 4) return ann_ex_rtn
def get_sharpe_ratio(prices, start_date, end_date, risk_free=0.04) -> float: '''calculates the sharpe ratio of a prices time-series between two dates''' assert check_prices(prices=prices) assert check_time(start_date=start_date, end_date=end_date) assert 0 <= risk_free <= 1, 'the risk free rate must be between 0 and 1' prices = prices.loc[start_date:end_date] start_date, end_date = prices.index[0], prices.index[-1] ann_rtn = get_annualized_return(prices, start_date, end_date) excess_rtn = ann_rtn - risk_free vo = get_strategy_volatility(prices, start_date, end_date) sharpe_ratio = excess_rtn / vo sharpe_ratio = round(sharpe_ratio, 4) return sharpe_ratio
def get_turnover_ratio(start_date, end_date, holdings, df_price): ''' Calculates the turnover ratio of a portfolio over a holding period. Only long positions are taken into consideration, short positions are ignored. ''' assert check_time(start_date=start_date, end_date=end_date) periods = sorted(list(holdings.holdings.keys())) # trim log to fit within the time period periods = [ period for period in periods if start_date <= period.start_time <= end_date ] rebalance_dates = [period.end_time for period in periods][:-1] start_portfolio = holdings.holdings[periods[0]] end_portfolio = holdings.holdings[periods[-1]] start_val = start_portfolio.get_net_liquidation(periods[0].start_time, df_price) end_val = end_portfolio.get_net_liquidation(periods[-1].end_time, df_price) agg_turnover = 0 buy_value = sell_value = [] for day in rebalance_dates: pre_portfolio = holdings.get_portfolio(day).long post_portfolio = holdings.get_portfolio(day + pd.Timedelta(days=1)).long buy_portfolio = Portfolio( long=dict(Counter(post_portfolio) - Counter(pre_portfolio))) sell_portfolio = Portfolio( long=dict(Counter(pre_portfolio) - Counter(post_portfolio))) buy_value = buy_portfolio.get_net_liquidation(day, df_price) sell_value = sell_portfolio.get_net_liquidation(day, df_price) turnover = (buy_value + sell_value) / 2 agg_turnover += turnover avg_liquidation = (end_val + start_val) / 2 n_years = periods[-1].end_time.year - periods[0].start_time.year avg_annual_turnover = agg_turnover / n_years avg_turnover_ratio = avg_annual_turnover / avg_liquidation return avg_turnover_ratio
def get_max_drawdown(prices, start_date, end_date) -> float: '''calculates the maximum drawdown of a prices time-series between two dates''' assert check_prices(prices=prices) assert check_time(start_date=start_date, end_date=end_date) prices = prices.loc[start_date:end_date] start_date, end_date = prices.index[0], prices.index[-1] max_drawdown = 0 # for each day, get the lowest price in the period after for day in prices.index: day_price = prices[day] lowest = prices.loc[day:].min() drawdown = (day_price - lowest) / day_price if drawdown > max_drawdown: max_drawdown = drawdown max_drawdown = round(max_drawdown, 4) return max_drawdown
def get_beta(strategy, benchmark, start_date, end_date) -> float: '''calculates the beta of a prices time-series between two dates''' assert check_prices(strategy=strategy, benchmark=benchmark) assert check_time(start_date=start_date, end_date=end_date) strategy = strategy.loc[start_date:end_date] benchmark = benchmark.loc[start_date:end_date] start_date, end_date = strategy.index[0], strategy.index[-1] r_strategy = get_daily_returns(strategy) r_benchmark = get_daily_returns(benchmark) var = np.var(r_benchmark, ddof=1) cov = np.cov(r_strategy, r_benchmark)[0][1] beta = cov / var beta = round(beta, 4) return beta
def get_information_ratio(strategy, benchmark, start_date, end_date) -> float: '''calculates the information ratio of a prices time-series between two dates''' assert check_prices(strategy=strategy, benchmark=benchmark) assert check_time(start_date=start_date, end_date=end_date) excess_return = get_annualized_excess_return(strategy, benchmark, start_date, end_date) strategy_daily = get_daily_returns(strategy) benchmark_daily = get_daily_returns(benchmark) strategy_daily = strategy_daily[strategy_daily != 0] benchmark_daily = benchmark_daily[benchmark_daily != 0] daily_excess_return = strategy_daily - benchmark_daily daily_stdev = np.std(daily_excess_return) * np.sqrt(250) ir = excess_return / daily_stdev ir = round(ir, 4) return ir
def get_daily_win_rate(strategy, benchmark, start_date, end_date) -> float: '''calculates the daily win rate of a strategy compared to a benchmark''' assert check_prices(strategy=strategy, benchmark=benchmark) assert check_time(start_date=start_date, end_date=end_date) strategy = strategy.loc[start_date:end_date] benchmark = benchmark.loc[start_date:end_date] start_date, end_date = strategy.index[0], strategy.index[-1] r_strategy = get_daily_returns(strategy) r_benchmark = get_daily_returns(benchmark) daily_diff = r_strategy - r_benchmark win = 0 for day in daily_diff: if day > 0: win += 1 daily_win_rate = win / len(daily_diff) daily_win_rate = round(daily_win_rate, 4) return daily_win_rate
def get_alpha(strategy, benchmark, start_date, end_date, risk_free=0.04) -> float: '''calculates the alpha of a prices time-series between two dates''' assert check_prices(strategy=strategy, benchmark=benchmark) assert check_time(start_date=start_date, end_date=end_date) assert 0 <= risk_free <= 1, 'the risk free rate must be between 0 and 1' strategy = strategy.loc[start_date:end_date] benchmark = benchmark.loc[start_date:end_date] start_date, end_date = strategy.index[0], strategy.index[-1] market_return = get_annualized_return(benchmark, start_date, end_date) beta = get_beta(strategy, benchmark, start_date, end_date) capm = risk_free + beta * (market_return - risk_free ) # asset price under the CAPM model annualized_return = get_annualized_return(strategy, start_date, end_date) alpha = annualized_return - capm alpha = round(alpha, 4) return alpha