Skip random symbols from watchlist

  1. I have a watchlist of say 12 stocks.
  2. I backtest my strategy, but I want to skip two random symbols from the overall backtest run using a random number generator.
  3. Each time I backtest, I want the skipped symbols to be different.

I am sure it can be done; I just cannot figure out a way without CBT, and my programming skills are not good for that. Thanks.

You can use random PositionScore (array) to get random selection on trade by trade basis.

// this would be random symbol pick on trade by trade basis
PositionScore = mtRandomA();  // array

or you can just do the pick once per backtest by using scalar:

// this would be random symbol pick for entire backtest
PositionScore = mtRandom(); // scalar

Now you would want to set MaxOpenPositions to 10 (so it ensures that 2 of 12 symbols are not entered)

SetOption("MaxOpenPositions", 10 ); //

Thank you, Tomasz. I tried something similar. The problem is that the open positions are not always 10, but they change according to entry logic, for example, an MA crossover. Then, at some point, all 12 symbols are used, regardless of MaxOpenPositions setting to 10.

I was thinking along the lines of dynamically creating an exclusion list to remove 2 random symbols. Or setting the bottom 2 rank to a negative number. But I have no idea how to do that.

No, MaxOpenPositions setting is absolute maximum. If you set it to 10, you will NEVER get more than 10 open positions (on 10 symbols).

1 Like

Thank you, Tomasz. I realize what you wrote. This is not an issue in my strategy because it rarely gets more than 7 simultaneous open positions.

The issue is that since the open positions are less than 10, eventually all symbols are used and it does not skip 2 random symbols for the duration of the backtest.

I also tried to add positionscore > 2 to buy and short rules. But the problem with this is that the random numbers are not evenly distributed and sometimes more stocks are ignored.

I discretized the position score according to your suggestion in another post as follows:

PositionScore = floor( mtRandom() * 12)+1;

But due to uniform distribution, several stocks may end up with a low score of 1 or 2, all getting ignored.

After a lot of trials and thinking, I concluded that the best way to ignore say k stocks from a watchlist of n stocks for the whole duration of a single backtest is to generate an exclusion list dynamically. Let us call this list EXLIST:

  1. Erase EXLIST
  2. Populate after removing k stocks randomly

This must be done once before the backtest starts.

Another brute force way is to generate all cobinations of lists without replacement j = (n,k) and name them LIST1 to LISTJ. For 12 stocks with 2 exluded each time, there are 66 combinations.

Then in the code, increment the watch list number for J=1 to 66 optimization backtests using these lists.

Sorry for the trouble with this problem. I appreciate the support.

Random numbers generated by Mersene Twister (mtRandom) ARE evenly distributed

@makhar88, FWIW, I wanted to test the current Chat-GPT capabilities to generate AFL code (using the GPT-4o model).
I submitted a prompt with my idea: use CBT to achieve your goal (at least as I understand it).

On the first try I received completely incorrect code.

Then, patiently, in multiple steps, I submitted the Porfolio Backtester Interface Reference document with additional examples, and references about the use of matrices, the TRACEF() and the mtRandom() functions.

Once done, I prompted my idea again (essentially use a matrix to store the scores of the signals, shuffle them, invalidate the first and second one to skip the corresponding signals - only doing this if there are 3 or more signals) and I got this code (only the top lines were added by me and I modified a single _TRACEF() line):


maxpos = 12; // maximum number of open positions
SetOption( "InitialEquity", 100000 ); // set initial equity = 25K
SetOption( "MaxOpenPositions", maxpos );
SetPositionSize( 100 / maxpos, spsPercentOfEquity );
RoundLotSize = 1;
SetOption("AllowPositionShrinking", True);
SetOption( "CommissionMode", 2 );
SetOption( "CommissionAmount", 1 );
SetTradeDelays( 1, 1, 1, 1 );
BuyPrice = SellPrice = Open;
PositionScore = RSI();
dt = DateTime();

/// from here Chat-GPT generated code (model GPT-4o)

// Generate random signals using MA crossover
ShortMA = MA(Close, 20);
LongMA = MA(Close, 50);

// Entry signals
Buy = Cross(ShortMA, LongMA);
Sell = Cross(LongMA, ShortMA);

// Set up Custom Backtester (CBT)
SetCustomBacktestProc("");

