Overall Statistics
Total Trades
1804
Average Win
0.54%
Average Loss
-0.43%
Compounding Annual Return
13.289%
Drawdown
55.900%
Expectancy
0.500
Net Profit
422.672%
Sharpe Ratio
0.509
Probabilistic Sharpe Ratio
1.159%
Loss Rate
33%
Win Rate
67%
Profit-Loss Ratio
1.24
Alpha
-0.007
Beta
1.338
Annual Standard Deviation
0.239
Annual Variance
0.057
Information Ratio
0.176
Tracking Error
0.145
Treynor Ratio
0.091
Total Fees
$3812.16
Estimated Strategy Capacity
$1900000.00
Lowest Capacity Asset
CBL R735QTJ8XC9X
Portfolio Turnover
1.00%
from AlgorithmImports import *
from System.Collections.Generic import List

### <summary>
### Demonstration of using coarse and fine universe selection together to filter down a smaller universe of stocks.
### </summary>
### <meta name="tag" content="using data" />
### <meta name="tag" content="universes" />
### <meta name="tag" content="coarse universes" />
### <meta name="tag" content="fine universes" />
class CoarseFineFundamentalComboAlgorithm(QCAlgorithm):

    def Initialize(self):
        # Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.

        self.SetStartDate(2010,1,1)  #Set Start Date
        self.SetEndDate(2023,4,1)    #Set End Date
        self.SetCash(50000)            #Set Strategy Cash

        # what resolution should the data *added* to the universe be?
        self.UniverseSettings.Resolution = Resolution.Minute

        self.default_currency = "USD"
        self.next_selection_month = None
        self.next_rebalance_month = None
        self.rebalance_months = 3
        self.security_ratio = 1.00
        self.forex_ratio = 0.25
        self.forex_live_value = 0
        self.crypto_ratio = 0.35
        self.crypto_live_value = 0

        self.securities = []
        self.coarse_result = []
        self.fine_result = []
        self.invested = {}

        # this add universe method accepts two parameters:
        # - coarse selection function: accepts an IEnumerable<CoarseFundamental> and returns an IEnumerable<Symbol>
        # - fine selection function: accepts an IEnumerable<FineFundamental> and returns an IEnumerable<Symbol>
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)

        self.__numberOfSymbols = 1000
        self.__numberOfSymbolsFine = 20
        self._changes = None


    # sort the data by daily dollar volume and take the top 'NumberOfSymbols'
    def CoarseSelectionFunction(self, coarse):
        # sort descending by daily dollar volume
        sortedByDollarVolume = sorted(coarse, key=lambda x: x.DollarVolume, reverse=True)

        if self.next_selection_month != None and self.next_selection_month != self.Time.month and len(self.coarse_result) > 0:
            self.Log("Ignore Universe (Coarse) -- Time: " + str(self.Time) + "; Result: " + str(self.coarse_result))
            return self.coarse_result

        # return the symbol objects of the top entries from our sorted collection
        self.coarse_result = [ x.Symbol for x in sortedByDollarVolume[:self.__numberOfSymbols] ]

        return self.coarse_result

    # sort the data by P/E ratio and take the top 'NumberOfSymbolsFine'
    def FineSelectionFunction(self, fine):
        if self.next_selection_month != None and self.next_selection_month == self.Time.month and len(self.fine_result) > 0:
            self.Log("Ignore Universe (Fine) -- Time: " + str(self.Time) + "; Result: " + str(self.fine_result))
            return self.fine_result

        # Make note that we did a selection
        self.next_selection_month = (self.Time.month + self.rebalance_months) % 12

        # sort descending by P/E ratio
        sortedByPeRatio = sorted(fine, key=lambda x: x.ValuationRatios.FCFYield, reverse=True)

        # take the top entries from our sorted collection
        self.fine_result = [ x.Symbol for x in sortedByPeRatio[:self.__numberOfSymbolsFine] ]
        return self.fine_result

    # this event fires whenever we have changes to our universe
    def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
        self.Log("Received securities chanegs: " + str(changes))
        self._changes = changes

        # First, make changes to our list of securities and liquidate anything we are not supposed to be in
        if self._changes != None:
            for security in self._changes.RemovedSecurities:
                if security in self.securities:
                    if security.Symbol in self.invested and self.invested[security.Symbol]:
                        self.Log("Sell -- " + str(self.Time) + ": Symbol " + str(security.Symbol) + " is no longer in the universe, selling it.")
                        liquidated = True
                        self.Liquidate(security.Symbol)
                        self.invested[security.Symbol] = False
                    self.securities.remove(security)
                    
            self.securities.extend(self._changes.AddedSecurities)
            self._changes = None

    # This gets called on every tick or trade bar, depending on resolution
    def OnData(self, data: Slice):
        liquidated = False

        # The amount of cash available for stocks is based on the total portfolio value, less any money currently in use by forex and crypto trades
        # We exclude live forex and crypto trades because they have specific profit targets to hit and risk/reward ratios, so we don't want to
        # cash them out early
        self.portfolio_value = self.Portfolio.CashBook[self.default_currency].Amount

        for security in self.securities:
            # Make sure we have trade data for the symbol before we try to do something with it
            if data.ContainsKey(security.Symbol):
                # Add the value of the security based on our cost to acquire it and its total closed profit
                if security.Symbol in self.invested and self.invested[security.Symbol]:
                    self.portfolio_value += self.Portfolio[security.Symbol].TotalCloseProfit() + self.Portfolio[security.Symbol].AbsoluteHoldingsCost
        
        # This is the amount of cash available for securities on a rebalance
        self.security_cash = self.security_ratio * self.portfolio_value

        # Compute the postion size for each security as a target percentage weight
        count = len(self.securities)
        if count == 0: return
        
        pos_size = (self.security_cash / self.portfolio_value) / count

        for security in self.securities:
            # Make sure we have trade data for the symbol before we try to do something with it
            if data.ContainsKey(security.Symbol):
                if not security.Symbol in self.invested or not self.invested[security.Symbol] or self.Time.month == self.next_rebalance_month:
                    self.Log("Buy -- " + str(self.Time) + ": Symbol " + str(security.Symbol) + " is not invested, adding it.")
                    self.SetHoldings(security.Symbol, pos_size)
                    self.invested[security.Symbol] = True

        self.next_rebalance_month = (self.Time.month + 1) % 12

    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status != OrderStatus.Filled:
            return

