Overall Statistics
Total Orders
32698
Average Win
0.18%
Average Loss
-0.18%
Compounding Annual Return
7.045%
Drawdown
32.800%
Expectancy
0.815
Start Equity
10000000
End Equity
14144135.39
Net Profit
41.441%
Sharpe Ratio
0.269
Sortino Ratio
0.26
Probabilistic Sharpe Ratio
6.399%
Loss Rate
9%
Win Rate
91%
Profit-Loss Ratio
1.00
Alpha
0.031
Beta
0.569
Annual Standard Deviation
0.131
Annual Variance
0.017
Information Ratio
0.244
Tracking Error
0.111
Treynor Ratio
0.062
Total Fees
$8056.01
Estimated Strategy Capacity
$40000.00
Lowest Capacity Asset
JEQ R735QTJ8XC9X
Portfolio Turnover
0.06%
from AlgorithmImports import *
class CustomDataEMFAlgorithm(QCAlgorithm):

    def initialize(self):

        self.set_start_date(2019, 1, 1)
        self.set_end_date(2024, 2, 1)
        self.set_cash(10000000)
        self.activeSecurities = set()

        self.Settings.FreePortfolioValuePercentage = 0.05

        ### Set benchmark
        # self.benchmark_symbol = self.get_parameter("benchmark_ticker")
        self.set_benchmark("EEM")
        self.AddEquity("EEM", Resolution.HOUR)

        universe = self.add_universe(MyCustomUniverseDataClass, "myCustomUniverse", self.selector_function)
        self.universe_settings.resolution = Resolution.DAILY

        self.portfolioTargets = []
        self.aro_data = {1: {}, 3: {}, 5: {}} # Add this line to initialize the dictionary
        self.cef_discount_data = {} 
        self.cef_purchase_price = {}
        self.cef_purchase_discount = {}
        self.cef_purchase_percentile = {}

        ### Helper variables
        self.take_profit_active = {} #Is take profit flag activated?
        self.profit_threshold = {}

        ### Other Helper variables
        self.firstOrderTicket = {}
        self.firstOrderTicketFillFlag = {}
        self.takeProfitMarketTicket = {}
        self.firstOrderEntryTime = {}
        self.firstOrderEntryTicketFillTime = datetime.min
        self.dipsTimeCheck = {}
        self.invalidOrderReSubmit = {}
        self.invalidOrderCount = 0

        self.averageFillPrices = {}
        self.buyDipsMarketTicket = {}

        ### Define take profit parameters - also used in trimming
        self.take_profit_percent = float(self.GetParameter("take_profit_percent"))
        self.price_rise_percent = float(self.GetParameter("price_rise_percent"))

        ### Define discount percentile upper and lower limits below
        self.aro_dptl_lower_limit = float(self.get_parameter("dptl_lower_limit"))
        self.aro_dptl_upper_limit = float(self.get_parameter("dptl_upper_limit"))

        ### Define discount differential 
        self.discount_differential = float(self.get_parameter("discount_differential_limit"))

        ###Define period whether, 1, 3, or 5 year below
        self.period = int(self.get_parameter("period_selection"))

     # Define the selector function
    def selector_function(self, data):
        sorted_data = sorted([ x for x in data if x["CEF_Discount"] <= 4 ],
                            key=lambda x: x.end_time) #,
                            # reverse=True)
        self.Log('Stocks trading at a discount to NAV: ' + str(len(sorted_data)))
        for x in sorted_data:
            # self.aro_data[5][x.symbol] = x["ARO_DPTL_5YR"]
            # self.aro_data[3][x.symbol] = x["ARO_DPTL_3YR"]
            self.aro_data[1][x.symbol] = x["ARO_DPTL_1YR"]
            self.cef_discount_data[x.symbol] = x["CEF_Discount"]
        return [x.symbol for x in sorted_data]    

    def on_securities_changed(self, changes):
        # remove stocks from active secutiries positions
        for x in changes.RemovedSecurities:
            if x.Symbol in self.activeSecurities:
                self.activeSecurities.remove(x.Symbol)
                         

        # can't open positions here since data might not be added correctly yet
        for x in changes.AddedSecurities:
            # Skip if the added security is the benchmark
            if x.Symbol.Value == "EEM":
                continue
            self.Debug(f"{self.Time}: Added {x.Symbol}")
            self.activeSecurities.add(x.Symbol)
            self.Securities[x.Symbol].SetLeverage(1.0)
            

        # adjust targets if universe has changed
        self.portfolioTargets = [PortfolioTarget(symbol, 1/len(self.activeSecurities)) 
                            for symbol in self.activeSecurities]
        
    def OnData(self, data):



        # check whether 30 days passed since 1st buy order before selling
        
        if self.portfolioTargets == []:
            return
            
        for target in self.portfolioTargets:
            symbol = target.Symbol
            price = round(self.Securities[symbol].Price,2)  # Get the current price
            aro_value = round(self.aro_data[self.period][symbol],2)
            cef_discount_value = self.cef_discount_data[symbol] # Get the CEF discount value
           
            ###Logic for topping-up
            # Step 1: Calculate buying power and order size
            buyingPower = self.Portfolio.MarginRemaining
            price = self.Securities[symbol].Price
            if price != 0:
                orderSize = max(1, buyingPower / price)
            else:
                self.Debug(f"{symbol} Price is ({price}) on {self.time}, cannot calculate order size")
                return

            # Step 2: Check top-up conditions are met
            if self.Portfolio[symbol].Invested and not self.transactions.get_open_orders(symbol) \
                and self.Portfolio[symbol].HoldingsValue < self.Portfolio.TotalPortfolioValue * 0.1 \
                    and aro_value < self.aro_dptl_lower_limit \
                    and (abs(cef_discount_value - self.cef_purchase_discount[symbol]) > self.discount_differential or price < self.Portfolio[symbol].average_price): # and (self.Time - self.dipsTimeCheck[symbol]).days >= 1:
                
                # self.Debug(f"time: {self.time}")
                # self.Debug(f"symbol: {symbol}")
                # self.Debug(f"self.Portfolio[symbol].Invested: {self.Portfolio[symbol].Invested}")
                # self.Debug(f"self.firstOrderTicketFillFlag[symbol]: {self.firstOrderTicketFillFlag[symbol]}")
                # self.Debug(f"self.Portfolio[symbol].HoldingsValue: {self.Portfolio[symbol].HoldingsValue}")
                # self.Debug(f"self.Portfolio.TotalPortfolioValue: {self.Portfolio.TotalPortfolioValue}")
                # self.Debug(f"cef_discount_value: {cef_discount_value}")
                # self.Debug(f"self.Portfolio[symbol].average_price: {self.Portfolio[symbol].average_price}")
                # self.Debug(f"self.cef_purchase_discount: {self.cef_purchase_discount[symbol]}")
                
                # Step 3: Adjust the order size according to the available buying power
                adjusted_order_size = min(orderSize, self.Portfolio[symbol].Quantity * self.take_profit_percent)

                self.buyDipsMarketTicket[symbol] = self.limit_order(symbol, adjusted_order_size, \
                    price,tag=f"Topping up {symbol} @ Discount: {cef_discount_value} / {self.cef_purchase_discount[symbol]} @ ARO_DPTL_{self.period}YR: {aro_value} @ Price: {price} / {round(self.Portfolio[symbol].average_price,2)}" )
                # self.dipsTimeCheck[symbol] = self.Time

            ###Logic for trimming positions
            if self.Portfolio[symbol].Invested and not self.transactions.get_open_orders(symbol) \
                and aro_value > self.aro_dptl_upper_limit \
                    and abs(cef_discount_value - self.cef_purchase_discount[symbol]) > self.discount_differential: # or (price / self.averageFillPrices[symbol] > 1+self.price_rise_percent):

                orderSize = min(-1, -self.Portfolio[symbol].Quantity * self.take_profit_percent)
                self.takeProfitMarketTicket[symbol] = self.limit_order(symbol, orderSize, \
                    price,tag=f"Trimming {symbol} @ Discount: {cef_discount_value} / {self.cef_purchase_discount[symbol]} @ ARO_DPTL_{self.period}YR: {aro_value} @ Price: {price} / {self.cef_purchase_price[symbol]}")
    
            ### INITIATE BUY
            if not self.Portfolio[symbol].Invested and not self.transactions.get_open_orders(symbol) and aro_value < self.aro_dptl_lower_limit and cef_discount_value < 0:
                self.cef_purchase_discount[symbol] = cef_discount_value
                self.cef_purchase_price[symbol] = price
                self.cef_purchase_percentile[symbol] = aro_value
                self.SetHoldings([target], tag=f"Initiate BUY {symbol} @ Discount: {cef_discount_value} @ ARO_DPTL_{self.period}YR: {aro_value}") # Buy
                self.log(f"Initiate BUY {symbol} @ Discount: {cef_discount_value} @ ARO_DPTL_{self.period}YR: {aro_value}") # Buy
                self.firstOrderEntryTime[symbol] = self.time

            ### RE-INITIATE INVALID BUY INVALID DUE TO INSUFFICIENT MARGIN
            if symbol in self.invalidOrderReSubmit and not self.Portfolio[symbol].Invested and self.invalidOrderReSubmit[symbol].Status == OrderStatus.INVALID:
                self.Debug(f"Im in ondata and invalidOrderReSubmit {symbol}: {self.invalidOrderReSubmit[symbol]}")
                # Calculate the available buying power and order size and only if > 0 proceed
                buyingPower = self.Portfolio.MarginRemaining
                self.Debug(f"self.Portfolio.MarginRemaining: {self.Portfolio.MarginRemaining}")
                if buyingPower > 0 and self.invalidOrderCount < 2 :
                    self.Debug(f"Ondata Counter: {self.invalidOrderCount}")
                    price = self.Securities[symbol].Price
                    self.Debug(f"price after margin check: {price}")
                    orderSize = max(1, buyingPower / price)
                    # Calculate the total value of the order 
                    total_order_value = price * orderSize
                    ten_percent_portfolio = 1 * self.Portfolio.TotalPortfolioValue
                    # Check if the total order value is greater than 10% of the portfolio value
                    if total_order_value > ten_percent_portfolio:
                        # Adjust the order size
                        orderSize = ten_percent_portfolio / price
                        self.Debug(f"Adjusted order size to maintain 10% portfolio value limit: {orderSize}")
                    self.Debug(f"ordersize after margin check: {orderSize}")
                # Re-submit invalid order with new size and clear 
                    self.cef_purchase_discount[symbol] = cef_discount_value
                    self.cef_purchase_price[symbol] = price
                    self.cef_purchase_percentile[symbol] = aro_value
                    self.market_order(symbol, orderSize, tag=f"Initiate BUY re-submit w/lower size @ Discount: {cef_discount_value} @ ARO_DPTL_{self.period}YR: {aro_value}")
                    self.Debug(f"Initiate BUY re-submit w/lower size ({orderSize}) @ Discount: {cef_discount_value} @ ARO_DPTL_{self.period}YR: {aro_value}")
                    self.firstOrderEntryTime[symbol] = self.time
                    # del self.invalidOrderReSubmit[symbol]
                    

            elif (self.time - self.firstOrderEntryTicketFillTime).days >= 30 and cef_discount_value > 0 and self.Portfolio[symbol].Invested and abs(cef_discount_value - self.cef_purchase_discount[symbol]) > self.discount_differential and (price / self.cef_purchase_price[symbol]) > 1:
                if self.Portfolio[symbol].Quantity <= 0:
                    return
                self.Debug(f"Liquidating {symbol} at {self.Time}, First order entry time: {self.firstOrderEntryTicketFillTime}, Days passed = {(self.time - self.firstOrderEntryTicketFillTime).days}, Price: {price}, purchase price @ {self.cef_purchase_price[symbol]} ARO_DPTL_{self.period}YR: {aro_value}, Selling Discount: {cef_discount_value}, Purchased at Discount: {self.cef_purchase_discount[symbol]}")  
                self.Liquidate(symbol, f"Liquidating {symbol} @ Discount: {cef_discount_value} - Differential: {cef_discount_value - self.cef_purchase_discount[symbol]}") # Sell
                self.take_profit_active[symbol] = False

            self.Plot("ARO Data", f"ARO_DPTL_{self.period}YR", self.aro_data[self.period][symbol])
            self.Plot("ARO Data", "CEF_Discount", cef_discount_value)
            self.Plot("ARO Data", "Price", price)
            benchmark_value = self.Benchmark.Evaluate(self.Time)
            self.Plot("ARO Data", "Benchmark:EWL", benchmark_value)
        
        
        # self.portfolioTargets = []

    def on_order_event(self, order_event):

        # order = self.Transactions.GetOrderById(order_event.OrderId)
        # self.Log(f"Order details: Id={order.OrderId}, Symbol={order.Symbol}, Type={order.Type}, Quantity={order.Quantity}, Time={order.Time}")
        
        entryTicket = self.Transactions.GetOrderTicket(order_event.OrderId)

        symbol = order_event.Symbol
        cef_discount_value = self.cef_discount_data[symbol]
        aro_value = round(self.aro_data[self.period][symbol],2)
        current_weighted_discount = self.Portfolio[symbol].Quantity * self.cef_purchase_discount[symbol]
        self.averageFillPrices[symbol] = round(entryTicket.AverageFillPrice,2)

        self.firstOrderTicket[symbol] = entryTicket
        self.buyDipsMarketTicket[symbol] = entryTicket

        if order_event.Status == OrderStatus.INVALID:
            self.invalidOrderCount += 1
            if self.invalidOrderCount == 2:
                return    
                # self.Debug(str(order_event))
            self.invalidOrderReSubmit[symbol] = entryTicket
                
   
        if order_event.Status == OrderStatus.FILLED:
            ### Change the flag to true if its the first order filled!
            if "Initiate BUY" in entryTicket.Tag:
                self.firstOrderTicketFillFlag[symbol] = True
            ### Change the flag to false if a symbol order is filled and no longer held!
            if "Liquidating" in entryTicket.Tag:
                self.firstOrderTicketFillFlag[symbol] = False
            ### Logic for updated CEF Purchase Discount once a new top-up order has been filled!
            if "Topping up" in entryTicket.Tag:
                new_weighted_discount = ((self.Portfolio[symbol].Quantity * self.take_profit_percent / 2) * cef_discount_value)
                new_quantity = self.Portfolio[symbol].Quantity + (self.Portfolio[symbol].Quantity * self.take_profit_percent / 2)
                new_average_purchase_discount = (current_weighted_discount + new_weighted_discount) / new_quantity
                self.cef_purchase_discount[symbol] = round(new_average_purchase_discount, 2)

        # save fill time of initial buy order and wait 30 days before 1st sale
        if self.firstOrderTicket[symbol] is not None and self.firstOrderTicket[symbol].order_id == order_event.order_id:
            self.firstOrderEntryTicketFillTime = self.time

        if order_event.status != OrderStatus.FILLED:
            return

    def OnEndOfDay(self):
        self.invalidOrderCount = 0


