Overall Statistics
Total Trades
4002
Average Win
0.53%
Average Loss
-0.39%
Compounding Annual Return
20.715%
Drawdown
40.300%
Expectancy
0.232
Net Profit
323.842%
Sharpe Ratio
0.785
Probabilistic Sharpe Ratio
19.130%
Loss Rate
48%
Win Rate
52%
Profit-Loss Ratio
1.37
Alpha
0.075
Beta
1.028
Annual Standard Deviation
0.268
Annual Variance
0.072
Information Ratio
0.367
Tracking Error
0.215
Treynor Ratio
0.204
Total Fees
$4060.49
Estimated Strategy Capacity
$6800000.00
Lowest Capacity Asset
ENPH V5C9T324VA05
from datetime import timedelta, time
from AlgorithmImports import *
from alpha import FundamentalFactorAlphaModel
import pandas as pd


class TutorialFundamental(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2014, 1, 1)  # Set Start Date
        self.SetCash(15000)  # Set Strategy Cash
        self.SetPortfolioConstruction(InsightWeightingPortfolioConstructionModel()) #Dette er vores portfefølje model, idet det kan lade os styre hvor stor procentdel der skal allokeres til diverse aktier
        self.SetExecution(ImmediateExecutionModel()) #Dette er vores executionmodel, idet at alle andre modeller (altså færdiglavet) introducerer gearing.
        self.SetRiskManagement(MaximumDrawdownPercentPerSecurity(0.07)) #Max drawdown, idet vi ikke vil have for meget drawdown på de enkelte aktier. Her kan 7% også skiftes ud med 10% eller andet. 
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin) #For at gøre det så realistisk som muligt. 
        self.SetWarmUp(timedelta(365))


        self.UniverseSettings.Resolution = Resolution.Hour#Kan også skiftes ud med Resolution.Daily
        self.AddUniverse(self.CoarseSelectionModel, self.FineSelectionModel) #Vores univers settings. 
        self.SetBenchmark('SPY')

        self.num_coarse = 200 #antallet af aktier der kan komme igennem vores coarse filter
        self.final_stocks = 10 #antallet af aktier som vi køber

        self.lastMonth = -1 #Bruges til at sætte vores rebalance funktion.

        #Vægten af vores 3 parametre, nemlig kvalitet, størrelse, og værdiansættelse. Size burde måske skiftes ud med momentum?
        quality_weight = 2 
        value_weight= 2


        #Her indsætter vi nogle parametre, idet vi har brug for dem i vores alphamodel
        self.AddAlpha(FundamentalFactorAlphaModel(self.final_stocks, quality_weight, value_weight))

    #for at plotte nogle features, som eksempelvis om vi bruger gearing (margin)
    def OnEndOfDay(self):
        self.Plot("Positions", "Num", len([x.Symbol for x in self.Portfolio.Values if self.Portfolio[x.Symbol].Invested]))
        self.Plot(f"Margin", "Used", self.Portfolio.TotalMarginUsed)
        self.Plot(f"Margin", "Remaining", self.Portfolio.MarginRemaining)
        self.Plot(f"Cash", "Remaining", self.Portfolio.Cash)


    def CoarseSelectionModel(self, coarse):
        #Rebalanceringsfunktion
        if self.Time.month == self.lastMonth:
            return Universe.Unchanged
        self.lastMonth = self.Time.month

        #Sotering af aktier, her i forhold til om vores aktie har fundamental data, om prisen er over 5 dollars og sorterer derefter efter højst volumen
        selected = sorted([x for x in coarse if x.HasFundamentalData and x.Price > 5], 
                        key = lambda x: x.DollarVolume, reverse=True)
        #Retunerer 200 symboler
        return [x.Symbol for x in selected[:self.num_coarse]]


    def FineSelectionModel(self, fine):
        #Vores fine selection model, som filtrerer aktier ud, som har følgende nøgletal over nul, idet nogle aktier kan have nøgletal på nul, da de ikke findes. 
        filtered_fine = [x.Symbol for x in fine if x.OperationRatios.GrossMargin.Value > 0 
                                                    and x.OperationRatios.QuickRatio.Value > 0
                                                    and x.OperationRatios.DebttoAssets.Value > 0
                                                    and x.ValuationRatios.BookValuePerShare > 0
                                                    and x.ValuationRatios.CashReturn > 0
                                                    and x.ValuationRatios.EarningYield > 0]



        rankBy360 = self.Returns(filtered_fine, 360)


        sorteret_mom360 = self.Sortere(rankBy360)


        sorteret_mom360 = [x[0] for x in sorteret_mom360[:10]]

        #Returnerer listen, stadig med 200 symboler.
        return [x for x in filtered_fine if str(x) in sorteret_mom360]


    def Returns(self, historie, periode):
        historie = self.History(historie, periode, Resolution.Daily)
        historie = historie.drop_duplicates().close.unstack(level = 0)
        afkast = historie.iloc[0] / historie.iloc[-1]

        afkast = afkast.to_dict()

        rangeret = sorted(afkast, key=afkast.get, reverse = True)
        return {symbol: rank for rank, symbol in enumerate(rangeret, 1)}


    def Sortere(self, historie):
        historie = {key: historie.get(key, 0)
                        for key in set(historie)}

        historie = sorted(historie.items(), key = lambda x: x[1], reverse=True)

        return historie
