| Overall Statistics |
|
Total Trades 0 Average Win 0% Average Loss 0% Compounding Annual Return 0% Drawdown 0% Expectancy 0 Net Profit 0% Sharpe Ratio 0 Probabilistic Sharpe Ratio 0% Loss Rate 0% Win Rate 0% Profit-Loss Ratio 0 Alpha 0 Beta 0 Annual Standard Deviation 0 Annual Variance 0 Information Ratio -3.128 Tracking Error 0.149 Treynor Ratio 0 Total Fees $0.00 Estimated Strategy Capacity $0 |
from clr import AddReference
AddReference("QuantConnect.Research")
from QuantConnect import *
from QuantConnect.Research import QuantBook
from QuantConnect.Statistics import *
from factors import Sortino, Drawdown
from datetime import timedelta
import pandas as pd
def get_performance_statistics(composite_equity_curve, bechmark_security_id="SPY R735QTJ8XC9X"):
"""
Calculates several performance statistics on the composite equity curve.
Input:
- composite_equity_curve
Equity curve of the optimized portfolio
- bechmark_security_id
Security ID of the benchmark to use in some of the statistic calculations (like Beta)
Returns a DataFrame that lists the performance statistics of the composite equity curve.
"""
performance_statistics = {}
daily_returns = list(composite_equity_curve.pct_change().dropna().values)
# CompoundingAnnualPerformance
start_equity = composite_equity_curve.iloc[0]
end_equity = composite_equity_curve.iloc[-1]
num_years = (composite_equity_curve.index[-1] - composite_equity_curve.index[0]).days / 365
comp_annual_performance = Statistics.CompoundingAnnualPerformance(start_equity, end_equity, num_years)
performance_statistics['CompoundingAnnualPerformance'] = "{:.2%}".format(comp_annual_performance)
# AnnualPerformance
performance_statistics['AnnualPerformance'] = Statistics.AnnualPerformance(daily_returns, 365)
# AnnualVariance
performance_statistics['AnnualVariance'] = Statistics.AnnualVariance(daily_returns, 365)
# AnnualStandardDeviation
performance_statistics['AnnualStandardDeviation'] = Statistics.AnnualStandardDeviation(daily_returns, 365)
# Fetch daily benchmark returns
qb = QuantBook()
start_date = composite_equity_curve.index[0] - timedelta(days=5) # 5 day buffer incase of holidays/weekends
end_date = composite_equity_curve.index[-1] + timedelta(days=5)
benchmark_symbol = qb.Symbol(bechmark_security_id)
history = qb.History(benchmark_symbol, start_date, end_date, Resolution.Daily)
closes = history.loc[benchmark_symbol].close
closes = closes.resample('D').mean().interpolate(method='linear')
closes = closes.reindex(pd.DatetimeIndex(composite_equity_curve.index)) # Line up benchmark index with portfolio index
benchmark_daily_returns = list(closes.pct_change().dropna().values)
# Beta
performance_statistics['Beta'] = Statistics.Beta(daily_returns, benchmark_daily_returns)
# Alpha
performance_statistics['Alpha'] = Statistics.Alpha(daily_returns, benchmark_daily_returns, 0)
# Tracking Error
performance_statistics['TrackingError'] = Statistics.TrackingError(daily_returns, benchmark_daily_returns, 365)
# Information Ratio
performance_statistics['InformationRatio'] = Statistics.InformationRatio(daily_returns, benchmark_daily_returns)
# Sharpe
performance_statistics['SharpeRatio'] = Statistics.SharpeRatio(daily_returns, 0)
# Sortino
performance_statistics['Sortino'] = Sortino.evaluate(composite_equity_curve)
# Max Drawdown
performance_statistics['MaxDrawdown'] = "{:.2%}".format(Drawdown.evaluate(composite_equity_curve))
# Treynor Ratio
performance_statistics['TreynorRatio'] = Statistics.TreynorRatio(daily_returns, benchmark_daily_returns, 0)
# PSR
#benchmark_sharpe = Statistics.SharpeRatio(benchmark_daily_returns, 0)
#performance_statistics['ProbabilisticSharpeRatio'] = Statistics.ProbabilisticSharpeRatio(daily_returns, benchmark_sharpe)
# Observed Sharpe Ratio
performance_statistics['ObservedSharpeRatio'] = Statistics.ObservedSharpeRatio(daily_returns)
# Round the statistics for a nice display
for key, value in performance_statistics.items():
if not isinstance(value, str):
performance_statistics[key] = round(value, 4)
return pd.DataFrame(pd.Series(performance_statistics, name='value'))import calendar
import matplotlib.pyplot as plt
import pandas as pd
def plot_allocations_over_time(allocations_over_time):
"""
Creates a plot to show the allocations given to each alpha over time.
Input:
- allocations_over_time
A DataFrame which lists how much to allocate to each alpha over time
"""
f, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(16, 10))
# Calculate bar plot widths
widths = []
for time, row in allocations_over_time.iterrows():
days_in_month = calendar.monthrange(time.year, time.month)[1]
widths.append(days_in_month - time.day - 0.5)
# Stacked bar plot
ax1.set_title('Alpha Allocations Over Time')
ax1.set_ylabel('Portfolio Weight (%)')
x = allocations_over_time.index
previous_allocations = [0] * allocations_over_time.shape[0]
for alpha in allocations_over_time.columns:
current_allocations = allocations_over_time[alpha].fillna(0) * 100
ax1.bar(x, current_allocations, widths, bottom=previous_allocations, align='edge', label=alpha)
previous_allocations = current_allocations + previous_allocations
ax1.legend(allocations_over_time.columns, frameon=True, framealpha=0.7, facecolor='white')
# Bar plot
number_of_alphas_allocated_to = []
for time, row in allocations_over_time.iterrows():
number_of_alphas_allocated_to.append(len(row[row > 0]))
ax2.bar(x, number_of_alphas_allocated_to, width=widths, align='edge')
ax2.set_xlabel('Month')
ax2.set_ylabel('Number of Alphas Allocated To')
plt.xticks(rotation=-90)
y_ticks = range(0, max(number_of_alphas_allocated_to)+1)
ax2.yaxis.set_ticks(y_ticks)
ax2.margins(0)
plt.show()
def plot_all_equity_curves(equity_curves, composite_equity_curve):
"""
Plot the equity curves and composite equity curve in a single line chart.
Input:
- equity_curves
Equity curves of the alphas
- composite_equity_curve
Equity curve of the optimized portfolio
"""
all_curves = pd.DataFrame()
all_curves = equity_curves.join(composite_equity_curve, how = 'outer')
all_curves[equity_curves.columns].plot(figsize=(16, 6), c='grey')
all_curves['*CompositeAlpha*'].plot(c='orange', linewidth=4)
plt.legend(all_curves.columns)
plt.title('Alpha Equity vs Optimized Portfolio Equity')
plt.xlabel('Date')
plt.ylabel('Normalized Equity')
plt.show()from scipy.optimize import minimize, LinearConstraint
import numpy as np
import pandas as pd
from clr import AddReference
AddReference("QuantConnect.Research")
from QuantConnect import *
from QuantConnect.Research import QuantBook
from factors import *
class AlphaStreamOptimizer:
"""
Provides an implementation of a portfolio optimizer that maximizes the Sortino ratio.
"""
def Optimize(self, equity_curves):
"""
Use SciPy to optimize the portfolio weights of the alphas included in the `equity_curves` DataFrame.
Input:
- equity_curves
DataFrame of trailing equity curves for n alphas
Array of doubles, representing the optimized portfolio weights for the alphas
"""
size = equity_curves.columns.size
x0 = np.array(size * [1. / size]) # initial guess is equal-weighting
# Σw <= 1
constraints = [{'type': 'eq', 'fun': lambda weights: self.get_budget_constraint(weights)}]
opt = minimize(lambda weights: self.objective_function(equity_curves, weights), # Objective function
x0, # Initial guess
bounds = self.get_boundary_conditions(size), # Bounds for variables: 0 ≤ w ≤ 1
constraints = constraints, # Constraints definition
method='SLSQP', # Optimization method: Sequential Least Squares Programming
options ={'ftol': 1e-10, 'maxiter': 200, 'disp': False}) # Additional options
return opt['x'] if opt['success'] else x0
def objective_function(self, equity_curves, weights):
"""
Objective function to use when optimizing the portfolio weights
Input:
- equity_curves
DataFrame of equity curves for the alphas
- weights
Test weights selected by the optimizer
Returns a score for the weights that's calculated by applying the Sortino factor.
"""
equity_curve = (equity_curves * weights).sum(axis=1)
return self.f_scale(-Sortino.evaluate(equity_curve)) # negative so we maximize it
def f_scale(self, x):
"""
Bounds the value of `x` to [-5, +5] using a sigmoidal curve
Input:
- x
Value to be bounded
Returns the bounded `x` value.
"""
return x*5/np.sqrt(10+x*x)
def get_boundary_conditions(self, size):
"""
Creates the boundary condition for the portfolio weights
Input:
- size
"""
return tuple((0.0, 1.0) for x in range(size))
def get_budget_constraint(self, weights):
"""
Defines a budget constraint: the sum of the weights = 1
Input:
- weights
Array of portfolio weights
"""
return np.sum(weights) - 1
def optimize_allocations(equity_curves, lookback):
"""
Determines how much of the portfolio to allocate to each alpha on a monthly basis.
Input:
- equity_curves
DataFrame of equity curves of individual Alpha Streams algorithms
- lookback
An integer representing the number of days to look back when calculating the factor values
Returns a DataFrame that shows how much to allocate to each alpha for each month in order
to maximize the trailing portfolio factor.
"""
allocation_by_alpha_id = {}
allocations_over_time = pd.DataFrame()
optimizer = AlphaStreamOptimizer()
month = 0
print("Working please wait...")
for time, row in equity_curves.iterrows():
# Rebalance monthly
if time.month == month:
continue
# Select active alphas
active_alphas = list(row.index[~row.isna()])
# Get trailing history
window = equity_curves[active_alphas].loc[:time].iloc[-lookback:].dropna(axis=1)
active_alphas = list(window.columns)
if len(active_alphas) < 2 or len(window) < lookback:
continue
month = time.month
# Scale each equity curve to have start value of 1 over the lookback period
normalized_equity_curves = window / window.iloc[0]
best_allocation_scheme = optimizer.Optimize(normalized_equity_curves)
# Save allocation scheme that the optimizer has selected
allocation_by_alpha_id = dict(zip(window.columns, best_allocation_scheme))
for alpha_name, allocation in allocation_by_alpha_id.items():
allocations_over_time.loc[time.date(), alpha_name] = allocation
return allocations_over_time
def get_composite_equity_curve(equity_curves, allocations_over_time):
"""
Builds the composite equity curve that's produced by following the
allocations_over_time in real time.
Input:
- equity_curves
A DataFrame holding the equity curves of all the alphas under analysis
- allocations_over_time
A DataFrame which lists how much to allocate to each alpha over time
Returns the composite equity curve
"""
composite_equity_curve = pd.Series()
daily_returns = equity_curves.pct_change().shift(-1)
current_allocation = pd.Series()
for time, row in daily_returns.iterrows():
date = time.date()
if date in allocations_over_time.index:
current_allocation = allocations_over_time.loc[date].dropna()
if current_allocation.empty:
continue
daily_return = sum(current_allocation * row[current_allocation.index])
composite_equity_curve = composite_equity_curve.append(pd.Series([daily_return], index=[date]))
composite_equity_curve = (composite_equity_curve + 1).cumprod().shift(1).dropna()
composite_equity_curve.name = '*CompositeAlpha*'
return composite_equity_curvefrom clr import AddReference
AddReference("QuantConnect.Research")
from QuantConnect import *
from QuantConnect.Research import QuantBook
from abc import ABC, abstractmethod
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
class Factor(ABC):
"""
Abstract base class used to create factors
"""
@abstractmethod
def evaluate(equity_curve):
"""
Calculates the factor value using the provided equity curve.
Input:
- equity_curve
The equity curve to calculate the factor on
Returns the factor value when applied to the equity curve.
"""
raise Exception("evaluate method not implemented yet.")
class Sortino(Factor):
"""
Sortino Ratio
"""
def evaluate(equity_curve):
returns = equity_curve.pct_change().dropna()
ann_ret = ((np.mean(returns) + 1) ** 252) - 1
ann_down_std = np.std(returns.loc[returns < 0]) * np.sqrt(252)
return ann_ret / ann_down_std if ann_down_std is not 0 else None
class Beta(Factor):
"""
Beta
"""
benchmark_data = pd.DataFrame()
@staticmethod
def load_data(benchmark_security_id='SPY R735QTJ8XC9X', benchmark_name='*Benchmark*'):
"""
Loads the benchmark data so we can calculate the factor value
Input:
- benchmark_security_id
Security ID of the benchmark security
- benchmark_name
The column name to use for the benchmark in the DataFrame that's loaded
"""
Beta.benchmark_data = pd.DataFrame()
qb = QuantBook()
benchmark_symbol = qb.Symbol(benchmark_security_id)
# Load benchmark history
history = qb.History(benchmark_symbol, datetime(1998, 1, 2), datetime.now(), Resolution.Daily)
Beta.benchmark_data = history.loc[benchmark_symbol].close
Beta.benchmark_data = Beta.benchmark_data.resample('D').mean().interpolate(method='linear', limit_area='inside')
Beta.benchmark_data.name = benchmark_name
def evaluate(equity_curve):
# Get benchmark equity curve
if Beta.benchmark_data.empty:
Beta.load_data()
start = equity_curve.index[0]
end = equity_curve.index[-1] + timedelta(days=1)
benchmark_equity_curve = Beta.benchmark_data.loc[start:end]
# Calculate Beta
equity_curve_returns = equity_curve.pct_change().dropna()
benchmark_returns = benchmark_equity_curve.pct_change().dropna()
equity_df = pd.concat([equity_curve_returns, benchmark_returns], axis=1)
corr = equity_df.corr()[benchmark_equity_curve.name][0]
std = equity_curve_returns.std()
if std == 0:
return np.nan
std_ratio = benchmark_returns.std() / std
return corr * std_ratio
class Drawdown(Factor):
"""
Drawdown
"""
def evaluate(equity_curve):
equity_curve = equity_curve.values
i = np.argmax(np.maximum.accumulate(equity_curve) - equity_curve)
if equity_curve[:i].size == 0:
return np.nan
j = np.argmax(equity_curve[:i])
return abs((equity_curve[i]/equity_curve[j]) - 1) #round(abs((equity_curve[i]/equity_curve[j]) - 1), 3)class LogicalFluorescentOrangeSalamander(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2020, 10, 5) # Set Start Date
self.SetCash(100000) # Set Strategy Cash
# self.AddEquity("SPY", Resolution.Minute)
def OnData(self, data):
'''OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here.
Arguments:
data: Slice object keyed by symbol containing the stock data
'''
# if not self.Portfolio.Invested:
# self.SetHoldings("SPY", 1)from clr import AddReference
AddReference("QuantConnect.Research")
from QuantConnect import *
from QuantConnect.Research import QuantBook
import pandas as pd
from io import StringIO
def get_live_equity_curves(alpha_id_by_name):
"""
Gathers the live equity curves of active alphas. We declare an alpha as 'inactive'
if the last data point in its equity curve is older that the last data point in the
equity curve of another alpha in the `alpha_id_by_name` dictionary. We truncate
the start of the equity curves so that the resulting DataFrame has always atleast
2 live alphas running at each timestep.
Input:
- client
Client used to communicate with alpha stream REST api
- alpha_id_by_name
Dictionary of alpha IDs, keyed by the alpha name
Returns a DataFrame of normalized live equity curves for the active alphas.
"""
# Get equity curves into a DataFrame
qb = QuantBook()
url = "https://s3.amazonaws.com/alphastreams.quantconnect.com/alphas/equity-unified-live-factors.csv"
csv = qb.Download(url)
equity_curves = pd.read_csv(StringIO(csv))
equity_curves['Time'] = pd.to_datetime(equity_curves['Time'])
equity_curves.set_index('Time', inplace=True)
equity_curves = equity_curves[[alpha_id for alpha_id in alpha_id_by_name.values()]]
equity_curves.columns = [alpha_name for alpha_name in alpha_id_by_name.keys()]
equity_curves = equity_curves.resample('D').mean().interpolate(method='linear', limit_area='inside')
# Drop inactive alphas
inactive_alphas = equity_curves.iloc[-1].isna().values
for alpha_name in equity_curves.columns[inactive_alphas]:
print(f"'{alpha_name}' excluded because it's marked as inactive.")
has_data = equity_curves.columns[~inactive_alphas]
# Truncate start of history to when there are atleast 2 alphas
equity_curves = equity_curves[has_data].dropna(thresh=2)
# Normalize the equity curves
normalized_curves = pd.DataFrame()
for alpha_id in equity_curves.columns:
alpha_equity = equity_curves[alpha_id].dropna()
alpha_equity = alpha_equity / alpha_equity.iloc[0]
normalized_curves = normalized_curves.join(alpha_equity, how = 'outer')
return normalized_curves