Overall Statistics
Total Trades
2695
Average Win
0.98%
Average Loss
-0.87%
Compounding Annual Return
17.041%
Drawdown
49.400%
Expectancy
0.238
Net Profit
1313.533%
Sharpe Ratio
0.62
Probabilistic Sharpe Ratio
1.848%
Loss Rate
42%
Win Rate
58%
Profit-Loss Ratio
1.12
Alpha
0.097
Beta
0.579
Annual Standard Deviation
0.236
Annual Variance
0.056
Information Ratio
0.273
Tracking Error
0.227
Treynor Ratio
0.252
Total Fees
$2984.32
Estimated Strategy Capacity
$23000000.00
Lowest Capacity Asset
OPRX WVKBTS94XO4L
#OnOrderEvent zeige aktuelle Holdings etc in Log


#Auf Basis von Fiverr entwickelt
#Momentum strategy von Nick Radge

#14.10.21
#good settings 100,100,100 ,atr false, 10 stocks

#19.10.21
#Order fix an einem Tag und nicht 30 days, das kommt zum versatz
# Macht nix, dadurch kein "daily bias, geändert auf Traden nur am 26.

#28.10.21
#OnOrderEvent zeige aktuelle Holdings etc in Log

#Was muss noch gemacht werden:

#ATR weight ausschalten geht noch nicht! aber bessere SR mit 

#Stoploss oder Portfolio Trailstop 

#Posi overnight vorher eingehen, nicht erst bei market open

import configs as cfg

class TrendROC(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2005, 1, 1)
     #   self.SetEndDate(2021, 1, 1)
        self.SetCash(10000) 
        self.AddUniverseSelection(QC500UniverseSelectionModel())
        self.UniverseSettings.Resolution = cfg.resolution
        self.symbolDataDict = {}
        self.next_rebalance = self.Time
        self.MyTradingDay = 26
    
        
        self.divisor = {
            Resolution.Daily: 1,
            Resolution.Hour: 24,
            Resolution.Minute: 24 * 60,
            Resolution.Second: 24 * 60 * 60
        }
        
        self.mkt = self.AddEquity(cfg.mkt, cfg.resolution).Symbol
        self.mkt_sma = self.SMA(self.mkt, cfg.mkt_sma_period, cfg.resolution)
        self.SetWarmUp(cfg.warmup_period)
        
    def OnData(self, data):
        
                             # ab hier raus bei day etc.
        if  self.IsWarmingUp: # or self.next_rebalance >= self.Time:
            return
        
      #  self.Log(f'{self.Time.weekday()}')
        #Trade only on Mondays
      #  if  not self.Time.weekday() == 0: # Indexed 0-5 for Mon-Sat. 
      # TradingDayType.Weekend==1
      
        if  (self.Time.day==26 and self.Time.weekday() == 5) or (self.Time.day==25 and self.Time.weekday() == 5): #TradingDayType.Weekend==1: # Wenn 26 Sa oder So ist,
        #    self.Log(f'{self.Time.weekday()}')
            self.MyTradingDay=28
        #    self.Log(f'{self.MyTradingDay}')
            pass
            
        else:
            pass
        
        if  self.Time.day==self.MyTradingDay: # Trade only am 26. des Monats
        #    self.Log(f'{TradingDayType.Weekend}')
            self.MyTradingDay=26
            pass
        else:
            return
        
        self.Log('Beginning rebalance logic')
        
        
        if not self.mkt_sma.IsReady:
            self.Log('Market is bearished, ejecting')
            return
        
        self.next_rebalance = self.Time + timedelta(cfg.rebalance_period / self.divisor[cfg.resolution])
        
        if self.GetPrice(self.mkt) < self.mkt_sma.Current.Value:
            self.Liquidate()
            return
        
        filtered_rdy = [sd for sd in self.symbolDataDict.values() if sd.IsReady]
        topROC = sorted(filtered_rdy, key=lambda sd: sd.get_roc(), reverse=True)
        filtered_sma = [sd for sd in topROC if sd.is_buy(self.GetPrice(sd.symbol))][:cfg.num_stocks]
        weights = {sd.symbol:self.GetPrice(sd.symbol)/sd.get_atr() for sd in filtered_sma}
        total_weight = sum(weights.values())
        adj_weights = {symbol: weight/total_weight for symbol, weight in weights.items()}
        
        invested = [kvp.Key for kvp in self.Portfolio if kvp.Value.Invested]
        to_buy = adj_weights.keys()
        for symbol in invested:
            if symbol not in to_buy:
                self.Liquidate(symbol)
        
        if len(adj_weights) > 0:
            self.Log('Going LONG')
        
        if cfg.enable_atr:
            for symbol, weight in adj_weights.items():
                self.SetHoldings(symbol, weight)
                
                # Get the current price of symbol which should be equal to the price that was bought
            #    stop_price = self.Securities[symbol].Price
                # Better use Avg price 
            #    self.AveragePrice = self.Portfolio[symbol].AveragePrice
                # Sell all of the XIV holdings if the price drops below StopPrice
            #    self.StopMarketOrder(symbol, -self.Portfolio[symbol].Quantity, stop_price*0.8)
            #    self.Log(f'Symbol:{symbol}')
            #    self.Log(f'Stop price:{stop_price}')
            #    self.Log(f'Avg price:{self.AveragePrice}')
            #    self.Log(f'Quantity:{self.Portfolio[symbol].Quantity}')
               

               
        else:
            weight = 1/len(adj_weights)
            for symbol in adj_weights:
                self.SetHoldings(symbol, weight)
   
                
        
    def OnSecuritiesChanged(self, changed):
    
    
    # Zeige Änderungen am Portfolio, hier QC500    
    #    self.changes = changed
    #    self.Log(f"OnSecuritiesChanged({self.UtcTime}):: {self.changes}")
        
        
        for security in changed.AddedSecurities:
            
            if security.Symbol == self.mkt:
                continue
            self.symbolDataDict[security.Symbol] = SymbolData(self, security.Symbol)
        for security in changed.RemovedSecurities:
            self.symbolDataDict.pop(security.Symbol, None)
            
 
    def OnOrderEvent(self, orderEvent):
        self.MyAssets = [ x.Symbol.Value for x in self.Portfolio.Values if x.Invested ]
        order = self.Transactions.GetOrderById(orderEvent.OrderId)
        if orderEvent.Status == OrderStatus.Filled: 
            self.Log("{0}: {1}: {2}".format(self.Time, order.Type, orderEvent))    #Zeige aktuell ausgeführte Order
            self.Log("invested: " + str(self.MyAssets))                            #Zeige aktuelle Holdings
            

    def GetPrice(self, symbol):
        return self.Securities[symbol].Price
        
    
