Overall Statistics |
Total Trades 473 Average Win 0.02% Average Loss -0.02% Compounding Annual Return -0.565% Drawdown 1.200% Expectancy -0.141 Net Profit -0.845% Sharpe Ratio -0.707 Probabilistic Sharpe Ratio 0.482% Loss Rate 56% Win Rate 44% Profit-Loss Ratio 0.95 Alpha -0.003 Beta 0.021 Annual Standard Deviation 0.006 Annual Variance 0 Information Ratio 0.557 Tracking Error 0.073 Treynor Ratio -0.185 Total Fees $0.00 Estimated Strategy Capacity $12000000000.00 Lowest Capacity Asset USDZAR 8G Portfolio Turnover 1.95% |
#region imports from AlgorithmImports import * from collections import deque #endregion class RecordIndicator(PythonIndicator): ''' This custom indicator was created to manage rolling indicators mainly. It takes an indicator, saves the amount of data required to perform a correct computation (passed the warm-up period), and every time it is updated it will use that saved data. The IntradayUpdate method does not store values, the normal Update does. ''' def __init__(self, name, indicator, update_method='bar'): ''' Inputs: - Name [String]: Name of the indicator. - indicator [Indicator Object]: Underlying indicator - update_method [str]: 'bar' reference to a Bar object, other reference to (datetime, decimal) object. The two conventions on QC. ''' self.Name = name self.Time = datetime.min # Last time update. self.Value = 0 # For this case It does not vary. self.indicator = indicator self.LastValues = deque(maxlen=self.indicator.WarmUpPeriod) # Stores the value self.update_method = update_method def SelectUpdate(self,input): # Perform the specified update method if self.update_method == 'bar': self.indicator.Update(input) else: self.indicator.Update(input.EndTime, input.Close) def get_current(self): # Reset the indicator to use the daily values self.indicator.Reset() for d in self.LastValues: self.SelectUpdate(d) return self.indicator def IntradayUpdate(self,input): # Update the indicator with intraday data. It do not store data self.get_current() self.SelectUpdate(input) return self.indicator def Update(self, input): # Reset the indicator, store and update the indicator with the daily data. self.get_current() self.LastValues.append(input) self.SelectUpdate(input) return len(self.LastValues) == self.LastValues.maxlen
#region imports from AlgorithmImports import * #endregion # Your New Python File # QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. # Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from AlgorithmImports import * class CustomRiskManagementModel(RiskManagementModel): '''Provides an implementation of IRiskManagementModel that limits the maximum possible loss measured from the highest unrealized profit''' def __init__(self, main, maximumProfit = 3, remainingProfitPercent=0, maximumDrawdown = 2, remainingStopLossPercent=0): '''Initializes a new instance of the TrailingStopRiskManagementModel class Args: maximumDrawdown: The maximum percentage drawdown allowed for the algorithm portfolio compared with the highest unrealized profit, defaults to a 5% drawdown maximumProfit: Profit percentages generated over security that will trigger the Take Profit. remainingProfitPercent: The percentage of the actual holding values to maintain. remainingStopLossPercent: The percentage of the actual holding values to maintain. ''' self.main = main self.maximumProfit = abs(maximumProfit) self.remainingProfitPercent = abs(remainingProfitPercent) self.remainingStopLossPercent = abs(remainingStopLossPercent) self.maximumDrawdown = abs(maximumDrawdown) self.trailingPriceState = dict() def ManageRisk(self, algorithm, targets): '''Manages the algorithm's risk at each time step Args: algorithm: The algorithm instance targets: The current portfolio targets are to be assessed for risk''' riskAdjustedTargets = list() for kvp in algorithm.Securities: symbol = kvp.Key security = kvp.Value # Remove if not invested if not security.Invested: # For positions closed outside the risk management model self.trailingPriceState.pop(symbol, None) # remove from dictionary continue tracker = self.main.SecuritiesTracker[symbol] # Current ATR Indicator currentAtr = tracker.Atr.Current.Value quantity = algorithm.Portfolio[symbol].Quantity # Get position side position = PositionSide.Long if security.Holdings.IsLong else PositionSide.Short # Recorded Holdings Value trailingPriceState = self.trailingPriceState.get(symbol) # Add newly invested security (if doesn't exist) or reset holdings state (if position changed) if trailingPriceState is None or position != trailingPriceState.position: order = tracker.Order # Filled Average Price price = order.AverageFillPrice # Create a HoldingsState object if not existing or reset it if the position direction changed self.trailingPriceState[symbol] = trailingPriceState = PriceState(position, price) CurrentPrice = security.Price initialPrice = self.trailingPriceState[symbol].initialPrice # If the profits reach the trigger if CurrentPrice > ((self.maximumProfit * currentAtr) + initialPrice): # Update position riskAdjustedTargets.append(PortfolioTarget(symbol, int(quantity*self.remainingProfitPercent))) # Pop the symbol from the dictionary since the holdings state of the security has been changed self.trailingPriceState.pop(symbol, None) # remove from dictionary continue elif CurrentPrice < (initialPrice - (self.maximumDrawdown * currentAtr)): # liquidate riskAdjustedTargets.append(PortfolioTarget(symbol, int(quantity*self.remainingStopLossPercent))) # Pop the symbol from the dictionary since the holdings state of the security has been changed self.trailingPriceState.pop(symbol, None) # remove from dictionary return riskAdjustedTargets class PriceState: def __init__(self, position, initialPrice): self.position = position self.initialPrice = initialPrice
#region imports from AlgorithmImports import * #endregion # Your New Python File #region imports from AlgorithmImports import * from collections import deque #endregion class SymbolData: # Object to Keep track of the securities ## INITIALIZATION def __init__(self,symbol, security, time, order_creator, atr, fast_ma, slow_ma): ''' Inputs: - Symbol [QC Symbol]: Reference to the underlying security. - Security [QC Security]: Reference to the security object to access data. - time [Main Algo function]: This function returns the time of the main algorithm. - Indicator objects: Atr, Fast_MA, Slow_MA ''' self.Symbol = symbol self.Security = security self.get_Time = time self.OrderCreator = order_creator self.CustomAtr = atr self.Atr = atr.indicator self.fast_ma = fast_ma self.slow_ma = slow_ma self.CreateConsolidator() self.FastIsOverSlow = False self.SetOrder(None) @property def SlowIsOverFast(self): return not self.FastIsOverSlow @property def Time(self): # Allow access to the Time object directly return self.get_Time() @property def IsReady(self): # Tells if all the indicators assciated are ready return self.Atr.IsReady and self.fast_ma.IsReady and self.slow_ma def CreateConsolidator(self): self.MyConsolidator = QuoteBarConsolidator(self.DefineConsolidator) self.MyConsolidator.DataConsolidated += self.ConsolidatorHandler ## INDICATORS def CheckOpenMarket(self, dt): '''Check market times''' last_open = self.Security.Exchange.Hours.GetPreviousMarketOpen(dt, False) next_close = self.Security.Exchange.Hours.GetNextMarketClose(dt, False) return (last_open < dt and next_close > dt) def DefineConsolidator(self, dt): next_close = self.Security.Exchange.Hours.GetNextMarketClose(dt, False) - timedelta(minutes=10) if self.Security.Exchange.Hours.IsDateOpen(dt): last_open = self.Security.Exchange.Hours.GetPreviousMarketOpen(dt, False) return CalendarInfo(last_open, next_close - last_open) else: next_open = self.Security.Exchange.Hours.GetNextMarketOpen(dt,False) return CalendarInfo(next_open, next_close - next_open) def ConsolidatorHandler(self, sender: object, consolidated_bar: TradeBar) -> None: self.slow_ma.Update(consolidated_bar.EndTime, consolidated_bar.Close) self.fast_ma.Update(consolidated_bar.EndTime, consolidated_bar.Close) self.CustomAtr.Update(consolidated_bar) target, direction = self.CreatePositionEvent() self.OrderCreator(self.Symbol, target, direction) self.FastIsOverSlow = self.fast_ma > self.slow_ma ## OPEN POSITION LOGIC: Logic specified to open a position associated to the tracked Equity. def CreatePositionEvent(self): if self.FastIsOverSlow: if self.slow_ma > self.fast_ma: if self.Order: return - self.Order.QuantityFilled, OrderDirection.Sell else: return 0, OrderDirection.Sell elif self.SlowIsOverFast: if self.fast_ma > self.slow_ma: if self.Order: return - self.Order.QuantityFilled, OrderDirection.Buy else: return 0, OrderDirection.Buy return 0, None ## MANEGE POSITIONS def SetOrder(self, order): '''Add associated order.''' self.Order = order
# region imports from AlgorithmImports import * import SymbolData import CustomIndicators as ci from CustomRiskManagementModel import CustomRiskManagementModel # endregion class CryingYellowGreenBadger(QCAlgorithm): TICKERS = [ "USDAUD", "USDCAD", "USDCNY", "USDEUR", "USDINR", "USDJPY", "USDMXN", "USDTRY", "USDZAR", ] INVESTMENT_PCT = 1/len(TICKERS) ## INITIALIZE def Initialize(self): self.SetStartDate(2021, 9, 10) # Set Start Date self.SetCash(100000) # Set Strategy Cash # Init Universe settings self.MyUniverseInitializer() self.AddRiskManagement(CustomRiskManagementModel(self)) self.SecuritiesTracker = {} # Initilize tracker parameters def MyUniverseInitializer(self): # Set the resolution for universal use # Even though this is a daily trading strategy, we set the resolution to # minute to have a constant flow of data. We feed the data to the algorithm with # a minute resolution if not, the update of the state would be too long. self.UniverseSettings.Resolution = Resolution.Minute # Set the data normalization raw, more like the real thing # self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw # Adding securities # We add the securities as a Universe Selection model so future implementations # can have the adaptability to any security that gets into the Universe. symbols = [Symbol.Create(t, SecurityType.Forex, Market.Oanda) for t in self.TICKERS] self.AddUniverseSelection(ManualUniverseSelectionModel(symbols)) # Add the Benchmark self.bench = self.AddForex('EURUSD').Symbol self.SetBenchmark(self.bench) # Set it ## SECURITIES LOGIC: CREATION, INDICATORS, UPDATE, TACKING def OnSecuritiesChanged(self, changes: SecurityChanges) -> None: # Gets an object with the changes in the universe # For the added securities we create a SymbolData object that allows us to # track the orders associated and the indicators created for it. for security in changes.AddedSecurities: if self.SecuritiesTracker.get(security.Symbol) is None: atr,fast_sma,slow_sma = self.InitIndicators(security.Symbol) # Pass a reference to the symbol, security object, algorithm time and indicators self.SecuritiesTracker[security.Symbol] = SymbolData.SymbolData(security.Symbol,self.Securities[security.Symbol],self.get_Time, self.CreateOrder, atr,fast_sma,slow_sma) self.SubscriptionManager.AddConsolidator(security.Symbol, self.SecuritiesTracker[security.Symbol].MyConsolidator) # The removed securities are liquidated and removed from the security tracker. for security in changes.RemovedSecurities: if self.Portfolio[security.Symbol].Invested: self.SetHoldings(security.Symbol,0) # Remove Consolidator self.SubscriptionManager.RemoveConsolidator(security.Symbol, self.SecuritiesTracker[security.Symbol].MyConsolidator) self.SecuritiesTracker.pop(security.Symbol, None) def InitIndicators(self,symbol): ''' Receive an equity symbol to track and create the indicators required from the strategy logic. The ATR required intraday updates without losing the past daily information, the use of the ci.RecordIndicator (also a created custom indicator) allows this functionality. Input: - symbol [QC Symbol object]: Reference to the security to feed the indicators. Returns: Returns the indicators objects it selfs - Atr, Fast_MA, Slow_MA ''' # This procets repeat itself per security atr = AverageTrueRange('ATR '+symbol.Value, 14) # Create indicator custom_atr = ci.RecordIndicator('Custom ATR '+symbol.Value, atr) # If required: Use ci.RecordIndicator for intraday update # self.RegisterIndicator(symbol, custom_atr, Resolution.Daily) # Associate the indicator to a ticker and a update resolution # (resolution has to be equal or lower than security resolution) # Here you could pass the consolidator as resolution as well fast_sma = SimpleMovingAverage('Fast SMA '+symbol.Value,7) slow_sma = SimpleMovingAverage('Slow SMA '+symbol.Value,20) return custom_atr, fast_sma, slow_sma def IntradayUpdate(self, data, symbol, tracker): ''' The OnData method will call this function every minute (set resolution), and the tracker will call the indicators associated with the symbol information to update them without saving or updating the daily data. Inputs: - data [Slice QC Object]: Slice QC Object with the information of the securities in the universe. - symbol [Symbol QC object]: QC Symbol identifier of the securities. - tracker [SymbolData object]: Tracker created for the specific symbol. Returns: None ''' if data.ContainsKey(symbol) and data[symbol] is not None and tracker.IsReady: tracker.CustomAtr.IntradayUpdate(data[symbol]) ## CHECK FOR BUYING POWER: This are functions that I usually apply to avoid sending orders wiout margin def CheckBuyingPower(self,symbol, order_direction): ''' Check for enough buying power. If the buying power for the target quantity is not enough, It will return the quantity for which the buying power is enough. ''' # Get the buying power depending of the order direction and symbol buy_power = self.Portfolio.GetBuyingPower(symbol, order_direction) # Compute possible quantity q_t = abs(buy_power) / self.Securities[symbol].Price # Select minimum quantity return round(min(abs(quantity),q_t),8)*np.sign(quantity) def CheckOrdeQuatity(self,symbol, quantity): '''Check that the quantity of shares computed meets the minimum requirments''' q = abs(quantity) # There are requirements for the minimum or maximum that can be purchased per security. if q > self.Settings.MinAbsolutePortfolioTargetPercentage and q < self.Settings.MaxAbsolutePortfolioTargetPercentage: symbol_properties = self.Securities[symbol].SymbolProperties if symbol_properties.MinimumOrderSize is None or q > symbol_properties.MinimumOrderSize: return True return False def ComputeOrderQuantity(self, price): # Compute desired quantity of shares based on the remaining margin (buying capacity). return (self.Portfolio.MarginRemaining * self.INVESTMENT_PCT) / price ## POSITION MANAGEMENT def CreateOrder(self, symbol, target, direction): if direction is None: return # No action sign = -1 if direction == OrderDirection.Sell else 1 quantity = target + self.ComputeOrderQuantity(self.Portfolio[symbol].Price) if self.CheckOrdeQuatity(symbol, quantity): self.SecuritiesTracker[symbol].SetOrder(self.MarketOrder(symbol, quantity)) return def OnData(self, data: Slice): for symbol,tracker in self.SecuritiesTracker.items(): # Iterate Over the securities on track self.IntradayUpdate(data, symbol, tracker)