#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_6015_ImportedScripts import *
# ---------------------------------------------------------------
#endregion
# Import the math package to gain access to the power and sign function.
import math
## <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="shortLookback" type="Integer" default="1" min="1" max="10000">The number of periods that define the short term. Must be less than longLookback.</param>
## <param name="longLookback" type="Integer" default="12" min="2" max="10000">The number of periods that define the long term. Must be greater than shortLookback.</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,
shortLookback,
longLookback,
holdSymbols,
holdBars,
returnPercentileCutoff,
enableShorting,
enableLonging,
stopLoss,
takeProfit,
minimumPrice):
# Set the script parameters to script variables.
self._shortLookback = shortLookback
self._longLookback = longLookback
self._holdSymbols = holdSymbols
self._holdBars = holdBars
self._enableShorting = enableShorting
self._enableLonging = enableLonging
self._stopLoss = stopLoss
self._takeProfit = takeProfit
self._minimumPrice = minimumPrice
self._returnPercentileCutoff = returnPercentileCutoff
# Create a list to hold a single indicator for each symbol.
self.ROC = []
# Create for holding whether the strategy is waiting for an open position to close.
self._waitingToClose = []
# Iterate over all of the symbol indexes.
for symbolIndex in range(SymbolCount()):
# Create a copy for the current symbol index.
self.ROC.append(IndicatorROC(self, IndicatorCLOSE(self, symbolIndex), 1))
# Plot the ROC on a new chart panel.
ChartIndicatorPlot(symbolIndex, self.ROC[symbolIndex], "", -1, 2)
# Add a list item for the current symbol index.
self._waitingToClose.append(False)
# Create for holding the number of bars open positions have been held.
self._heldBars = 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 and the short lookback is less than the long lookback.
if isLastSymbol and self._shortLookback < self._longLookback:
# Switch the API functions to work with the current symbol.
SymbolSwitch(symbolIndex)
# Check whether an open position exists and whether the bar is complete.
if PositionCountByStatusAll(C_PositionStatus.OPEN) > 0 and DataIsComplete(0):
# Increase the number of held bars.
self._heldBars = self._heldBars + 1
# Check whether there are no open positions or whether the number of held bars matches the specified number of hold bars.
if PositionCountByStatusAll(C_PositionStatus.OPEN) == 0 or self._heldBars == self._holdBars:
# Iterate through each symbol.
for symIndex in range(SymbolCount()):
# Switch the API functions to work with the current symbol.
SymbolSwitch(symIndex)
# Check whether the strategy is not waiting for a position to be closed and there is currently an open position.
if not self._waitingToClose[symIndex] and PositionExists(C_PositionStatus.OPEN):
# Close the open position.
BrokerClosePosition("Time to close")
# Record that the strategy is waiting for the position to be closed.
self._waitingToClose[symIndex] = True
# Create a list to hold the symbol return and GARR ratio values.
symbolReturnGarrRatioValues = []
# Iterate over all of the symbol indexes.
for symIndex in range(SymbolCount()):
# Switch the API functions to work with the current symbol.
SymbolSwitch(symIndex)
# Check whether there is enough history to calculate the long term return, whether the stock price is above the minimum required price, and whether the symbol is active.
if DataClose(self._longLookback) != 0 and DataClose(0) >= self._minimumPrice and SymbolIsAvailable():
# Create a variable to hold the short term GARR.
garrShort = self.CalculateGARR(symIndex, self._shortLookback)
# Create a variable to hold the long term GARR.
garrLong = self.CalculateGARR(symIndex, self._longLookback)
# Create a variable to hold the long term GARR.
garrRatio = 0
# Check whether the long term GARR is non-zero.
if garrLong != 0:
# Calculate the GARR ratio.
garrRatio = garrShort / garrLong
# Check whether the short term GARR is non-zero.
elif garrShort != 0:
# Calculate the GARR ratio.
garrRatio = math.copysign(float('inf'), garrShort)
# Calculate the long term return.
longReturn = DataClose(0) / DataClose(self._longLookback) - 1
# Get the long term symbol return and GARR ratio for the latest bar of the current symbol.
symbolReturnGarrRatioValues.append([symIndex, longReturn, garrRatio])
# Sort the symbols by descending return values so that those with higher values come first.
symbolReturnGarrRatioValues = sorted(symbolReturnGarrRatioValues, key = lambda item: item[1], reverse = True)
# Calculate the number of symbols that satisfy the return percentile cutoff.
symbolCutoff = int(len(symbolReturnGarrRatioValues) * self._returnPercentileCutoff / 100)
# Create a list of long symbols that satisfy the return percentile cutoff.
winners = symbolReturnGarrRatioValues[0:symbolCutoff]
# Sort the symbols by ascending GARR ratios so that those with lower values come first.
winners = sorted(winners, key = lambda item: item[2])
# Check whether the trading strategy can go long.
if self._enableLonging:
# Create a variable to hold the current long symbol index.
longSymbolIndex = 0
# Iterate over the number of symbols to hold with positive returns.
while longSymbolIndex < len(winners) and longSymbolIndex < self._holdSymbols and winners[longSymbolIndex][1] > 0:
# Switch the API functions to work with the current symbol.
SymbolSwitch(winners[longSymbolIndex][0])
# Check to ensure there is not an open position or pending order.
if not OrderExists(C_Status.PENDING, None):
# 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.")
# 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.
symbolReturnGarrRatioValues = sorted(symbolReturnGarrRatioValues, key = lambda item: item[1])
# Create a list of short symbols that satisfy the return percentile cutoff.
losers = symbolReturnGarrRatioValues[0:symbolCutoff]
# Sort the symbols by descending GARR ratios so that those with higher values come first.
losers = sorted(losers, key = lambda item: item[2], reverse = True)
# Check whether the trading strategy can go short.
if self._enableShorting:
# Create a variable to hold the current short symbol index.
shortSymbolIndex = 0
# Iterate over the number of symbols to hold with negative returns.
while shortSymbolIndex < len(losers) and shortSymbolIndex < self._holdSymbols and losers[shortSymbolIndex][1] < 0:
# Switch the API functions to work with the current symbol.
SymbolSwitch(losers[shortSymbolIndex][0])
# Check to ensure there is not an open position or pending order.
if not OrderExists(C_Status.PENDING, None):
# 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.")
# 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
# Clear the number of held bars.
self._heldBars = 0
#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):
# Switch the API functions to work with the current symbol.
SymbolSwitch(symbolIndex)
# Check whether the position just closed.
if status == C_PositionStatus.CLOSED:
# Record that the strategy is no longer waiting for the position to be closed.
self._waitingToClose[symbolIndex] = False
#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
def CalculateGARR(self, symbolIndex, lookback):
# Create a variable to hold the GARR.
garr = 1
# Iterate through the periods that define the lookback.
for barShift in range(lookback):
# Include the return of the current symbol and bar shift in the calculation of the GARR.
garr = garr * (1 + self.ROC[symbolIndex][barShift] / 100)
# Finish calculation of GARR.
return math.pow(garr, 1.0 / lookback) - 1