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")