Overall Statistics |
Total Trades 3822 Average Win 0.07% Average Loss -0.07% Compounding Annual Return -1.655% Drawdown 10.600% Expectancy -0.044 Net Profit -5.493% Sharpe Ratio -0.383 Probabilistic Sharpe Ratio 0.579% Loss Rate 53% Win Rate 47% Profit-Loss Ratio 1.03 Alpha -0.015 Beta -0.004 Annual Standard Deviation 0.041 Annual Variance 0.002 Information Ratio -0.699 Tracking Error 0.212 Treynor Ratio 3.96 Total Fees $3964.22 |
def InitCharts(algorithm): performance_plot = Chart('Performance Breakdown') performance_plot.AddSeries(Series('Total Fees', SeriesType.Line, 0)) performance_plot.AddSeries(Series('Total Gross Profit', SeriesType.Line, 0)) algorithm.AddChart(performance_plot) exposure_plot = Chart('Exposure/Leverage') exposure_plot.AddSeries(Series('Gross', SeriesType.Line, 0)) exposure_plot.AddSeries(Series('Net', SeriesType.Line, 0)) algorithm.AddChart(exposure_plot) country_exposure_plot = Chart('Country Exposure') for etf, country in algorithm.etf_country.items(): country_exposure_plot.AddSeries(Series(country, SeriesType.Line, 0)) def PlotPerformanceChart(algorithm): algorithm.Plot('Performance Breakdown', 'Total Fees', algorithm.Portfolio.TotalFees) algorithm.Plot('Performance Breakdown', 'Total Gross Profit', algorithm.Portfolio.TotalProfit) def PlotExposureChart(algorithm): long_val = 0 short_val = 0 for security, v in algorithm.Portfolio.items(): if v.Invested: val = v.AbsoluteHoldingsValue if v.IsLong: long_val += val elif v.IsShort: short_val += val total_equity = algorithm.Portfolio.TotalPortfolioValue gross = (long_val + short_val) / total_equity net = (long_val - short_val) / total_equity algorithm.Plot('Exposure/Leverage', 'Gross', gross) algorithm.Plot('Exposure/Leverage', 'Net', net) def PlotCountryExposureChart(algorithm): for etf, country in algorithm.etf_country.items(): exposure = algorithm.Securities[etf].Holdings.HoldingsValue / algorithm.Portfolio.TotalHoldingsValue algorithm.Plot('Country Exposure', country, exposure)
import pandas as pd import numpy as np import cvxpy as cv class OptimisationPortfolioConstructionModel: def __init__(self, turnover, max_wt, longshort, mkt_neutral): self.turnover = turnover self.max_wt = max_wt self.longshort = longshort self.mkt_neutral = mkt_neutral def GenerateOptimalPortfolio(self, algorithm, alpha_df): # algorithm.Log("Generating target portfolio...") alphas = alpha_df['alpha_score'] optimal_portfolio = self.Optimise(algorithm, self.AddZeroHoldings(algorithm, alphas)) # algorithm.Log(f"Created a portfolio of {len(optimal_portfolio[optimal_portfolio != 0])} securities") return optimal_portfolio def AddZeroHoldings(self, algorithm, portfolio): zero_holding_securities = [str(s.Symbol) for s in algorithm.Portfolio.Values if s.Invested and str(s.Symbol) not in portfolio.index] for security in zero_holding_securities: portfolio.loc[security] = 0 return portfolio def Optimise(self, algorithm, alphas): invested_securities = [security for security in algorithm.Portfolio.Values if security.Invested] if len(invested_securities) == 0: algorithm.Log('Initial portfolio rebalance') self.initial_rebalance = True turnover = 1 initial_portfolio = pd.DataFrame(columns=['symbol', 'weight', 'alpha']).set_index('symbol') else: self.initial_rebalance = False turnover = self.turnover initial_portfolio = pd.DataFrame.from_records( [ { 'symbol': str(security.Symbol), 'weight': security.HoldingsValue / algorithm.Portfolio.TotalHoldingsValue, 'alpha': alphas.loc[security] if security in alphas.index else 0, } for security in invested_securities ]).set_index('symbol') for security, alpha in alphas.iteritems(): if security not in initial_portfolio.index: initial_portfolio.loc[security, 'weight'] = 0 initial_portfolio.loc[security, 'alpha'] = alpha for i in range(int(turnover * 100), 101, 1): to = i / 100 optimiser = Optimiser(initial_portfolio, turnover=to, max_wt=self.max_wt, mkt_neutral=self.mkt_neutral) optimal_portfolio, optimisation_status = optimiser.optimise() if optimisation_status != 'optimal': algorithm.Log(f'Optimisation with {to} turnover not feasible: {optimisation_status}') else: break return optimal_portfolio class Optimiser: def __init__(self, initial_portfolio, turnover, max_wt, longshort=True, mkt_neutral=True): self.symbols = np.array(initial_portfolio.index) self.init_wt = np.array(initial_portfolio['weight']) self.opt_wt = cv.Variable(self.init_wt.shape) self.alpha = np.array(initial_portfolio['alpha']) self.longshort = longshort self.mkt_neutral = mkt_neutral self.turnover = turnover self.max_wt = max_wt if self.longshort: self.min_wt = -self.max_wt self.net_exposure = 0 self.gross_exposure = 1 else: self.min_wt = 0 self.net_exposure = 1 self.gross_exposure = 1 def optimise(self): constraints = self.get_constraints() optimisation = cv.Problem(cv.Maximize(cv.sum(self.opt_wt * self.alpha)), constraints) optimisation.solve() status = optimisation.status if status == 'optimal': optimal_portfolio = pd.Series( np.round(optimisation.solution.primal_vars[list(optimisation.solution.primal_vars.keys())[0]], 3), index=self.symbols) else: optimal_portfolio = pd.Series(np.round(self.init_wt, 3), index=self.symbols) return optimal_portfolio, status def get_constraints(self): min_wt = self.opt_wt >= self.min_wt max_wt = self.opt_wt <= self.max_wt turnover = cv.sum(cv.abs(self.opt_wt - self.init_wt)) <= self.turnover * 2 net_exposure = cv.sum(self.opt_wt) == self.net_exposure gross_exposure = cv.sum(cv.abs(self.opt_wt)) <= self.gross_exposure if self.mkt_neutral: return [min_wt, max_wt, turnover, net_exposure, gross_exposure] else: return [min_wt, max_wt, turnover, gross_exposure]
import pandas as pd from io import StringIO from datetime import timedelta from portfolio_construction import OptimisationPortfolioConstructionModel from execution import Execution from charting import InitCharts, PlotPerformanceChart, PlotExposureChart, PlotCountryExposureChart def normalise(series, equal_ls=True): if equal_ls: series -= series.mean() sum = series.abs().sum() return series.apply(lambda x: x / sum) class StockifySentiment(QCAlgorithm): def Initialize(self): self.SetStartDate(2017, 1, 1) # Set Start Date self.SetEndDate(2020, 5, 20) self.SetCash(100000) # Set Strategy Cash # Weighting style - normalise or alpha_max (alpha maximisation w/ optimisation) self.weighting_style = 'normalise' # Market neutral self.mkt_neutral = True # Audio feature to use self.audio_feature = 'valence' # Get data self.data, self.etf_list, self.etf_country = self.DataSetup() # Add ETFs for etf in self.etf_list: self.AddEquity(etf, Resolution.Minute) # Portfolio construction model self.CustomPortfolioConstructionModel = OptimisationPortfolioConstructionModel(turnover=1, max_wt=0.2, longshort=True, mkt_neutral=self.mkt_neutral) # Execution model self.CustomExecution = Execution(liq_tol=0.005) # Schedule rebalancing self.Schedule.On(self.DateRules.Every(DayOfWeek.Wednesday), self.TimeRules.BeforeMarketClose('IVV', 210), Action(self.RebalancePortfolio)) # Init charting InitCharts(self) # Schedule charting self.Schedule.On(self.DateRules.Every(DayOfWeek.Wednesday), self.TimeRules.BeforeMarketClose('IVV', 5), Action(self.PlotCharts)) def OnData(self, data): pass def RebalancePortfolio(self): df = self.data.loc[self.Time - timedelta(7):self.Time].reset_index().set_index('symbol') if self.weighting_style == 'normalise': portfolio = normalise(df['alpha_score'], equal_ls=self.mkt_neutral) elif self.weighting_style == 'alpha_max': df = df[['alpha_score']] portfolio = self.CustomPortfolioConstructionModel.GenerateOptimalPortfolio(self, df) else: raise Exception('Invalid weighting style') self.CustomExecution.ExecutePortfolio(self, portfolio) def PlotCharts(self): PlotPerformanceChart(self) PlotExposureChart(self) PlotCountryExposureChart(self) def DataSetup(self): df = pd.read_csv(StringIO( self.Download('https://raw.githubusercontent.com/Ollie-Hooper/StockifySentiment/master/data/scores.csv'))) data = df[['date', 'country', f's_{self.audio_feature}']].copy() data['date'] = pd.to_datetime(data['date']) data.rename(columns={f's_{self.audio_feature}': 'alpha_score'}, inplace=True) etf_df = pd.read_csv(StringIO( self.Download('https://raw.githubusercontent.com/Ollie-Hooper/StockifySentiment/master/data/etf.csv'))) data = pd.merge(data, etf_df) data = data.sort_values('date') data.set_index(['date', 'symbol'], inplace=True) data = data[['alpha_score']] return data, etf_df['symbol'].to_list(), etf_df.set_index('symbol')['country_name'].to_dict()
class Execution(): def __init__(self, liq_tol): self.liq_tol = liq_tol def ExecutePortfolio(self, algorithm, portfolio): # algorithm.Log(f"Executing portfolio trades...") liquidate_securities = portfolio[abs(portfolio) < self.liq_tol].index holding_port = portfolio[abs(portfolio) >= self.liq_tol] self.LiquidateSecurities(algorithm, liquidate_securities) self.SetPortfolioHoldings(algorithm, holding_port) def LiquidateSecurities(self, algorithm, securities): liquid_count = 0 for security in securities: if algorithm.Securities[security].Invested: algorithm.Liquidate(security) liquid_count += 1 # algorithm.Log(f"Successfully liquidated {liquid_count} securities") def SetPortfolioHoldings(self, algorithm, portfolio): # algorithm.Log(f"Setting portfolio holdings for {len(portfolio)} securities...") for security, weight in portfolio.iteritems(): algorithm.SetHoldings(security, weight) # algorithm.Log(f"Successfully set all holdings")