Implementing cross-sectional momentum factors (beyond ROC) in AmiBroker

Hello everyone,

This is my first post here, so I’d like to start by saying how glad I am to be part of this community. I really value the knowledge that is shared in this forum, and I’ve already learned a lot just by reading through past discussions. Thank you all for the time and effort you put into contributing here.

Over the past few months I’ve been working on migrating some of my cross-sectional momentum research from Python into AmiBroker. In Python, I’ve tested different variations, from the classic ROC to smoother approaches (e.g. daily ROC → rank → rolling → z-score).

I’ll attach one image with the Python code (based on adj_closes_unstack, with tickers as columns and dates as rows), and below is the current AmiBroker implementation of the classic ROC using StaticVarGenerateRanks, as recommended by this post from @Tomasz. That part works fine!

// === Momentum and ranking calculation (SCAN only) ===
if( Status( "action" ) == actionScan )
{
    if( Status( "StockNum" ) == 0 )
    {
        StaticVarRemove( "*" );
    }

	rocFactor = ROC(Close, lookback_period_roc);
	StaticVarSet("roc_" + Name(), rocFactor);

    _exit();
}

// === Ranking generation (once in BACKTEST mode) ===
if( Status("StockNum") == 0 )
{
    // remove previous rankings if any
    StaticVarRemove("rank*");

    StaticVarGenerateRanks("rank_", "roc_", 0, 1224);
}

// === Get the ranking of the current symbol ===
rankingROC = StaticVarGet("rank_roc_" + Name());


// === Signals ===
PositionScore = 10000 - rankingROC;  // Prioritize the best ranks

Where I’m having trouble is with the more advanced factors, which require calculating cross-sectional statistics by bar (daily rank/z-score across symbols, then rolling averages, then another standardization).

I’ve searched the forum but haven’t yet found a concrete discussion on this. I also experimented with AI-generated code, but the results were often too complex or unreliable.

I may simply have overlooked existing solutions, so if this has already been discussed I’d be grateful if someone could point me in the right direction. Otherwise, any guidance or hints on how to structure these kinds of cross-sectional calculations in AFL would be very much appreciated.

Thanks again for this great forum and community!

1 Like

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.

1 Like

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