Hi everyone,
I’d like to thank @smquantum for the internal support on this topic. After some testing I was finally able to put together a system that reflects the idea I originally had in mind.
// Norgate Data Formulas are included
#include_once "Formulas\Norgate Data\Norgate Data Functions.afl";
OnSecondLastBarOfDelistedSecurity = !IsNull(GetFnData("DelistingDate")) AND (BarIndex() == (LastValue(BarIndex()) -1) OR DateTime() >= GetFnData("DelistingDate") ) ;
OnLastTwoBarsOfDelistedSecurity = !IsNull(GetFnData("DelistingDate")) AND (BarIndex() >= (LastValue(BarIndex()) -1) OR DateTime() >= GetFnData("DelistingDate") );
/* --- Watchlist and number of positions are detected --- */
wlnumber = GetOption( "FilterIncludeWatchlist" );
watchlist = GetCategorySymbols( categoryWatchlist, wlnumber );
NumOfStocks = StrCount( watchlist, "," ) + 1;
// System Parameters
PositionsNumber = Param("PositionsNumber",15,5,50,5);
holdingDays = Param("holdingDays",60,50,300,5);
lookBackPeriod = Param("lookBackPeriod",200,20,400,10);
//Settings
SetOption( "InitialEquity", 100000 );
SetOption( "MaxOpenPositions", PositionsNumber );
SetOption( "CommissionMode", 1 );
SetOption( "CommissionAmount", 0.1 );
SetOption( "usecustombacktestproc", False );
SetBacktestMode( backtestRegular );
SetTradeDelays( 1, 1, 1, 1 );
BuyPrice= O;
SellPrice = O;
PositionSize = -100 / PositionsNumber;
// === Universe ===
benchmark = "$SPX";
isInIndex = NorgateIndexConstituentTimeSeries( benchmark );
TradingUniverse = isInIndex AND NOT OnLastTwoBarsOfDelistedSecurity AND NorgatePaddingStatusTimeSeries() == 0;
if( Status( "StockNum" ) == 0 )
{
StaticVarRemove( "*" );
/* We generate a loop to find the daily returns for each ticker over the entire history.
Then we mask them for the days that were part of the benchmark index, and finally save
and accumulate the metrics that will later be used to calculate the statistics for the
daily returns of the investment universe, such as the mean and standard deviation.*/
for( n = 0; ( Symbol = StrExtract( watchlist, n ) ) != ""; n++ )
{
SetForeign( Symbol );
isInIndex = NorgateIndexConstituentTimeSeries( benchmark );
StaticVarAdd( "UniverseNumberStocksCount", IIf( IsNull( IIf( isInIndex, isInIndex, Null ) ), 0, 1 ) );
dailyRets = C / Ref( C, -1 ) - 1;
dailyRetsInIndex = IIf( isInIndex AND NOT IsNull( dailyRets ), dailyRets, 0 );
StaticVarSet( "dailyRetsInIndex_" + Symbol, dailyRetsInIndex );
StaticVarAdd( "dailyRetsUniverseSum", dailyRetsInIndex );
StaticVarAdd( "dailyRetsUniverseSumSquared", dailyRetsInIndex * dailyRetsInIndex );
RestorePriceArrays();
}
/* We read the accumulated static variables with the information necessary to calculate the mean
and standard deviation of the daily returns of the investment universe..*/
dailyRetsUniverseSum = StaticVarGet( "dailyRetsUniverseSum" );
dailyRetsUniverseSumSquared = StaticVarGet( "dailyRetsUniverseSumSquared" );
UniverseNumberStocksCount = Max( StaticVarGet( "UniverseNumberStocksCount" ), 1 );
dailyRetsUniverseMean = dailyRetsUniverseSum / UniverseNumberStocksCount;
dailyRetsUniverseVar = Max( dailyRetsUniverseSumSquared / UniverseNumberStocksCount - dailyRetsUniverseMean * dailyRetsUniverseMean, 1e-12 );
dailyRetsUniverseStd = Sqrt( dailyRetsUniverseVar );
StaticVarSet( "dailyRetsUniverseMean", dailyRetsUniverseMean );
StaticVarSet( "dailyRetsUniverseStd", dailyRetsUniverseStd );
/* We generated a cycle to find the cross-sectional z-scores of the investment universe, normalized by
the daily returns of each ticker. We then smoothed and masked them for the days that were part of
the benchmark index, and finally saved and accumulated the metrics that will later be used
to calculate the cross-sectional z-score statistics of the investment universe, such as the mean
and standard deviation.*/
for( n = 0; ( Symbol = StrExtract( watchlist, n ) ) != ""; n++ )
{
SetForeign( Symbol );
isInIndex = NorgateIndexConstituentTimeSeries( benchmark );
dailyRets = C / Ref( C, -1 ) - 1;
dailyRetsUniverseMean = StaticVarGet( "dailyRetsUniverseMean" );
dailyRetsUniverseStd = Max( StaticVarGet( "dailyRetsUniverseStd" ), 1e-12 );
dailyRetsZscore = ( dailyRets - dailyRetsUniverseMean ) / dailyRetsUniverseStd;
StaticVarSet( "dailyRetsZscore_" + Symbol, dailyRetsZscore );
dailyRetsZscoreMA = MA( dailyRetsZscore, lookBackPeriod );
StaticVarSet( "dailyRetsZscoreMA_" + Symbol, dailyRetsZscoreMA );
dailyRetsZscoreMAInIndex = IIf( isInIndex AND NOT IsNull( dailyRetsZscoreMA ), dailyRetsZscoreMA, 0 );
StaticVarSet( "dailyRetsZscoreMAInIndex_" + Symbol, dailyRetsZscoreMAInIndex );
StaticVarAdd( "universedailyRetsZscoreMASum", dailyRetsZscoreMAInIndex );
StaticVarAdd( "universedailyRetsZscoreMASumSquared", dailyRetsZscoreMAInIndex * dailyRetsZscoreMAInIndex );
RestorePriceArrays();
}
/* We read the accumulated static variables with the information necessary to calculate the mean and standard
deviation of the smoothed z-scores of the investment universe..*/
universedailyRetsZscoreMASum = StaticVarGet( "universedailyRetsZscoreMASum" );
universedailyRetsZscoreMASumSquared = StaticVarGet( "universedailyRetsZscoreMASumSquared" );
UniverseNumberStocksCount = Max( StaticVarGet( "UniverseNumberStocksCount" ), 1 );
universedailyRetsZscoreMAMean = universedailyRetsZscoreMASum / UniverseNumberStocksCount;
universedailyRetsZscoreMAVar = Max( universedailyRetsZscoreMASumSquared / UniverseNumberStocksCount - universedailyRetsZscoreMAMean * universedailyRetsZscoreMAMean, 1e-12 );
universedailyRetsZscoreMAStd = Sqrt( universedailyRetsZscoreMAVar );
StaticVarSet( "universedailyRetsZscoreMAMean", universedailyRetsZscoreMAMean );
StaticVarSet( "universedailyRetsZscoreMAStd", universedailyRetsZscoreMAStd );
/* We generate a final cycle to calculate the final normalized factor, mask it, and store
the days the stock was included in the investment universe.*/
for( n = 0; ( Symbol = StrExtract( watchlist, n ) ) != ""; n++ )
{
SetForeign( Symbol );
isInIndex = NorgateIndexConstituentTimeSeries( benchmark );
dailyRetsZscoreMAInIndex = StaticVarGet( "dailyRetsZscoreMAInIndex_" + Symbol );
universedailyRetsZscoreMAMean = StaticVarGet( "universedailyRetsZscoreMAMean" );
universedailyRetsZscoreMAStd = Max( StaticVarGet( "universedailyRetsZscoreMAStd" ), 1e-12 );
finalFactor = ( dailyRetsZscoreMAInIndex - universedailyRetsZscoreMAMean ) / universedailyRetsZscoreMAStd;
finalFactorInIndex = IIf( isInIndex AND NOT IsNull( finalFactor ), finalFactor, 0 );
StaticVarSet( "finalFactorInIndex_" + Symbol, finalFactorInIndex );
RestorePriceArrays();
}
}
// Trading System signals
smoothedCrossSectionalMomZscore = StaticVarGet("finalFactorInIndex_" + Name());
PositionScore = 10000 + Ref(smoothedCrossSectionalMomZscore,-1);
//PositionScore = 10000 + ROC(C,lookBackPeriod);
Buy = TradingUniverse;
Sell = OnSecondLastBarOfDelistedSecurity OR NOT isInIndex;
ApplyStop(stopTypeNBar, stopModeBars, holdingDays);
The factor here can easily be adapted to other indicators (RSI, etc.) to build cross-sectional versions. So far, results look quite similar to the classic ROC approach, but it was a very useful exercise for me to practice AmiBroker and understand better how to handle these cross-sectional calculations.
Big thanks also to the community for the knowledge that is constantly shared here. If anyone wants to take a look at the code or share feedback, it would be much appreciated.