'''
# region imports
from AlgorithmImports import *
# endregion

class AutomatedPortfolioManager(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2010, 1, 1)  # Set Start Date
        self.SetCash(20000)  # Set Strategy Cash
        self.UniverseSettings.Resolution = Resolution.Minute
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)        
        self.portfolio_value = 0.0 # Initial value; updated automatically on data reception

        self.last_selection_month = None
        self.default_currency = "USD"
        self.default_symbol = self.AddSecurity(SecurityType.Equity, "VTI").Symbol

        self.security_ratio = 1.00
        self.forex_ratio = 0.25
        self.forex_live_value = 0
        self.crypto_ratio = 0.35
        self.crypto_live_value = 0

        self.securities = []
        self.invested = {}

        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)

    # Selection functions run every trade day at market open, before trade bars arrive
    def CoarseSelectionFunction(self, coarse: List[CoarseFundamental]) -> List[Symbol]:
        # Allow selection on first run, and also once per month
        if self.last_selection_month != None and self.last_selection_month != self.Time.month:
            return

        # We want to start with just the top 1000 most liquid stocks to minimize slippage
        #unsorted = [x for x in coarse if x.HasFundamentalData]
        #sortedByDollarVolume = sorted(unsorted, key=lambda x: x.DollarVolume, reverse=True)
        #filtered = [x.Symbol for x in sortedByDollarVolume]

        #if len(filtered) == 0:
        filtered = [self.default_symbol]

        return filtered[:1]

    def FineSelectionFunction(self, fine: List[FineFundamental]) -> List[Symbol]:
        # Allow selection on first run, and also once per month
        if self.last_selection_month != None and self.last_selection_month != self.Time.month:
            return

        # Sort securities by cost per dollar of Free Cash Flow
        #sortedByPeRatio = [x for x in fine] #sorted(fine, key=lambda x: (x.ValuationRatios.FCFYield), reverse=False)

        # Make note that we did a selection
        self.last_selection_month = self.Time.month

        #symbols = [x.Symbol for x in sortedByPeRatio[:10]]
        #if len(symbols) == 0:
        symbols = [self.default_symbol]

        return symbols

    # this event fires whenever we have changes to our universe
    def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
        self._changes = changes

    # This gets called on every tick or trade bar, depending on resolution
    def OnData(self, data: Slice):
        liquidated = False

        # First, make changes to our list of securities and liquidate anything we are not supposed to be in
        if self._changes != None:
            for security in self._changes.RemovedSecurities:
                if security in self.securities:
                    if security.Symbol in self.invested and self.invested[security.Symbol]:
                        liquidated = True
                        self.Liquidate(security.Symbol)
                        self.invested[security.Symbol] = False
                    self.securities.remove(security)
                    
            self.securities.extend(self._changes.AddedSecurities)
            self._changes = None

            # If we removed an asset, give it time to close; note: we use minute bar data, so it has a whole minute
            if liquidated: return

        # The amount of cash available for stocks is based on the total portfolio value, less any money currently in use by forex and crypto trades
        # We exclude live forex and crypto trades because they have specific profit targets to hit and risk/reward ratios, so we don't want to
        # cash them out early
        self.portfolio_value = self.Portfolio.CashBook[self.default_currency].Amount

        for security in self.securities:
            # Add the value of the security based on our cost to acquire it and its total closed profit
            if security.Symbol in self.invested and self.invested[security.Symbol]:
                self.portfolio_value += self.Portfolio[security.Symbol].TotalCloseProfit() + self.Portfolio[security.Symbol].AbsoluteHoldingsCost
        
        # This is the amount of cash available for securities on a rebalance
        self.security_cash = self.security_ratio * self.portfolio_value

        # Compute the postion size for each security as a target percentage weight
        count = len(self.securities)
        pos_size = (self.security_cash / self.portfolio_value) / count

        for security in self.securities:
            if not security.Symbol in self.invested or not self.invested[security.Symbol]:
                self.SetHoldings(security.Symbol, pos_size)
                self.invested[security.Symbol] = True

    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status != OrderStatus.Filled:
            return
'''