if (Status("action") == actionPortfolio) 
{
    bo = GetBacktesterObject();    // Get backtester object
    bo.PreProcess();               // Do pre-processing

    for (i = 0; i < BarCount; i++) // Loop through all bars
    {
        sigCount = bo.GetSignalQty(i, 1); // Get count of entry signals at bar i
        _TRACEF("Bar: %g - %s - Entry Signals Count: %g", i, DateTimeToStr(dt[i]), sigCount);

        if (sigCount >= 3) 
        {
            // Initialize a 1D matrix (vector) for scores
            signalScores = Matrix(sigCount, 1, 0);

            k = 0; // Matrix index
            // Loop through signals to populate the scores matrix
            for (sig = bo.GetFirstSignal(i); sig; sig = bo.GetNextSignal(i)) 
            {
                if (sig.IsEntry() && sig.IsLong()) 
                {
                    score = abs(sig.PosScore); // Get the absolute score of the signal
                    signalScores[k][0] = score; // Assign to the matrix
                    // Line edited to make it more useful in debugging
                    _TRACEF("%g - %s - [%s] - Score added: %g at index: %g", i, DateTimeToStr(dt[i]), sig.Symbol, score, k);
                    k++;
                }
            }

            // Shuffle the signalScores matrix
            for (j = 0; j < sigCount; j++) 
            {
                rndIdx = floor(mtRandom() * sigCount); // Generate random index
                temp = signalScores[j][0];
                signalScores[j][0] = signalScores[rndIdx][0];
                signalScores[rndIdx][0] = temp; // Swap values
            }

            _TRACEF("Shuffled Signal Scores: %s", MxToString(signalScores));

            // Replace the first 2 scores with zero to skip those signals
            signalScores[0][0] = 0;
            signalScores[1][0] = 0;
            _TRACEF("Updated Signal Scores (First 2 Skipped): %s", MxToString(signalScores));

            // Loop again to update signals based on shuffled scores
            for (sig = bo.GetFirstSignal(i); sig; sig = bo.GetNextSignal(i)) 
            {
                if (sig.IsEntry() && sig.IsLong()) 
                {
                    absScore = abs(sig.PosScore);
                    found = 0; // Flag to check if score exists in matrix
                    for (m = 0; m < sigCount; m++) 
                    {
                        if (signalScores[m][0] == absScore) 
                        {
                            found = 1;
                            break;
                        }
                    }
                    if (!found) 
                    {
                        sig.Price = -1; // Mark signal to be skipped
                        _TRACEF("Signal Skipped: Score = %g, Symbol = %s", absScore, sig.Symbol);
                    }
                }
            }
        }
        else
        {
            _TRACEF("Bar: %g, Signals < 3, No Skipping Required.", i);
        }

        bo.ProcessTradeSignals(i); // Process remaining signals
    } // End of bar loop

    bo.PostProcess(); // Post-processing
}

Not bad IMO.

It's worth pointing out that this logic is probably not the best way to randomly skip some signals: my aim was mainly to test the ability to get correct code - here I mean runnable - using an LLM model.

2 Likes

Tomasz, it is true what you wrote, but in the limit of sufficient samples. If you have a sample of 12 stocks, some may be close. When you discretize, you may skip more or less than 2. It is similar to when you toss a fair coin 12 times. You will not get exactly 6 heads and 6 tails. That will happen only if you toss many times the coin.

Regardless, if there is some way to generate a random exclusion list once in the beginning of the backtest for two symbols out of 12 in the watchlist, I would appreciate the help. Or if there is a way to generate all combinations of 12 that exclude 2 without replacement and store them in lists, that would be the best.

It is good but for signals it can be done with a few stratements in standard AFL, as Tomasz showed. Also, when doing it with signals, you have the issue of needing to go back and adjust position sizes of the signals you kept.

@makhar88, as I wrote, the main goal of the previous post was to evaluate Chat-GPT's ability to spit out "correct" code. (By the way, when I provided documentation and some examples, I only had to request one change since the generated code included an "append" function on the matrix that doesn't exist...).

Below is something similar, without using the CBT, selecting # tickers to discard randomly (which seems closer to your goal).
I left the _TRACEF() functions to help understand the code.

// https://forum.amibroker.com/t/skip-random-symbols-from-watchlist/39544

#pragma maxthreads 1

maxToSkip = Param( "How many tickers to skip", 1, 0, 10, 1 );

wlnum = GetOption( "FilterIncludeWatchlist" );
watchlist = CategoryGetSymbols( categoryWatchlist, wlnum ) ;
symbolCount = StrCount( watchlist, "," ) + 1;

// maxpos = symbolCount - maxToSkip;
maxpos = 12 - maxToSkip;
SetOption( "InitialEquity", 100000 ); // set initial equity = 100K
SetOption( "MaxOpenPositions", maxpos );
SetPositionSize( 100 / maxpos, spsPercentOfEquity );
RoundLotSize = 1;
SetOption( "AllowPositionShrinking", True );
SetOption( "CommissionMode", 2 );
SetOption( "CommissionAmount", 1 );
SetTradeDelays( 1, 1, 1, 1 );
BuyPrice = SellPrice = Open;
PositionScore = RSI();
dt = DateTime();