from AlgorithmImports import *
from datetime import timedelta
from System.Collections.ObjectModel import ReadOnlyDictionary
import pandas as pd

from System.Diagnostics import Debug

class FundamentalFactorAlphaModel(AlphaModel): #Her indsætter vi 'AlphaModel' idet at det er en alpha model vi laver. Det er altså en subclass af alphamodel. 
    def __init__(self, final_stocks, quality_weight, value_weight): #Vi starter med at implementerer en initialize funktion, dog bare init i stedet for initialize
        self.lastMonth = -1 #Hjælper med rebalancering
        self.longs = [] #En liste som skal indeholde de symboler vi gerne vil købe. 
        self.final_stocks = final_stocks 
        self.period = timedelta(31) #Vi holder hvert symbol i 31 dage

        #Tilføjer alle vores værdier til en liste, og herefter deler vi hvert element i listen med summen af listen. 
        weights = [quality_weight, value_weight] 
        weights = [float(i)/sum(weights) for i in weights]
        #Weights kommer til at hedde [0.4, 0.2, 0.4], hvilket giver 1, da vores Insights ikke må være mere end 1

        #Vi tager altså det første element, og tildeler til self.qu..., osv. 
        self.quality_weight = weights[0]
        self.value_weight = weights[1]


        
    def Update(self, algorithm, data):
        #Rebalanceringsfunktion
        if algorithm.Time.month == self.lastMonth:
            return []
        self.lastMonth = algorithm.Time.month
        
        #Her er det vi opbevarer vores insights, hvor at vi til sidst retunerere vores insights
        insights = []

        #Hvis vores symboler ikke er i self.long, så sender vi et flat signal, hvilket betyder at vi 
        #skal likvidere vores portefølje
        for security in algorithm.Portfolio.Values:
            if security.Invested and security.Symbol not in self.longs:
                insights.append(Insight(security.Symbol, self.period, InsightType.Price, InsightDirection.Flat, 
                                        None, None, None, None))

        #vores symboler i self.longs skal vi sende et langt signal
        lenght = len(self.longs)
        for i in range(lenght):
            insights.append(Insight(self.longs[i], self.period, InsightType.Price, InsightDirection.Up, 
                                None, (lenght - i)**2, None, (lenght - i)**2))
    

        #returnere insights
        return insights

    def OnSecuritiesChanged(self, algorithm, changes):
        #Tilføjer alle vores added securities til en listen, nemlig added
        added = [x for x in changes.AddedSecurities]

        #Her tager vi hvert element i vores fundamentale, og laver en lambda funktion over tallet. Her betyder "True", om det er godt at have et lavt, eller et højt tal. Hvis True
        #så er det godt at have et højt tal, og omvendt. Vores float, betyder hvilken vægt at det skal have. Det er normalized vægt, hvilket betyder at det er forholdet mellem tallene
        # der betyder noget, og ikke selve tallene. 
        #Herefter så smider vi tallene ind i vores egen funktion, nemlig Scores. Til sidst bliver vores resultat fra funktionen gemt i vores forskellige variabler.
        quality_scores = self.Scores(added, [(lambda x: x.Fundamentals.OperationRatios.GrossMargin.Value, True, 2),
                                            (lambda x: x.Fundamentals.OperationRatios.QuickRatio.Value, True, 1),
                                            (lambda x: x.Fundamentals.OperationRatios.DebttoAssets.Value, False, 2)])

        value_scores = self.Scores(added, [(lambda x: x.Fundamentals.ValuationRatios.BookValuePerShare, True, 0.5),
                                            (lambda x: x.Fundamentals.ValuationRatios.CashReturn, True, 0.25),
                                            (lambda x: x.Fundamentals.ValuationRatios.EarningYield, False, 0.25)])


        #Vi laver et tomt dictionary
        scores = {}
        #Her laver vi et for loop over vores dictionary, hvor vi har 2 variabler. Nemlig symbol og value. 
        for symbol, value in quality_scores.items():
            #Her gemmer vi jsymbolerne i hver af vores liste, da det er blevet delt op og sorteret
            quality_rank = value
            value_rank = value_scores[symbol]

            #Til sidst så ganger vi vores vægt på, som vi fik fra vores alphamodel. Disse variabler er blevet gemt i vores main.py
            scores[symbol] = quality_rank*self.quality_weight + value_rank*self.value_weight

        #Vi skal altså sorterer vores symboler og vægt igen, da vi har ganget en vægt på. Her er den laveste vægt det bedste. Her sorterer vi et dictionary, derfor skal vi have fat i tup[1]
        sorted_stock = sorted(scores.items(), key = lambda tup: tup[1], reverse=False)
        #Nu skal vi have fat i de 10 første symboler, som vi gør med denne funktion. 
        sorted_symbol = [tup[0] for tup in sorted_stock][:self.final_stocks]

        #Her skal vi bare have fat i vores symboler, og ikke værdien, så derfor skal vi lave dette for loop. 
        self.longs = [security.Symbol for security in sorted_symbol]

        algorithm.Log(", ".join(str(x.Symbol.Value) + ": " + str(scores[x]) for x in sorted_symbol))

    #Vores egen funktion for at udregne scoren
    def Scores(self, added, fundamentals):
        #3 korte linjer der tjekker om vi overhovedet har nogle fundamentale værdier at gå igennem. Hvis vi ikke har, så er det ligemeget. 

        lenght = len(fundamentals)
        if lenght == 0:
            return {}
        
        #Vi indledder to variabler 
        scores = {}
        sortedBy = []
        
        #Her laver vi et for loop som looper igennem alle variabler i fundamentals, og sætter dem lig med 0
        rank = [0 for _ in fundamentals]

        #fundamentals er en streng, hvor at vi lopper igennem de forskellige strenge, og vi udtager det 3 element
        #i strengen, idet at det er vores fundamentale værdi
        weights = [tup[2] for tup in fundamentals]
        #Herefter normaliserer vi vægten
        weights = [float(i)/sum(weights) for i in weights]

        #Her sorterer vi vores fundamentale værdier, hvor at de første værdier, er det bedste værdier
        for tup in fundamentals:
            #Her tilføjer vi værdierne til vores liste
            sortedBy.append(sorted(added, key=tup[0], reverse=tup[1]))

        #Her sætter vi et index for hver security, hvor at vi laver en enumeration over vores sortedBy
        for index, symbol in enumerate(sortedBy[0]):
            rank[0] = index
            for j in range(1, lenght):
                rank[j] = sortedBy[j].index(symbol)

            #Til sidst laver vi et loop, hvor at vi udregner vores score
            score = 0
            for i in range(lenght):
                score += rank[i] * weights[i]
            scores[symbol] = score

        #Til sidst retunerer vi vores streng
        return scores