Momentum Combined with Volatility Effect in Stocks - Python

#region Namespaces
# ---------- DON'T REMOVE OR EDIT THESE LINES -------------------
# These lines are required for integrating Python with our .NET platform.
import clr
clr.AddReference("Tickblaze.Model")
import ScriptCode
from MultiSymbolTradingStrategyAPI import *
from AssemblyMultiSymbolTradingStrategy_6012_ImportedScripts import *
# ---------------------------------------------------------------
#endregion

## <summary>
## Multi-Symbol Trading Strategy scripts are used for simultaneously trading a group of symbols from a single strategy instance. 
## Common use-cases include pair trading strategies, basket trading strategies and dynamic index strategies, all of which need to evaluate multiple symbols at the same time in order to make trading decisions.
## </summary>
class MyMultiSymbolTradingStrategy(ScriptCode.MultiSymbolTradingStrategyScriptBase):  # NEVER CHANGE THE CLASS NAME
    #region Variables
    # Variables Content
    #endregion

    #region OnInitialize
    ## <summary>
    ## This function is used for accepting the script parameters and for initializing the script prior to all other function calls.
    ## Once the script is assigned to a Desktop, its parameter values can be specified by the user and can be selected for optimization. 
    ## </summary>
    ## --------------------------------------------------------------------------------------------------
    ##                                 INSTRUCTIONS - PLEASE READ CAREFULLY
    ## --------------------------------------------------------------------------------------------------
    ## YOU MUST SET A PARAM TAG FOR EACH PARAMETER ACCEPTED BY THIS FUNCTION.
    ## ALL PARAM TAGS SHOULD BE SET IN THE 'OnInitialize' REGION, RIGHT ABOVE THE 'OnInitialize' FUNCTION.
    ## THE ORDER OF THE TAGS MUST MATCH THE ORDER OF THE ACTUAL PARAMETERS.

    ## REQUIRED ATTRIBUTES:
    ## (1) name: The exact parameter name.
    ## (2) type: The type of data to collect from the user: 
    ## Set to "Integer" when the data type is an integer.
    ## Set to "IntegerArray" when the data type is an integer list.
    ## Set to "DateTime" when the data type is is an integer representing a date/time.
    ## Set to "DateTimeArray" when the data type is an integer list representing a list of date/time.
    ## Set to "Boolean" when the data type is a boolean.
    ## Set to "BooleanArray" when the data type is a list of booleans.
    ## Set to "Double" when the data type is a number.
    ## Set to "DoubleArray" when the data type is a list of numbers.
    ## Set to "String" when the data type is a string.
    ## Set to "StringArray" when the data type is a list of strings.

    ## OPTIONAL ATTRIBUTES:
    ## (3) default: The default parameter value is only valid when the type is Integer, Boolean, Double, String or an API Type. 
    ## (4) min: The minimum parameter value is only valid when the type is Integer or Double.
    ## (5) max: The maximum parameter value is only valid when the type is Integer or Double.

    ## EXAMPLE: <param name="" type="" default="" min="" max="">Enter the parameter description here.</param> 
    ## --------------------------------------------------------------------------------------------------
	## <param name="returnPeriods" type="Integer" default="6" min="1" max="10000000">The number of periods used to calculate the return.</param>
	## <param name="volatilityPeriods" type="Integer" default="252" min="1" max="10000000">The number of daily periods used to calculate the volatility.</param>
	## <param name="holdSymbols" type="Integer" default="10" min="1" max="10000">The number of symbols to hold for each trade direction (long/short).</param>
	## <param name="holdBars" type="Integer" default="1" min="1" max="10000000">The number of bars to hold the selected symbols.</param>
	## <param name="returnPercentileCutoff" type="Double" default="20" min="1" max="100">The percentile on the return distribution below which stocks are not considered for trading.</param>
	## <param name="enableShorting" type="Boolean" default="True">Indicates whether to enable the trading strategy to short symbols. </param>
	## <param name="enableLonging" type="Boolean" default="True">Indicates whether to enable the trading strategy to long symbols. </param>
	## <param name="stopLoss" type="Double" default="0">The percent distance from the entry price in which to place a stop loss order. (0 to ignore). </param>
	## <param name="takeProfit" type="Double" default="0">The percent distance from the entry price in which to place a take profit order. (0 to ignore). </param>
	## <param name="minimumPrice" type="Double" default="5">The minimum price a symbol can have to be eligible for trading.</param>
    def OnInitialize(self,
            returnPeriods,
            volatilityPeriods,
            holdSymbols,
            holdBars,
            returnPercentileCutoff,
            enableShorting,
            enableLonging,
            stopLoss,
            takeProfit,
            minimumPrice):
		# Set the script parameters to script variables.
        self._returnPeriods = returnPeriods
        self._volatilityPeriods = volatilityPeriods
        self._holdSymbols = holdSymbols
        self._holdBars = holdBars
        self._enableShorting = enableShorting
        self._enableLonging = enableLonging
        self._stopLoss = stopLoss
        self._takeProfit = takeProfit
        self._minimumPrice = minimumPrice
        self._returnPercentileCutoff = returnPercentileCutoff
		# Calculate number of symbols to invest each close.
        self._symbolsToInvestEachClose = holdSymbols // holdBars
		# Calculate the number of symbols to invest if _holdSymbols is not a multiple of _holdBars.
        self._symbolsToInvestWithRemainder = self._symbolsToInvestEachClose + 1
		# Record the bar in the holding period cycle when number of symbols to invest is switched from _symbolsToInvestWithRemainder to _symbolsToInvestEachClose.
        self._barToSwitchNumberOfSymbols = holdSymbols % holdBars
		# Create a list to hold a single indicator for each symbol.
        self._return = []
		# Create a list to hold a single indicator for each symbol.
        self._volatility = []
		# Iterate over all of the symbol indexes.
        for symbolIndex in range(SymbolCount()):
			# Create a copy for the current symbol index.
            self._return.append(IndicatorROC(self, IndicatorCLOSE(self, symbolIndex), returnPeriods))
		# Switch to the daily data series.
        DataSeriesSwitch(1)
		# Iterate over all of the symbol indexes.
        for symbolIndex in range(SymbolCount()):
			# Create a copy for the current symbol index.
            self._volatility.append(IndicatorSDEV(self, IndicatorCLOSE(self, symbolIndex), volatilityPeriods))
		# Switch back to the original data series.
        DataSeriesSwitch(0)
        # Create a variable to hold the total number of bars processed.
        self._barsProcessed = 0
    #endregion

    #region OnBarUpdate
    ## <summary>
    ## This function is called after each new bar of each symbol assigned to the Desktop strategy. 
    ## It should evaluate the specified symbol and its new bar in order to determine whether to generate new orders for it. 
    ## Never create indicators, signals or patterns from OnBarUpdate, for performance reasons those should be created from OnInitialize.
    ## </summary>
    ## <param name="symbolIndex" type="Integer">The index of the symbol in the strategy symbol table</param>
    ## <param name="dataSeries" type="Integer">The number indicating the data series from which the symbol was updated.
    ## According to the Desktop strategy data series settings: 0 for the main data series, 1 for the second data series, etc.</param>
    ## <param name="completedBars" type="Integer">The number of completed bars for the specified symbol since the last call to OnBarUpdate.
    ## Always 1 unless the bar type can generate multiple completed bars from a single tick/minute/day update (depending on the underlying bar source).</param>
    ## <param name="isLastSymbol" type="Boolean">Indicates whether this is the last symbol to be updated for the current bar. 
    ## The parameter is valid when the bars for different symbols have matching timestamps, e.g. 1m, 5m, 1d, 1w, etc.</param>
    def OnBarUpdate(self, symbolIndex, dataSeries, completedBars, isLastSymbol):
		# Check whether all of the symbols have been updated.
        if isLastSymbol:
			# Switch the API functions to work with the current symbol.
            SymbolSwitch(symbolIndex)
			# Check whether the bar is complete.
            if DataIsComplete(0):
		        # Create a list to hold the symbol return and volatility values.
                symbolReturnVolatilityValues = []
				# Create a list to hold the symbols indices of positions that were just closed.
                symbolIndicesJustClosed = []
				# Iterate through each symbol.
                for symIndex in range(SymbolCount()):
					# Switch the API functions to work with the current symbol.
                    SymbolSwitch(symIndex)
					# Check whether a position exists.
                    if PositionExists(C_PositionStatus.OPEN):
						# Record the open position index.
                        openPositionIndex = PositionByStatus(C_PositionStatus.OPEN)[0]
						# Check whether it is time to close the open position.
                        if PositionBars(openPositionIndex) == self._holdBars:
							# Close the open position.
                            BrokerClosePosition("Time to close")
							# Record the symbol index of the position that just closed.
                            symbolIndicesJustClosed.append(symIndex)
                            
					# Switch to the daily data series.
                    DataSeriesSwitch(1)
					# Record the oldest close needed to calculate volatility.
                    volatilityClose = DataClose(self._volatilityPeriods)
					# Switch back to the original data series.
                    DataSeriesSwitch(0)
					# Check whether the symbol is trading above the minimum price, there is enough history to calculate the return and volatility, and the symbol is active.
                    if DataClose(0) > self._minimumPrice and DataClose(self._returnPeriods) != 0 and volatilityClose != 0 and SymbolIsAvailable():
						# Get the return and volatility values for the latest bar of the current symbol.
                        symbolReturnVolatilityValues.append([symIndex, self._return[symIndex][0], self._volatility[symIndex][0]])
                        
				# Sort the symbols by descending return values so that those with the higher values come first.
                symbolReturnVolatilityValues = sorted(symbolReturnVolatilityValues, key = lambda item: item[1], reverse = True)
				# Calculate the number of symbols to sort by return.
                symbolCutoff = int(len(symbolReturnVolatilityValues) * self._returnPercentileCutoff / 100)
				# Create a list of long symbols that satisfy the return percentile cutoff.
                longHighVolatilitySymbols = symbolReturnVolatilityValues[0:symbolCutoff]
				# Sort the symbols by descending volatility values so that those with higher values come first.
                longHighVolatilitySymbols = sorted(longHighVolatilitySymbols, key = lambda item: item[2], reverse = True)
				# Calculate the number of orders to send for the current close.
                ordersToSend = self._symbolsToInvestWithRemainder if self._barsProcessed % self._holdBars < self._barToSwitchNumberOfSymbols else self._symbolsToInvestEachClose
				# Check whether the trading strategy can go long.
                if self._enableLonging:
					# Create a variable to hold the number of buy orders sent.
                    buyOrdersSent = 0
					# Create a variable to hold the current long symbol index.
                    longSymbolIndex = 0
					# Iterate over the number of long symbols with positive returns until enough orders are generated.
                    while longSymbolIndex < len(longHighVolatilitySymbols) and buyOrdersSent < ordersToSend and longHighVolatilitySymbols[longSymbolIndex][1] > 0:
						# Switch the API functions to work with the current symbol.
                        SymbolSwitch(longHighVolatilitySymbols[longSymbolIndex][0])
						# Check to ensure there is not an open position or pending order or an open position was just closed.
                        if not PositionExists(C_PositionStatus.OPEN) and not OrderExists(C_Status.PENDING, None) or longHighVolatilitySymbols[longSymbolIndex][0] in symbolIndicesJustClosed:
							# Buy the current symbol while assuming that a position sizing script will assign the quantity
                            orderIndex = BrokerMarket(C_ActionType.BUY, 0, C_TIF.DAY, "Buy to open.")
							# Increase the number of buy orders sent.
                            buyOrdersSent = buyOrdersSent + 1
							# Set a stop loss on the order. 
                            BrokerSetStopLossPercent(orderIndex, self._stopLoss, True, "Stop loss")
							# Set a take profit on the order. 
                            BrokerSetTakeProfitPercent(orderIndex, self._takeProfit, True, "Profit target")
                        # Increment the long symbol index.
                        longSymbolIndex = longSymbolIndex + 1
                            
				# Sort the symbols by ascending return values so that those with lower values come first.
                symbolReturnVolatilityValues = sorted(symbolReturnVolatilityValues, key = lambda item: item[1])
				# Create a list of short symbols that satisfy the return percentile cutoff.
                shortHighVolatilitySymbols = symbolReturnVolatilityValues[0:symbolCutoff]
				# Sort the symbols by descending volatility values so that those with higher values come first.
                shortHighVolatilitySymbols = sorted(shortHighVolatilitySymbols, key = lambda item: item[2], reverse = True)
				# Check whether the trading strategy can go short.
                if self._enableShorting:
					# Create a variable to hold the number of sell orders sent.
                    sellOrdersSent = 0
					# Create a variable to hold the current short symbol index.
                    shortSymbolIndex = 0
					# Iterate over the number of short symbols with negative returns until enough orders are generated.
                    while shortSymbolIndex < len(shortHighVolatilitySymbols) and sellOrdersSent < ordersToSend and shortHighVolatilitySymbols[shortSymbolIndex][1] < 0:
						# Switch the API functions to work with the current symbol.
                        SymbolSwitch(shortHighVolatilitySymbols[shortSymbolIndex][0])
						# Check to ensure there is not an open position or pending order or an open position was just closed.
                        if not PositionExists(C_PositionStatus.OPEN) and not OrderExists(C_Status.PENDING, None) or shortHighVolatilitySymbols[shortSymbolIndex][0] in symbolIndicesJustClosed:
							# Sell short the current symbol while assuming that a position sizing script will assign the quantity
                            orderIndex = BrokerMarket(C_ActionType.SELL_SHORT, 0, C_TIF.DAY, "Sell short to open.")
							# Increase the number of sell orders sent.
                            sellOrdersSent = sellOrdersSent + 1
							# Set a stop loss on the order. 
                            BrokerSetStopLossPercent(orderIndex, self._stopLoss, True, "Stop loss")
							# Set a take profit on the order. 
                            BrokerSetTakeProfitPercent(orderIndex, self._takeProfit, True, "Profit target")
                        # Increment the short symbol index.
                        shortSymbolIndex = shortSymbolIndex + 1
                            
				# Increase the number of bars processed.
                self._barsProcessed = self._barsProcessed + 1
    #endregion

    #region OnOrderFillUpdate
    ## <summary>
    ## This function is called for each new order fill.
    ## </summary>
    ## <param name="symbolIndex" type="Integer">The symbol index</param>
    ## <param name="orderIndex" type="Integer">The order index</param>
    ## <param name="orderFillIndex" type="Integer">The order fill index</param>
    def OnOrderFillUpdate(self, symbolIndex, orderIndex, orderFillIndex):
        # OnOrderFillUpdate Content
        pass
    #endregion

    #region OnOrderUpdate
    ## <summary>
    ## This function is called when an order is executed or cancelled.
    ## </summary>
    ## <param name="symbolIndex" type="Integer">The underlying symbol index of the order</param>
    ## <param name="orderIndex" type="Integer">The order index</param>
    ## <param name="status" type="C_Status">The updated status of the order</param>
    def OnOrderUpdate(self, symbolIndex, orderIndex, status):
        # OnOrderUpdate Content
        pass
    #endregion

    #region OnPositionUpdate
    ## <summary>
    ## This function is called when a position is opened or closed. 
    ## </summary>
    ## <param name="symbolIndex" type="Integer">The underlying symbol index of the position</param>
    ## <param name="positionIndex" type="Integer">The position index</param>
    ## <param name="status" type="C_PositionStatus">The updated status of the position</param>
    def OnPositionUpdate(self, symbolIndex, positionIndex, status):
        # OnPositionUpdate Content
        pass
    #endregion

    #region OnSessionUpdate
    ## <summary>
    ## This function is called when a session is opened or closed.
    ## </summary>
    ## <param name="symbolIndex" type="Integer">The symbol index whose session is updated</param>
    ## <param name="status" type="C_SessionStatus">The session status</param>
    def OnSessionUpdate(self, symbolIndex, status):
        # OnSessionUpdate Content
        pass
    #endregion

    #region OnNewsUpdate
    ## <summary>
    ## This function is called when a news update is received and only if the NO_NEWS_UPDATES comment is removed.
    ## </summary>
    ## <param name="symbolIndex" type="Integer">The symbol index for the update</param>
    ## <param name="dateTime" type="DateTime">The date/time in which the update was received by the platform</param>
    ## <param name="title" type="String">The update title</param>
    ## <param name="message" type="String">The update message</param>   
    ## <param name="type" type="C_MessageType">The update message type</param>
    def OnNewsUpdate(self, symbolIndex, dateTime, title, message, type):
        # OnNewsUpdate Content
        # [NO_NEWS_UPDATES] - Delete this comment to enable news updates to this strategy.
        pass
    #endregion

    #region OnRSSUpdate
    ## <summary>
    ## This function is called when an RSS update is received and only if the NO_RSS_UPDATES comment is removed.
    ## </summary>
    ## <param name="symbolIndex" type="Integer">The symbol index for the update</param>
    ## <param name="dateTime" type="DateTime">The date/time in which the update was received by the platform</param>
    ## <param name="title" type="String">The update title</param>
    ## <param name="message" type="String">The update message</param>   
    ## <param name="type" type="C_MessageType">The update message type</param>
    def OnRSSUpdate(self, symbolIndex, dateTime, title, message, type):
        # OnRSSUpdate Content
        # [NO_RSS_UPDATES] - Delete this comment to enable RSS updates to this strategy.
        pass
    #endregion

    #region OnAlertUpdate
    ## <summary>
    ## This function is called when an alert update is received and only if the NO_ALERT_UPDATES comment is removed.
    ## </summary>
    ## <param name="symbolIndex" type="Integer">The symbol index for the update</param>
    ## <param name="dateTime" type="DateTime">The date/time in which the update was received by the platform</param>
    ## <param name="message" type="String">The update message</param>   
    ## <param name="type" type="C_MessageType">The update message type</param>
    def OnAlertUpdate(self, symbolIndex, dateTime, message, type):
        # OnAlertUpdate Content
        # [NO_ALERT_UPDATES] - Delete this comment to enable alert updates to this strategy.
        pass
    #endregion

    #region OnJournalUpdate
    ## <summary>
    ## This function is called when a journal update is received and only if the NO_JOURNAL_UPDATES comment is removed.
    ## </summary>
    ## <param name="symbolIndex" type="Integer">The symbol index for the update</param>
    ## <param name="dateTime" type="DateTime">The date/time in which the update was received by the platform</param>
    ## <param name="title" type="String">The update title</param>
    ## <param name="message" type="String">The update message</param>   
    ## <param name="type" type="C_MessageType">The message type</param>
    def OnJournalUpdate(self, symbolIndex, dateTime, title, message, type):
        # OnJournalUpdate Content
        # [NO_JOURNAL_UPDATES] - Delete this comment to enable journal updates to this strategy.
        pass
    #endregion

    #region OnDataConnectionUpdate
    ## <summary>
    ## This function is called when a data connection update is received and only if the NO_DATA_CONNECTION_UPDATES comment is removed.
    ## </summary>
    ## <param name="symbolIndex" type="Integer">The symbol index for the update</param>
    ## <param name="dateTime" type="DateTime">The date/time in which the update was received by the platform</param>
    ## <param name="message" type="String">The update message</param>   
    ## <param name="type" type="C_MessageType">The update message type</param>
    def OnDataConnectionUpdate(self, symbolIndex, dateTime, message, type):
        # OnDataConnectionUpdate Content
        # [NO_DATA_CONNECTION_UPDATES] - Delete this comment to enable data connection updates to this strategy.
        pass
    #endregion

    #region OnBrokerConnectionUpdate
    ## <summary>
    ## This function is called when a broker connection update is received and only if the NO_BROKER_CONNECTION_UPDATES comment is removed.
    ## </summary>
    ## <param name="dateTime" type="DateTime">The date/time in which the update was received by the platform</param>
    ## <param name="message" type="String">The update message</param>   
    ## <param name="type" type="C_MessageType">The update message type</param>
    def OnBrokerConnectionUpdate(self, dateTime, message, type):
        # OnBrokerConnectionUpdate Content
        # [NO_BROKER_CONNECTION_UPDATES] - Delete this comment to enable broker connection updates to this strategy.
        pass
    #endregion

    #region OnShutdown
    ## <summary>
    ## This function is called when the script is shutdown.
    ## </summary>
    def OnShutdown(self):
        # OnShutdown Content
        pass
    #endregion