# Example custom universe data; it is virtually identical to other custom data types.
class MyCustomUniverseDataClass(PythonData):		
    def get_source(self, config, date, is_live_mode):
        source = "https://raw.githubusercontent.com/jabertech/backtest-qc/main/combined_selection_3.csv"
        return SubscriptionDataSource(source, SubscriptionTransportMedium.RemoteFile);
		
    def Reader(self, config, line, date, isLive):
        if not (line.strip() and line[0].isalnum()):
            return None
			
        items = line.split(",")
    
        # Generate required data, then return an instance of your class.
        data = MyCustomUniverseDataClass()
		
        try:
            data.end_time = datetime.strptime(items[1], '%Y-%m-%d')+timedelta(hours=20)

            # define Time as exactly 1 day earlier Time
            data.time = data.end_time - timedelta(1)

            data.symbol = Symbol.create(items[0], SecurityType.EQUITY, Market.USA)
            data.value = float(items[5])
            data["Open"] = float(items[2])
            data["High"] = float(items[3])
            data["Low"] = float(items[4])
            data["Close"] = float(items[5])
            data["Volume"] = int(items[6])
            data["CEF_NAV"] = float(items[7])
            data["CEF_Discount"] = float(items[8])
            data["CEF_Price"] = float(items[9])
            data["ARO_DPTL_1YR"] = float(items[10])
            # data["ARO_DPTL_3YR"] = float(items[12])
            # data["ARO_DPTL_5YR"] = float(items[14])
        except ValueError:
            return None
        
        return data