A big emphasis in algorithmic trading is put on finding the right entry and exit signals for trades. Especially, in the field of technical analysis, buy and sell signals are most too often solely based on the evaluation of indicators or combinations thereof. When it comes to entry and exit signals, the latter takes an even more crucial role in adverse situations.

In this blog post, we focus on getting out of trades, i.e. closing positions. We cover interesting topics of our new and improved Order API and put everything together in a simple trading strategy using the Trality Code Editor. All Python code is explained in a step-by-step fashion.

Why worry about exit rules?

Setting a decision rule to exit trades is at least as important as timing your entry points. Most generally speaking we can distinguish between two broad categories for exit signals:

  • signal driven exits: closing a position is purely driven by some sort of trade signal, i.e. evaluation of an indicator, cross overs or other model signals.
  • profit / time-related exits: closing a position is based on some (risk-adjusted) profitability measure and or time related component (close after X days).

Although these two crudely defined categories are by no means mutually exclusive, they are good enough for us to be used as a distinction in this blog post.

The main issue with purely signal-based exits is that traders might not be willing to accept huge swings in their profit and loss account until they receive an exit signal. Hence most algorithmic trading strategies include some sort of stop limit on profits as well as losses. These limits are very often also tied to some risk measure (e.g. volatility). Besides, some investment strategies might also introduce a maximum holding period for a given position.

In light of the importance of stop limits in an algorithmic trading context, we focus on implementing profit / loss and time-related exits using the new Trality Order API.

Tripple-Barrier Method

The Tripple-Barrier-Method introduced by M. Lopez de Prado in Advances in Financial Machine Learning, is a labelling method for financial time-series. It can be used in supervised machine learning algorithms, to predict the sign of the return on the first barrier touched.

Please note, that in subsequent sections we are not using Machine Learning to estimate the labels ourselves, but instead use this method as a decision rule for exiting our trades.

To get a better intuition about this method we visualize it graphically.

Source: mlfinlab

As the name of the Tripple-Barrier-Method suggests we are looking at three boundaries.

  • take-profit-barrier: topmost horizontal dashed line
  • stop-loss-barrier: bottommost horizontal dashed line
  • time-barrier: dashed vertical line to the right (max-holding period)

To see how easily this can be implemented using our Python API, we disentangle this exit rule step by step. First of all, we look at if-touched-market orders, which will serve as the key order type for the barrier method. Secondly, we explain the concept of Order Scope which enables us to chain orders together. Last but not least, we show you how this can be used in a simplified trading-bot.

Sending stop orders

In our new order API we have tried to simplify order creation for you as much as possible. You can send all commonly available order types such as market, limit, if-touched and trailing orders. Besides most order types come with 3 quantity configuration types. For a more detailed description and much more functionality please visit the Trality Documentation. We can now look at the key order type to implement our exit rule.

If-touched-Market-Order

An if-touched market order is a conditional order. It triggers a Market Order if a given stop price is reached or crossed. The direction of crossing is resolved at order creation using the current market price.

To simplify this, imagine the following situation:

Let's assume the current market price for BTC lies at around 8840 USDT and we want to sell 3 BTC at 9000 USDT. We could do so by using an if-touched market order in the following way

order_iftouched_market_amount("BTCUSDT",amount=-3,stop_price=9000)

This is already helpful, but in a profit and loss setting it seems more natural to specify stops in percentage terms. Therefore we have created two easy to use wrapper functions for you

Take Profit

Creates an If-Touched Market sell order for a given amount with a stop_price that lies stop_percent above the current market price (take-profit).

order_take_profit(symbol="BTCUSDT",amount=3,
				  stop_percent=0.05,subtract_fees=False)

Stop Loss

Creates an If-Touched Market sell order for a given amount with a stop_price that lies stop_percent below the current market price (stop-loss).

order_stop_loss(symbol="BTCUSDT",amount=3,
				stop_percent=0.05,subtract_fees=False)
