You might have noticed that detecting a take profit can be a tricky endeavor on Tradingview. The difficulty arises from the lack of support for order notifications (at the time of writing!). Without order notifications, it is hard to perform advanced position management techniques like moving a stop to break even. In this article, we will take a look at a workaround that can be done to detect when a take a profit is hit. We will also look at some limitations of the workaround so you know exactly what to expect before copying the code.
The code in the example below will attempt to implement a “move to break even” strategy. In other words, it will set two take profit targets and then move our stop loss to break even as soon as the first take profit is hit.
strategy("Take Profit Detection", overlay=true, default_qty_type=strategy.fixed, default_qty_value=2)
tp_perc = input(1, step=0.25, title='Take Profit 1 %')/100
tp2_perc = input(2, step=0.25, title='Take Profit 2 %')/100
sl_perc = input(1, step=0.25, title='Stop Loss %')/100
sma_len = input(200, minval=1, title='SMA Length')
sma1 = sma(close, sma_len)
long_condition = crossover(close, sma1)
short_condition = crossunder(close, sma1)
strategy.entry('Long', strategy.long, when=long_condition)
strategy.entry('Short', strategy.short, when=short_condition)
long_tp_hit = strategy.position_size > 0 and strategy.position_size > 0 and strategy.position_size > strategy.position_size
short_tp_hit = strategy.position_size < 0 and strategy.position_size < 0 and strategy.position_size < strategy.position_size
// Setup our stop losses and take profits
float long_sl_level = na
float short_sl_level = na
// float long_tp_level = na
// float short_tp_level = na
// float long_tp2_level = na
// float short_tp2_level = na
long_sl_level := long_condition ? close * (1 - sl_perc) : long_tp_hit ? strategy.position_avg_price : long_sl_level
short_sl_level := short_condition ? close * (1 + sl_perc) : short_tp_hit ? strategy.position_avg_price : short_sl_level
long_tp_level = strategy.position_avg_price * (1 + tp_perc)
long_tp2_level = strategy.position_avg_price * (1 + tp2_perc)
short_tp_level = strategy.position_avg_price * (1 - tp_perc)
short_tp2_level = strategy.position_avg_price * (1 - tp2_perc)
strategy.exit('L-SLTP1', 'Long', stop=long_sl_level, limit=long_tp_level, qty_percent=50)
strategy.exit('L-SLTP2', 'Long', stop=long_sl_level, limit=long_tp2_level, qty_percent=100)
strategy.exit('S-SLTP1', 'Short', stop=short_sl_level, limit=short_tp_level, qty_percent=50)
strategy.exit('S-SLTP2', 'Short', stop=short_sl_level, limit=short_tp2_level, qty_percent=100)
// Take Profits and Stops
plot(strategy.position_size > 0 ? long_sl_level : na, color=color.red, style=plot.style_circles, title="Long Stop")
plot(strategy.position_size < 0 ? short_sl_level : na, color=color.maroon, style=plot.style_circles, title="Short Stop")
plot(strategy.position_size > 0 ? long_tp_level : na, color=color.lime, style=plot.style_circles, title="Long TP")
plot(strategy.position_size > 0 ? long_tp2_level : na, color=color.lime, style=plot.style_circles, title="Long TP2")
plot(strategy.position_size < 0 ? short_tp_level : na, color=color.green, style=plot.style_circles, title="Short TP")
plot(strategy.position_size < 0 ? short_tp2_level : na, color=color.green, style=plot.style_circles, title="Short TP")
alertcondition(long_condition, title='Go Long')
Before we discuss the workaround, there are a couple of other points worth mentioning.
The first thing you might notice is that the default position size for any trade is set to 2. If you don’t adjust this, your default size would be 1 and we would exit the position as soon as the first take profit is hit. Setting the default to 2 avoids this. Of course, you can still edit your order size as usual in the properties tab if you want to go bigger.
Next, we need some sort of signal/entry to get us into a position and demonstrate this technique. As such, a simple strategy was used so we do not distract from the main objective of the post. We simply enter long or short anytime price crosses above or below a 200 period moving average.
Once we have entered a position, we can then attempt to implement the workaround. As you will see, it is actually quite simple! All we need to do is monitor our position size. For example, if we are long and our position size decreases but we are still long after our position size decreased, then we can assume one of the take profits was hit. Similarly, if we are short and our position size increases whilst remaining short, then we again can assume our short take profit was hit.
Once our first take profit is hit, we simply move our stop loss to the same level as the
strategy.position_avg_price variable. We do this by using a ternary conditional operator.
Taking off 50%
So we now know how to detect a take profit. However, that is only part of the battle. We also need to ensure we set up our exit orders correctly. If we don’t do this, we can end up exiting the full position when the first take profit is hit.
Taking off 50% of our position size can be achieved using the
qty_percentargument inside the
strategy.exit()function. However, we do need to be a little careful about how we do this. This is because your script will be run once on every bar. Therefore, it means that you can also update the order on every bar. The problem with that is that if you are updating orders to take off 50%, then before your take profit is hit, it will be 50% of the full position. After your take profit is hit, it will actually be 50% of the remaining size and not the original size. This results in insufficient size for the second take profit like so.
In the image above, you can see that only 0.5 is taken off the position instead of 1. We have unintentionally remained in the position.
Fear not though, there are a couple of solutions to this. The first is that you can hide your
strategy.exit()calls behind an
ifstatement. E.g. behind
long_condition. This way, the order is only sent once at entry and is not updated every bar. If you do this, make sure that you do update the order if
truewhilst you are already in a long position! You can do this by checking
strategy.position_size <=0before sending the orders.
The alternative is to set the second portion of the order to 100% and let it update the order every bar. This does not sound intuitive but at the time of writing, pine-script will handle this gracefully for you. If both are hit on the same bar, you will not take off 150%. This second solution is good for situations where you have a complex stop which could be updated frequently.
The main limitation to this workaround is that if one of your take profit levels is near to your entry level, you can enter and exit on the same bar. This can also happen in volatile markets like crypto where a huge price spike occurs. When you enter and exit on the same bar, we are unable to detect that the position reduced.
If you are struggling to picture why that would be the case, it is important to remember that your script runs once per bar. When it does this, we get a snapshot of data at that moment in time to perform calculations on and make a trading decision. So imagine that on the previous bar to our entry, our position size is nothing. Then on the current bar our we enter but we also exit part of the position on the same bar. Now let’s pretend that this is a long position. What has actually happened is our position size has gone from zero to greater than zero. In other words, it increased. We did not decrease our size and remain above zero! As such, the TP cannot be detected.
So as we can see, the solution above is not perfect but it does at least provide an option to perform some more advanced position management techniques. If you set your take profit levels to a reasonable level given the timeframe/volatility you are working with, then the impact should be minimal.
Support the site
If you like this article and are thinking to become a Tradingview subscriber, you can support the site by clicking on the affiliate link below before signing up!