// relevant code to define tickers to skip
if( Status( "stocknum" ) == 0 )
{
    _TRACEF( "Selected watchlist (%g) - %g symbols: %s", wlNum, symbolCount, watchlist );
    StaticVarRemove( "Stocks_IDs_*" );
    m = Matrix( symbolCount, 1, 0 );

    // Fill the matrix and set some static variables
    for( i = 0; i < symbolCount;  i++ )
    {
        symbol = StrExtract( watchlist, i, ',' );
        m[i][0] = i + 1; // add one since we will use negative numbers to indicate what to skip and -0 is the same as 0...
        _TRACEF( "%g/%g - %s (ID: %g)", i + 1, symbolCount, symbol, m[i][0] );
        StaticVarSet( "Stocks_IDs_" + symbol, i + 1 );
    }
    _TRACEF( "Matrix IDs: %s", MxToString( m ) );

    // Shuffle the matrix
    for( j = 0; j < symbolCount; j++ )
    {
        rndIdx = floor( mtRandom() * symbolCount ); // Generate random index
        _TRACEF( "Random indexID: %g", rndIdx );
        temp = m[j][0];
        m[j][0] = m[rndIdx][0];
        m[rndIdx][0] = temp; // Swap values
    }
    _TRACEF( "Shuffled matrix IDs: %s", MxToString( m ) );

    for( j = 0; j < maxToSkip; j++ )
    {
        _TRACEF( "Will skip: [%s]", StrExtract( watchlist, m[j][0] - 1 ) );
        m[j][0] = -m[j][0]; // flag as selected to be ignored - negate the ID
    }
    _TRACEF( "Shuffled ID with flags: %s", MxToString( m ) );
	
	// Save the matrix
    StaticVarSet( "Stocks_IDs_Matrix", m );
}

//// A FAKE SYSTEM - Replace with your rules
// Generate random signals using MA crossover
ShortMA = MA( Close, 20 );
LongMA = MA( Close, 50 );
// Entry signals
BuySignal = Cross( ShortMA, LongMA );
SellSignal = Cross( LongMA, ShortMA );


// Skipping the random selected tickers
symbol = Name();
symbolID = StaticVarGet( "Stocks_IDs_" + symbol );
canTrade = True;
_TRACEF( "Processing [%s] - ID: %g",  symbol, symbolID );
// Retrieve the saved shuffled matrix  
m = StaticVarGet( "Stocks_IDs_matrix" );
for( j = 0; j < symbolCount ; j++ )
{
    mID = m[j][0];
    if( mID == -symbolID )
    {
        _TRACEF( "----------------> Skipping trades for [%s] - ID: %g == %g",  symbol, symbolID, mID );
        canTrade = False;
        break;
    }
    
    else if( mID == symbolID )
    {
        _TRACEF( "Ok to trade [%s] - ID: %g == %g",  symbol, symbolID, mID );
        break;
    }
}

// Rest of code...
Buy = canTrade and BuySignal;
Sell = canTrade and SellSignal;

When done with debugging, remove/comment out the #pragma

1 Like

@makhar88 (regardless of whether my formula reaches your goal) I have one question: what exactly is the purpose of this type of testing? What do you want to verify/demonstrate? How much the results depend on the composition of the watchlist used?
(In particular, if the watchlist includes an action like NVDA, randomly including or excluding it can lead to extremely different results).

Thanks in advance for your feedback.

You wrote a more refined version of what I was going to say.

"stocknum" is assigned alphabetically to the LIST prior to Analysis begin, so it should not change over multiple runs. ie. stock ID of a symbol is "stocknum" for a constant list.

Now all you have to do is get 2 Random numbers from 0 to stockCount-1 and if stocknum==rand1 or rand2, skip

Of course there is the Monte Carlo way too.

1 Like

@nsm51, I used the "shuffle" way because I'm lazy and prefer to avoid having to check that each new random number is different from all the others already present in the list when generating a list of x random numbers (especially simpler if x is greater than 2...)

2 Likes

Building on the solution from @beppe, this is the way that I would solve it:


wlnum = GetOption( "FilterIncludeWatchlist" );
watchlist = CategoryGetSymbols( categoryWatchlist, wlnum ) ;
symbolCount = StrCount( watchlist, "," ) + 1;

if (Status("stocknum") == 0)
{
	rndIdx1 = rndIdx2 = floor( mtRandom() * symbolCount ); 	// Generate random index
	excludeSym1 = StrExtract(watchlist, rndIdx1);
	
	while (rndIdx1 == rndIdx2)
	{
		rndIdx2 = floor( mtRandom() * symbolCount );
	}
	excludeSym2 = StrExtract(watchlist, rndIdx2);
	
	StaticVarSetText("excludeSym1", excludeSym1);
	StaticVarSetText("excludeSym2", excludeSym2);
	
	_TRACE("rndIdx1="+rndIdx1+" excludeSym1="+excludeSym1);
	_TRACE("rndIdx2="+rndIdx2+" excludeSym2="+excludeSym2);
}

excludeSym1 = StaticVarGetText("excludeSym1");
excludeSym2 = StaticVarGetText("excludeSym2");

isExcluded =	Name() == excludeSym1 OR
				Name() == excludeSym2;
				
Filter = Status("LastBarInRange");
AddColumn(isExcluded, "Excluded?", 1.0);
SetSortColumns(-3);
3 Likes

Thank you all for your input. I wish you happy holidays!

@nsm51 The objective is stochastic modelling, akin to a Monte Carlo analysis.

This topic was automatically closed 100 days after the last reply. New replies are no longer allowed.