Note, that both of these convenience functions have a parameter subtract_fees. Setting it `True` will automatically deduct exchange fees form the amount.

Now that we know how to send orders that will be executed once a specific price is touched or breached we need a way to link orders together.

Order Scope

With the concept of order scope we offer you the possibility to link orders together and thereby control their execution.

At Trality we currently provide you with two different kinds of OrderScopes

  • Sequential: orders created within this scope are executed strictly executed on after another
  • One Cancels Other: orders created with this scope are cancelled as soon as one of the linked orders is filled.

For more information on available order scopes please see our documentation here.

Setup horizontal barriers

The "One Cancels Other" order scope is exactly what we need for our tripple-barrier exit method.

For the moment we ignore the vertical barrier and see how we can implement a suitable function to handle our profit and loss-taking barriers. The goal is to obtain a simple function that creates this "double-barrier" for a given symbol, the amount and upper as well as lower barrier. Let's see how we can do this in Python

def make_double_barrier(symbol,amount,take_profit,stop_loss,state):

    """make_double_barrier

    This function creates two iftouched market orders with the onecancelsother
    scope. It is used for our tripple-barrier-method

    Args:
        amount (float): units in base currency to sell
        take_profit (float): take-profit percent
        stop_loss (float): stop-loss percent
        state (state object): the state object of the handler function
    
    Returns:
        TralityOrder:  two order objects
    
    """

    with OrderScope.one_cancels_others():
        order_upper = order_take_profit(symbol,amount,
        								take_profit,
                                        subtract_fees=True)
        order_lower = order_stop_loss(symbol,amount,
        							  stop_loss,
                                      subtract_fees=True)
        
    if order_upper.status != OrderStatus.Pending:
        errmsg = "make_double barrier failed with: {}"
        raise ValueError(errmsg.format(order_upper.error))
    
    # saving orders
    state["order_upper"] = order_upper
    state["order_lower"] = order_lower
    state["created_time"] = order_upper.created_time

    return order_upper, order_lower

As we can see this method uses the given input information to create our take-profit and stop-loss barriers stores relevant information in the state object of our bot.

Our order scope insures that if one order is filled the other is canceled immediately. No need to keep track of the individual orders because cancelation happens automatically.

Adding a maximum holding period

As an additional feature we can include a maximum holding period (i.e. vertical barrier). This method just uses the state information to check if our position is held longer than our maximum holding period. If that is the case we close the position and cancels our barrier orders.

def check_max_holding_period(timestamp,state):
    
    """check_max_holding_period

    This function checks the for a first touch in the vertical barrier.
    If the vertical barrier is touched the double barrier orders are canceled.

    Args:
        timestamp (float): milliseconds of current engine time
        state (state object): the state object of the handler function          
    
    Returns:
        bool value

    """

    if check_state_info(state) is None:
        return True
    
    time_delta = timestamp - state["created_time"]
    
    if state["max_period"] is None:
        return True

    # cancel order if vertical barrier reached
    if time_delta >= state["max_period"]:
        print("vertical barrier reached")
        cancel_order(state["order_upper"].id)
        cancel_order(state["order_lower"].id)
        close_position(state["order_lower"].symbol)


    return True

In the above we use a simple function to check our state object for all required information.

def check_state_info(state):
    
    """check_state_info

    This function checks the state object for relevant order information.
    If the information exists it also refreshes the order_upper from the
    double barrier function

    Args:
        state (state object): the state object of the handler function          
    
    Returns:
        None or TralityOrder order_upper 

    Raises:
        
        AssertionError: If invalid order specification.
    
    """
    
    if "order_upper" not in state.keys():
        return None
    elif state["order_upper"] is None:
        return None
    
    order_upper = state["order_upper"]
    
    errmsg = "No max_period in state. Unable to check max holding period"
    
    assert "max_period" in state.keys() , errmsg
    # refreshing order from api
    order_upper.refresh()
    
    if order_upper.status != OrderStatus.Pending:
                
        # resetting state information
        state["order_upper"] = None
        state["order_lower"] = None
        state["created_time"] = None
        return None

    return order