class SymbolData:
    def __init__(self, algo, symbol):
        self.symbol = symbol
        self.roc = algo.ROCP(symbol, cfg.roc_period, cfg.resolution)
        self.sma = algo.SMA(symbol, cfg.sma_period, cfg.resolution)
        self.atr = algo.ATR(symbol, cfg.atr_period, cfg.resolution)
        
        hist = algo.History(symbol, cfg.warmup_period, cfg.resolution).loc[symbol]
        
        if hist.empty or len(hist)==0:
            return
        
        for time, row in hist.iterrows():
            tradeBar = TradeBar(time, symbol, row.open, row.high, row.low, row.close, row.volume, timedelta(1))
            self.atr.Update(tradeBar)
            self.roc.Update(time, row.close)
            self.sma.Update(time, row.close)
    
    @property
    def IsReady(self):
        return self.atr.IsReady and self.roc.IsReady and self.sma.IsReady
    
    def get_roc(self):
        return self.roc.Current.Value
        
    def get_sma(self):
        return self.sma.Current.Value
        
    def get_atr(self):
        return self.atr.Current.Value
        
    def is_buy(self, price):
        return price > self.get_sma()
        
  
# ---configurables that won't be changed often---

# resolution for data/indicators
resolution = Resolution.Daily 

# wait how many periods of above resolution to rebalance
rebalance_period = 1

# market proxy - used to turn on/off bull
mkt = 'SPY'   #SPY


# ---configurables you might want to change---

# SMA period of the market proxy chosen
mkt_sma_period = 100

# ROC period for sorting
roc_period = 100

# SMA period of individual stocks to determine uptrend
sma_period = 100

# ATR for weighting, where lower ATR means higher weight
atr_period = 20 # 20 - From Perry Kaufmann in BST #010 

# if enabled, use inverse adj ATR weight
# if disabled, use equal weighting
enable_atr = 1

# how many stocks you want
# used to choose how many of the top ROC stocks
num_stocks = 12 #12-16 From BST #??

# probably don't touch this
warmup_period = max(mkt_sma_period, roc_period, sma_period, atr_period)