In my charts and exploration I am getting correct signals. In backtesting the buy/short signals seem to be correct, but the sell/cover signals seem to be incorrect. Following is an example.
My system generates the following trade.
In exploration, there is a buy and sell signal on the correct dates.
There are no intervening signals.
When I run the backtest, the buy signal is taken correctly. However, the sell signal is not taken. In the backtest, this trade is not exited for another two years. The following image is of the trade list from the backtest.
I checked the detailed log. The buy signal is taken correctly.
On the date of the sell signal, there is nothing in the detailed log and it still indicates an open position.
Then, a second position is opened in this same ticker according to the detailed log, which I thought I had prevented in my trailing stop code (code to follow).
However, this is a correct buy signal and shows up in the exploration.
Here is the detailed log when the initial position is finally exited roughly two years after the sell signal.
There is a sell signal on that date, but it's not the correct one for the initial trade.
I am using custom backtester but only to generate custom metrics (mostly to calculate my risk multiple). Having looked at other forum questions, it seems that either something in my backtest or settings is causing this, but I am really struggling to figure out what it is.
Here is my code.
// Variables for entry
highest_high = IIf( BarIndex() > entry_period, Ref( HHV( H, entry_period ), -1 ), Null ); // Highest high over the lookback period
lowest_low = IIf( BarIndex() > entry_period, Ref( LLV( L, entry_period ), -1 ), Null ); // Lowest low over the lookback period
// Variables for trailing stop
trail_ATRs = abs( trail_stop_multiplier * ATR( 20 ) ); // Multiple of ATR(20) to calculate the trailing stop
trail_stop_buy = TickSize * round( ( Close - trail_ATRs ) / TickSize ); // Trailing stop for a buy rounded to nearest tick
trail_stop_short = TickSize * round( ( Close + trail_ATRs ) / TickSize ); // Trailing stop for a short rounded to nearest tick
//Variables for initial stop
initial_ATRs = abs( 1.5 * ATR( 20 ) ); // Multiple of ATR(20) to calculate the initial stop
initial_stop_buy = TickSize * round( ( Close - initial_ATRs ) / TickSize ); // Initial stop for a buy rounded to the nearest tick
risk_points_buy = Ref( C, -1 ) - Ref( initial_stop_buy, -1 ); // Known risk points for a buy trade from the bar prior to entry
initial_stop_short = TickSize * round( ( Close + initial_ATRs ) / TickSize ); // Initial stop for a short rounded to the nearest tick
risk_points_short = Ref( initial_stop_short, -1) - Ref( C, -1 ); // Risk points for a short trade from the bar prior to entry
// Calculate the volatility state of the ticker
true_range = Max( H - L, Max( abs( H - Ref( C,-1 ) ), abs( L - Ref( C,-1 ) ) ) ); // Calculate the true range
vol_ratio = true_range / EMA( true_range, 252 ); // Volatility ratio is the true range divided by the EMA of the true range
vol_ratio_75 = LastValue( Percentile( vol_ratio, BarIndex(), 75 ) ); // The 75th percentile of the volatility ratio
vol_ratio_25 = LastValue( Percentile( vol_ratio, BarIndex(), 25 ) ); // The 25th percentile of the volatility ratio
gt_vol_ratio_75 = Sum( vol_ratio > vol_ratio_75, 63); // Days in the last quarter the volatility ratio was greater than the 75th percentile
lt_vol_ratio_25 = Sum( vol_ratio < vol_ratio_25, 63); // Days in the last quarter the volatility ratio was less than the 25th percentile
vol_state = IIf( gt_vol_ratio_75 >= 20, 1, 0) + IIf( lt_vol_ratio_25 >= 20, -1, 0 ); // 1 = volatile, 0 = normal, -1 = quiet
vol_state_entry = Ref( vol_state, -1 ); // Volatility state one bar before entry
// Calculate the trend state of the ticker
// RAVI = abs( 100 * ( ( MA( C, 7 ) - MA( C, 65 ) ) / MA( C, 65 ) ) ); // Calculate the range action verification index
// RAVI_state = IIf( RAVI >= 3, 2, IIf( ( RAVI >= 1.5 AND RAVI < 3 ), 1, 0 ) ); // 2 = trend, 1 = weak trend, 0 = no trend
// Custom backtest procedure
SetOption( "UseCustomBacktestProc", True );
SetCustomBacktestProc( "" );
// Custom backtest metrics for direction and volatility states, risk, risk multiple, and average expectancy
StaticVarSet( "CBT_risk_points_buy" + Name(), risk_points_buy );
StaticVarSet( "CBT_risk_points_short" + Name(), risk_points_short );
StaticVarSet( "CBT_vol_state_entry" + Name(), vol_state_entry );
// StaticVarSet( "CBT_RAVI_state" + Name(), RAVI_state );
if( Status( "action" ) == actionPortfolio )
{
bo = GetBacktesterObject();
bo.Backtest( 1 );
sum_profit_per_risk = 0;
total_trades = 0;
for( trade=bo.GetFirstTrade(); trade; trade=bo.GetNextTrade() )
{
if ( trade.IsLong )
risk = Lookup( StaticVarGet( "CBT_risk_points_buy" + trade.Symbol ), trade.EntryDateTime ) * trade.PointValue() * trade.EntryFxRate; // Initial dollar risk for each buy trade
else
risk = Lookup( StaticVarGet( "CBT_risk_points_short" + trade.Symbol ), trade.EntryDateTime ) * trade.PointValue() * trade.EntryFxRate; // Initial dollar risk for each short trade
r_multiple = trade.GetProfit() / risk; // R multiple for each trade
vol_state_backtest = Lookup( StaticVarGet( "CBT_vol_state_entry" + trade.Symbol ), trade.EntryDateTime ); // Volatility state for each trade
// RAVI_state_backtest = Lookup( StaticVarGet( "CBT_RAVI_state" + trade.Symbol ), trade.EntryDateTime ); // RAVI state for each trade
trade.AddCustomMetric( "Initial Risk", risk );
trade.AddCustomMetric( "R-Multiple", r_multiple );
trade.AddCustomMetric( "Volatility State", vol_state_backtest );
// trade.AddCustomMetric( "RAVI State", RAVI_state_backtest );
sum_profit_per_risk = sum_profit_per_risk + r_multiple; // Sum of R multiples
total_trades++;
}
expectancy = sum_profit_per_risk / total_trades; // Expectancy of all trades
bo.AddCustomMetric( "Avg. Expectancy", expectancy );
bo.ListTrades();
}
SetChartOptions(0,chartShowArrows|chartShowDates);
_N(Title = StrFormat("{{NAME}} - {{INTERVAL}} {{DATE}} Open %g, Hi %g, Lo %g, Close %g (%0.1%%) {{VALUES}}",O,H,L,C,SelectedValue(ROC(C,1))));
// Buy-Short Rules
Buy = Cross( C, highest_high ) AND vol_state == -1; // Buy when the high crosses the highest high of the lookback window and market filter applies
Short = Cross( lowest_low, C ) AND vol_state == -1; // Short when the low crosses the lowest low of the lookback window and market filter applies
// Sell-Cover Rules
Sell = 0;
trail_array_buy = Null;
trailing_stop_buy = 0;
risk_stop_buy = 0;
for( i = 1; i < BarCount; i++ ) // For loop to calculate the trailing stop for a buy
{
if( trailing_stop_buy == 0 AND Buy[i] ) // Condition when the buy trailing stop is 0 and a buy signal
{
trailing_stop_buy = trail_stop_buy[i]; // Set the buy trailing stop equal to the buy stop
risk_stop_buy = initial_stop_buy[i]; // Set the initial buy risk stop equal to the initial risk stop
}
else Buy[i] = 0; // Remove excess buy signals
if(trailing_stop_buy != 0 AND risk_stop_buy != 0 AND ( C[i] < trailing_stop_buy OR L[i] < lowest_low[i] OR C[i] < risk_stop_buy) ) // Condition when the buy trailing and risk stops don't equal 0 and a sell signal
{
Sell[i] = 1; // Generate a sell signal
trailing_stop_buy = 0; // Return the trailing stop to 0
risk_stop_buy = 0; // Return the initial risk stop to 0
}
if( trailing_stop_buy != 0 AND risk_stop_buy != 0 ) // Condition when the buy trailing and risk stops don't equal 0 and no sell signal
{
trailing_stop_buy = Max( trail_stop_buy[i], trailing_stop_buy ); // The trailing stop is the max of the initial value or the value at the current bar
trail_array_buy[i] = Max( trailing_stop_buy, risk_stop_buy ); // The buy trail array is the max of the trailing and initial risk stops
}
}
Cover = 0;
trail_array_short = Null;
trailing_stop_short = 0;
risk_stop_short = 0;
for( i=1; i < BarCount; i++ ) // For loop to calculate the short trailing stop
{
if( trailing_stop_short == 0 AND Short[i] ) // Condition when the short trailing stop is 0 and a short signal
{
trailing_stop_short = trail_stop_short[i]; // Set the short trailing stop equal to the short stop
risk_stop_short = initial_stop_short[i]; // Set the initial short stop equal to the initial short risk stop
}
else Short[i] = 0; // Remove excess short signals
if( trailing_stop_short != 0 AND risk_stop_short != 0 AND
( C[i] > trailing_stop_short OR H[i] > highest_high[i] OR C[i] > risk_stop_short ) ) // Condition when the short trailing and risk stops don't equal 0 and a sell signal
{
Cover[i] = 1; // Generate a cover signal
trailing_stop_short = 0; // Return the short trailing stop to 0
risk_stop_short = 0; // Return the initial short risk stop to 0
}
if( trailing_stop_short != 0 AND risk_stop_short != 0 ) // Condition when the short trailing and risk stops don't equal 0 and no cover signal
{
trailing_stop_short = Min( trail_stop_short[i] ,trailing_stop_short ); // The trailing stop is the min of the initial value and the value at the current bar
trail_array_short[i] = Min( trailing_stop_short, risk_stop_short ); // The short trail array is the min of the trailing and initial risk stops
}
}