Putting it all together

We are now ready to use what we have developed and package everything in a simple trading strategy on the pair BTCUSDT trading in 15min intervals. First let's define a simple entry rule for our bot

Our Entry rule

We define a price signal and a volume signal. If both are true we go long BTCUSDT

1) Price signal
The last five consecutive close prices tick upwards (we call it upticks)

$$\text{upticks} = \sum_{t = T-5}^{T} sign(close_t - close_{t-1})$$

Hence our price signal will be true if upticks == 5. Yes this is easy, we write a little helper function.

def last_five_up(data):
    prices = data.select("close")
    signs = np.sign(np.diff(prices))[-5:]
    return sum(signs) == 5

2) Volume signal
We define our volume signal such that

$$ema(volume,20) >ema(volume,40)$$

As usual we can use the data object directly

ema_short_volume, ema_long_volume = volume.ema(20).last, volume.ema(40).last

    # return early on missing data
    if ema_short_volume is None:
        return False 
    
    has_high_volume = ema_short_volume[-1] > ema_long_volume[-1]

Exit Rule

According to the previous elaboration we use our tripple-barrier method with take-profit of 5% and stop-loss of 3%. For a first try we set the maximum holding period to None which will just ignore the parameter.

Handler Function

Finally we are ready to code our handler function and put the entire algorithm together. We commit 95% of the capital to our entry signals


def initialize(state):
    state.max_period = None # exclude vertical


@schedule(interval="15min", symbol=["BTCUSDT"], window_size=200)
def handler(state, data):
    
    # moving averages on volume
    volume = data.volume
    ema_short_volume, ema_long_volume = volume.ema(20).last, volume.ema(40).last

    # return early on missing data
    if ema_short_volume is None:
        return False 
    
    has_high_volume = ema_short_volume[-1] > ema_long_volume[-1]

    # at every timestamp check max holding period
    check_max_holding_period(get_timestamp(),state)

    # getting portfolio and position information
    portfolio = query_portfolio()
    buy_value = float(portfolio.excess_liquidity_quoted) * 0.95
    position = query_open_position_by_symbol(data.symbol)
    has_position = position is not None
    
    if not has_position and last_five_up(data) and has_high_volume:
        price = data.close_last
        buy_amount = buy_value / price
        buy_order = order_market_amount(data.symbol,buy_amount)

        # setup barriers
        make_double_barrier(data.symbol,float(buy_order.quantity),
                            0.05,0.03,state)



Sample Backtest

Just for illustration purposes we run the bot for one month in January 2020 to illustrate the concept. This is by no means an elaborate analysis of strategy performance only a simple show case.

Even if this period is not at all representative we can see that some drawdowns could be avoided due to our take profit and stop loss barrier. To see this we look at our positions.

Please be aware that our backtesting system fills if-touched orders precisely at the respective stop price. This simplification cannot be guaranteed in live trading.

Of course this is an over-simplification and if we look at our entry and exit points we can already detect room for improvement.

In this case we get lucky that after most exits the price really drops. However if our take-profit barrier is triggered our entry rule could be triggered right after it again. In the worst case we could enter right before a steep drop in price.

Summary

In this blog post we show alternative exit rules that have more real-world character and are more closely related to our natural risk-aversion towards financial losses versus gains. The focus here was to get you acquainted with our new order api and show you how it can be used.


Disclaimer: None of what is found in this article should be considered investment advice. The above article is merely an opinion piece and does not represent any kind of trading advice or suggestions on how to invest, how to trade or in which assets to invest in or suggestions on how trading bots or trading algorithms can or should be used! Always do your own research before investing and always (!) only invest what you can afford to lose! Backtests are not indicative of future results.