| 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)