Buying a given asset instead of cash for empty positions

Hello

I should appreciate it if you would let me know how to programme a system which buys a given asset instead of going to cash everytime one of its positions is empty.

As an example, consider I backtest the following system, using a wachlist which contains SPY, VEU, TLT, VNQ, DBC

EnableRotationalTrading();
SetTradeDelays(0,0,0,0);
BuyPrice = Close;
SellPrice = Close;
PositionScore = IIf(Close > EMA(Close,10),1,0);
PositionSize = -20;

If one position is sold, its value goes to cash. I would prefer it goes to IEF instead, I am not sure if there is a simple way to programme that.

Besides, if I do not want to have repeated positions for SPY, VEU, TLT, VNQ, DBC, I should use BacktestRegular mode. It seems uncompatible with buying successive positions of IEF.

Thank you in advance for your answer.

Best regards,

1 Like

Hello -
I am also having similar challenge i.e., how to program so that when a position is sold then instead of cash it goes into a given asset (ex: IEF). Most pair-switching strategies are of of this type.

If it is not possible and is a limitation of Amibroker then so be it. Good to know that as well. Then I can explore some other alternative.

Regards,
Durga

There are many methods to achieve that.

In simple scenario with two symbols (just trade one pair), but both in one watch list and apply backtest to that watch list

Symbol1 = "Sym A";
Symbol2 = "IEF";

// symbol1 generates buy / sell rules
SetForeign( Symbol1 );
Buy = ... your normal buy rules
Sell = ... your normal sell rules
RestorePriceArrays();

n = Name();

if( n == Symbol2 )
{
   // for IEF use REVERSE rules
   tmp = Buy;
   Buy = Sell;
   Sell = tmp;
}

The code like this will just trade "reverse" rules on both symbols.

This is similar to concepts described in this KB article:
http://www.amibroker.com/kb/2014/09/20/broad-market-timing-in-system-formulas/

For multiple symbol scenario (when you trade many symbols, but switch to money market fund if you have free cash), you can use a very simple method. Just enter your money market interest symbol in the Settings. It will calculate your earnings as if you would put cash into the fund when it is not allocated for your traders.

Documentation from the Release notes for version 5.93

Backtester: implemented variable interest rate earnings
To use variable interest rate:

  1. Add a symbol that will hold interest rates. It does not need to have quotes every day,
    you can have only quotes on days when interest rate changes.
    Interest rates should be expressed in PERCENTS. So if interest is 5% you should enter 5 in "close" price field for particular date

  2. Enter the rate symbol into "Dynamic interest symbol" in the Settings.

  3. Enter "Fixed interest rate" in the Settings. It will be used for days PRIOR to very first date available in intrest rate symbol.

Since IEF is not expressed in percents you will need to create synthetic symbol that follows the documentation.

A third method would be to use custom backtest procedure.

9 Likes

Hi @Tomasz - Thank you very much. This was a big issue for me and your response is helpful. I will try creating a synthetic symbol to have IEF daily percent change and try.

I read docs on custom backtest procedure but not sure how to do in this case. Any chance of pointers on logic/pseudo-code on how to solve using custom backtest procedure? I am a professional developer albeit in Python & Java land. So I can translate myself to actual code once I understood the logic.

Regards,
Durga

One note: synthetic interest rate symbol should hold current annual interest rate.

As to custom backtest code, you would need to: use mid-level CBT and AFTER processing bar’s signals check if you have any cash left each day and scale-in to IEF the way described here:
http://www.amibroker.com/kb/2006/03/06/re-balancing-open-positions/

Then each day BEFORE processing of bar’s signals you would need to find out if you have any entry signals, sum up their position size and release required amount of cash by scaling out of IEF (again the way described in KB)

// This example just shows the logic
// It is not complete
SetCustomBacktestProc("");

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

    for (i = 0; i < BarCount; i++)	//  Loop through all bars
    {
        // each bar in CBT
        cashneeded = CheckEntrySignalsAndFindOutHowMuchCashYouNeed();

        ScaleOutFromMoneyFund( cashneeded  );
        
        bo.ProcessTradeSignals( i );	//  Process trades at bar (always required)

        // do we have more than $1000 of cash left ? 
        if( bo.Cash > 1000 ) ScaleInToMoneyFund( bo.Cash ); 
    }	  
      
    bo.PostProcess();	//  Do post-processing (always required)
}

or you could just close money fund position at the beginning of each day (that would be simpler)

5 Likes

Hi @Tomasz - Thanks. I appreciate much. I will pursue CBT model based on your pointers and share code with community once I am done.

Regards,
Durga

Tomasz,

Thank you very much for your answer, it has been very useful. However, I would like to make another turn of the screw, considering a scenario with three pairs with different defensive assets. I propose a system from Dick Stoken’s book " Survival of the Fittest for Investors", which accounts the following pairs: SPY vs IEF, GLD vs TLT, VNQ vs IEF.

When using BacktestRegular, I have to introduce BIV instead of IEF for the third pair, since the backtest will not take a second position in IEF.

When using BacktestRegularRawMulti, the backtest considers positions with repeated symbols that distort the intended system operation.

ÂżHow could I take IEF as defensive asset in pairs 1 and 3, preserving the correct operation of the system?

Thank you in advance for your answer.

Best Regards.

// --- Inputs ---

UpperChannelPeriod = 6; 
LowerChannelPeriod = 12; 

Pair_1A = "SPY";
Pair_1B = "IEF";
Pair_2A = "GLD";
Pair_2B = "TLT";
Pair_3A = "VNQ";
Pair_3B = "BIV";

// --- Channel Calculation ---

UpperChannelValue_1 = Ref(HHV(Foreign(Pair_1A, "Close"), UpperChannelPeriod), -1);
LowerChannelValue_1 = Ref(LLV(Foreign(Pair_1A, "Close"), LowerChannelPeriod), -1);

UpperChannelValue_2 = Ref(HHV(Foreign(Pair_2A, "Close"), UpperChannelPeriod), -1);
LowerChannelValue_2 = Ref(LLV(Foreign(Pair_2A, "Close"), LowerChannelPeriod), -1);

UpperChannelValue_3 = Ref(HHV(Foreign(Pair_3A, "Close"), UpperChannelPeriod), -1);
LowerChannelValue_3 = Ref(LLV(Foreign(Pair_3A, "Close"), LowerChannelPeriod), -1);

// --- Operation Rules ---

if (Name() == Pair_1A)
{Buy = Foreign(Pair_1A, "Close") > UpperChannelValue_1; Sell = Foreign(Pair_1A, "Close") < LowerChannelValue_1;} 
if (Name() == Pair_1B)
{Buy = Foreign(Pair_1A, "Close") < LowerChannelValue_1; Sell = Foreign(Pair_1A, "Close") > UpperChannelValue_1;} 

if (Name() == Pair_2A)
{Buy = Foreign(Pair_2A, "Close") > UpperChannelValue_2; Sell = Foreign(Pair_2A, "Close") < LowerChannelValue_2;} 
if (Name() == Pair_2B)
{Buy = Foreign(Pair_2A, "Close") < LowerChannelValue_2; Sell = Foreign(Pair_2A, "Close") > UpperChannelValue_2;} 

if (Name() == Pair_3A)
{Buy = Foreign(Pair_3A, "Close") > UpperChannelValue_3; Sell = Foreign(Pair_3A, "Close") < LowerChannelValue_3;} 
if (Name() == Pair_3B)
{Buy = Foreign(Pair_3A, "Close") < LowerChannelValue_3; Sell = Foreign(Pair_3A, "Close") > UpperChannelValue_3;} 

As I wrote, in multiple symbol case and necessity to scale in/out the proper approach is to use CBT (custom backtest) or the Settings as explained before.

Hi @Tomasz -
I have written the code to do ScaleOut & ScaleIn to money fund. The system (given below) executes without errors but I don’t see Scale trades nor my printf(…) statements. Not sure what I am doing wrong. I appreciate much any suggestions.

/***
========================================
System: Trend Trading with Money Fund
========================================
*/
// Detect watchlist
wlnumber		= GetOption( "FilterIncludeWatchlist" );
watchlist		= GetCategorySymbols( categoryWatchlist, wlnumber );
numAssets		= StrCount( watchlist, "," ) + 1;

// Backtester Settings 
SetOption("InitialEquity",							100000	);
SetOption("MinShares",								100 		);
SetOption("FuturesMode", 						False		);
SetOption("AllowPositionShrinking",		False		);
SetOption("ActivateStopsImmediately",	False		);
SetOption("ReverseSignalForcesExit", 		False		);
SetOption("AllowSameBarExit",					False		);
SetOption("CommissionMode", 				2				); // 2 = per trade
SetOption("CommissionAmount", 			9.95		); //per trade
SetOption("AccountMargin",						100			); // 100 = no margin
SetOption("UseCustomBacktestProc",		True		);
SetTradeDelays( 1, 1, 1, 1);
BuyPrice = SellPrice = ShortPrice = CoverPrice = Open;

// inputs
maLength 	    =  20;
maxPositions = numAssets;
PositionSize 	=  100 /maxPositions;

// Portfolio settings
SetBacktestMode(backtestRegular );
SetOption("MaxOpenPositions", maxPositions);
SetPositionSize(PositionSize,	spsPercentOfEquity );

// signals
Buy = Cross(Close, MA(Close, maLength));
Sell = Cross(MA(Close, maLength), Close);
Buy = ExRem(Buy, Sell);
Sell = ExRem(Sell, Buy);

//############  Functions follows ########
function cashRequired(bar, bo)
{
	cashNeeded = 0;
	for (sig = bo.GetFirstSignal(bar); sig; sig = bo.GetNextSignal(bar)) // Loop through signals at this bar
	{ 
		pSize = sig.PosSize;
		if(pSize < 0)  pSize = (-pSize/100) * bo.Equity;  // Convert from percent to dollar value
		if(sig.IsEntry()) cashNeeded = cashNeeded + pSize;				
		if(sig.IsExit()) cashNeeded = cashNeeded - pSize;	
	}
	return cashNeeded;
}

function ScaleOutFromMoneyFund(bar, bo, symbol, cash)
{
	mfPosition = bo.FindOpenPos(symbol);
	if(mfPosition != null)
	{
		if(mfPosition.GetPositionValue() < cash)  cash = mfPosition.GetPositionValue(); 
		price = mfPosition.GetPrice(bar, "C");
		bo.ScaleTrade(bar, symbol, False, price, cash); // ScaleOut
	}
}

// ########## Custom-backtest procedure follows ##########
if( Status("action") == actionPortfolio ) 
{ 
    moneyFundSymbol = "VFITX";
	moneyFundClose = Foreign(moneyFundSymbol, "Close");
	
    bo = GetBacktesterObject();	//  Get backtester object
    bo.PreProcess();	//  Do pre-processing (always required)
    for(i = 0; i < BarCount; i++)	//  Loop through all bars
    {
		cashNeeded = cashRequired(i, bo); // Get cash needed for entry signals
		
		// Scale out from Money Fund for required cash.
		if(cashNeeded > 1000)  
		{
			ScaleOutFromMoneyFund(i, bo, moneyFundSymbol, cashNeeded);
			printf("Scaled Out...Cash:%g", cashNeeded);
		}
		
        bo.ProcessTradeSignals( i );	//  Process trades at bar (always required)

        // Scale in to Money Fund if we have more than $1000 of cash left. 
        price = moneyFundClose[i];
        if( bo.Cash > 1000 ) 
        {
			bo.ScaleTrade(i, moneyFundSymbol, True, price,  bo.Cash); // ScaleIn
			printf("Scaled In...Cash:%g", bo.Cash);
		}
    }	  
    bo.PostProcess();	//  Do post-processing (always required)
    
    st = bo.GetPerformanceStats(0); // get stats for all trades 
    // Expectancy = (%Win * AvgProfit - %Los * AvgLos)/Avg Loss
    expectancy = st.GetValue("WinnersAvgProfit")*st.GetValue("WinnersPercent")/100 + 
                st.GetValue("LosersAvgLoss")*st.GetValue("LosersPercent")/100; 
    expectancy = expectancy/(-st.GetValue("LosersAvgLoss"));

    // Here we add custom metric to backtest report 
    bo.AddCustomMetric( "Expectancy", expectancy ); 
} 

Regards,
Durga

Updated the code with _TRACE statements. I can see the backtester ScaleOut & ScaleIn functions being called in the log. Not sure why I cannot see the Scaled trades in the trades list or backtest report though.

//========================================
// System: Trend Switching with Money Fund
//========================================

// Detect watchlist
wlnumber		= GetOption( "FilterIncludeWatchlist" );
watchlist		= GetCategorySymbols( categoryWatchlist, wlnumber );
numAssets		= StrCount( watchlist, "," ) + 1;
_TRACE("!CLEAR!");

// Backtester Settings 
SetOption("InitialEquity",							100000	);
SetOption("MinShares",								100 		);
SetOption("FuturesMode", 						False		);
SetOption("AllowPositionShrinking",		False		);
SetOption("ActivateStopsImmediately",	False		);
SetOption("ReverseSignalForcesExit", 		False		);
SetOption("AllowSameBarExit",					False		);
SetOption("CommissionMode", 				2				); // 2 = per trade
SetOption("CommissionAmount", 			9.95		); //per trade
SetOption("AccountMargin",						100			); // 100 = no margin
SetOption("UseCustomBacktestProc",		True		);
SetTradeDelays( 1, 1, 1, 1);
BuyPrice = SellPrice = ShortPrice = CoverPrice = Open;

// inputs
maLength 	    = Param("MA Length", 9, 3, 12, 1 );
isOptimize 	= ParamToggle( "Optimize", "No|Yes", 0);
if (isOptimize)
{
    maLength 	= Optimize("MA Length", 9, 3, 12, 1 );
}
maxPositions = numAssets;
PositionSize 	=  100 /maxPositions;

// Portfolio settings
SetBacktestMode(backtestRegular );
SetOption("MaxOpenPositions", maxPositions);
SetPositionSize(PositionSize,	spsPercentOfEquity );

// signals
Buy = Cross(Close, MA(Close, maLength));
Sell = Cross(MA(Close, maLength), Close);
Buy = ExRem(Buy, Sell);
Sell = ExRem(Sell, Buy);

//############  Functions follows ########
function cashRequired(bar, bo)
{
	cashNeeded = 0;
	for (sig = bo.GetFirstSignal(bar); sig; sig = bo.GetNextSignal(bar)) // Loop through signals at this bar
	{ 
		pSize = sig.PosSize;
		if(pSize < 0)  pSize = (-pSize/100) * bo.Equity;  // Convert from percent to dollar value
		if(sig.IsEntry()) cashNeeded = cashNeeded + pSize;				
		if(sig.IsExit()) cashNeeded = cashNeeded - pSize;	
	}
	_TRACE("Bar: " + bar + ", CashNeeded: " + cashNeeded);
	return cashNeeded;
}

// ########## Custom-backtest procedure follows ##########
if( Status("action") == actionPortfolio ) 
{ 
    moneyFundSymbol = "IEF";
	moneyFundClose = Foreign(moneyFundSymbol, "Close");
	
    bo = GetBacktesterObject();	//  Get backtester object
    bo.PreProcess();	//  Do pre-processing (always required)
    for(i = 0; i < BarCount; i++)	//  Loop through all bars
    {
        price = moneyFundClose[i];
		cashNeeded = cashRequired(i, bo); // Get cash needed for entry signals
		
		// Scale out from Money Fund for required cash.
		if(cashNeeded > 1000)  
		{
			bo.ScaleTrade(i, moneyFundSymbol, False, price, cashNeeded); // ScaleOut
			_TRACE("ScaleOut... Bar: " + i + ", Symbol: " + moneyFundSymbol + ", Price: " + price + ", Cash: " + cashNeeded);
		}
		
        bo.ProcessTradeSignals( i );	//  Process trades at bar (always required)

        // Scale in to Money Fund if we have more than $1000 of cash left. 
        if( bo.Cash > 1000 ) 
        {
			bo.ScaleTrade(i, moneyFundSymbol, True, price,  bo.Cash); // ScaleIn
			_TRACE("ScaleIn... Bar: " + i + ", Symbol: " + moneyFundSymbol + ", Price: " + price + ", Cash: " + bo.Cash);
		}
    }	  
    bo.PostProcess();	//  Do post-processing (always required)
    
    st = bo.GetPerformanceStats(0); // get stats for all trades 
    // Expectancy = (%Win * AvgProfit - %Los * AvgLos)/Avg Loss
    expectancy = st.GetValue("WinnersAvgProfit")*st.GetValue("WinnersPercent")/100 + 
                st.GetValue("LosersAvgLoss")*st.GetValue("LosersPercent")/100; 
    expectancy = expectancy/(-st.GetValue("LosersAvgLoss"));

    // Here we add custom metric to backtest report 
    bo.AddCustomMetric( "Expectancy", expectancy ); 
}   

Log:
trend-switching-log

You are NOT scaling because the arguments you pass are incorrect. You should switch Report Mode to DETAILED LOG and then you will see the messages like “Scale In ignored … because of insufficient funds”. This happens when you want to use ALL cash without realizing that you need money for commission too. You need to pass the position size that is little bit slower to allow for commission or you need to enable “allow position shrinking”. There are also constraints like “roundlotsize” that are checked and won’t allow to buy odd lots.

Hi @Tomasz - I have tried the combinations you mentioned. Still cannot get any messages related to "ScaleIn or ScaleOut" in the "DETAILED LOG" or see scaled trades. I have made few additional fixes & trace comments. From the log, I see the portfolio does have excess cash when ScaleIn/Scaleout is initiated. I must be missing something but cannot pinpoint it. I appreciate much your time and another look from you.

// Detect watchlist
wlnumber		= GetOption( "FilterIncludeWatchlist" );
watchlist		= GetCategorySymbols( categoryWatchlist, wlnumber );
numAssets		= StrCount( watchlist, "," ) + 1;
_TRACE("!CLEAR!");

// Backtester Settings 
SetOption("InitialEquity",	100000);
SetOption("MinShares",	100 );
SetOption("FuturesMode", False);
SetOption("AllowPositionShrinking", False);
SetOption("ActivateStopsImmediately",	False);
SetOption("ReverseSignalForcesExit", 	False);
SetOption("AllowSameBarExit",	False);
SetOption("CommissionMode",  2); // 2 = per trade
SetOption("CommissionAmount", 	9.95); //per trade
SetOption("AccountMargin",	100); // 100 = no margin
SetOption("UseCustomBacktestProc",	True	);
SetTradeDelays( 1, 1, 1, 1);
BuyPrice = SellPrice = ShortPrice = CoverPrice = Open;

// inputs
maLength 	    = Param("MA Length", 9, 3, 12, 1 );
isOptimize 	= ParamToggle( "Optimize", "No|Yes", 0);
if (isOptimize)
{
    maLength 	= Optimize("MA Length", 9, 3, 12, 1 );
}
maxPositions = numAssets;
PositionSize 	=  100 /maxPositions;

// Portfolio settings
SetBacktestMode(backtestRegular );
SetOption("MaxOpenPositions", maxPositions);
SetPositionSize(PositionSize,	spsPercentOfEquity );

// signals
Buy = Cross(Close, MA(Close, maLength));
Sell = Cross(MA(Close, maLength), Close);
Buy = ExRem(Buy, Sell);
Sell = ExRem(Sell, Buy);

//############  Functions follows ########
//function ScaleOutFromMoneyFund(bar, bo, symbol, cash)
//{
//	mfPosition = bo.FindOpenPos(symbol);
//	if(mfPosition != null)
//	{
//		if(mfPosition.GetPositionValue() < cash)  cash = mfPosition.GetPositionValue(); 
//		price = mfPosition.GetPrice(bar, "C");
//		bo.ScaleTrade(bar, symbol, False, price, cash); // ScaleOut
//		_TRACE("ScaleOut... Bar:" + bar + " Symbol:" + symbol + " Price:" + price + " Cash:" + cash);
//	}
//}

function cashRequired(bar, bo)
{
	cashNeeded = 0;
	for (sig = bo.GetFirstSignal(bar); sig; sig = bo.GetNextSignal(bar)) // Loop through signals at this bar
	{ 
		pSize = sig.PosSize;
		if(pSize < 0)  pSize = (-pSize/100) * bo.Equity;  // Convert from percent to dollar value
		if(sig.IsEntry()) cashNeeded = cashNeeded + pSize;				
		if(sig.IsExit()) cashNeeded = cashNeeded - pSize;	
	}
	return cashNeeded;
}

// ########## Custom-backtest procedure follows ##########
if( Status("action") == actionPortfolio ) 
{ 
    moneyFundSymbol = "IEF";
	moneyFundClose = Foreign(moneyFundSymbol, "Close");
	
    bo = GetBacktesterObject();	//  Get backtester object
    bo.PreProcess();	//  Do pre-processing (always required)
    for(i = 0; i < BarCount; i++)	//  Loop through all bars
    {
		_TRACE("Bar: " + i + " ======================= ");
        price = moneyFundClose[i];
		cashNeeded = cashRequired(i, bo); // Get cash needed for entry signals

		// Scale out from Money Fund for required cash.
		scaleOutCash = cashNeeded - bo.Cash - 1000;
		if(scaleOutCash < 0) scaleOutCash = 0;
		_TRACE("CashAvailable: " + bo.Cash + ", CashNeeded: " + cashNeeded + ", ToScaleOutCash: " + scaleOutCash);
		if(scaleOutCash > 1000)  
		{
			bo.ScaleTrade(i, moneyFundSymbol, False, price, scaleOutCash); // ScaleOut
			_TRACE("SCALE OUT... Symbol: " + moneyFundSymbol + ", Price: " + price + ", ScaleOutCash: " + scaleOutCash);
		}
		
        bo.ProcessTradeSignals( i );	//  Process trades at bar (always required)

        // Scale in to Money Fund if we have more than $1000 of cash left. 
        scaleInCash = bo.Cash -  cashNeeded - 1000;
        _TRACE("CashAvailable: " + bo.Cash + ", CashNeeded: " + cashNeeded + ", ScaleInCash: " + scaleInCash);
        if( bo.Cash - cashNeeded > 1000 ) 
        {
			bo.ScaleTrade(i, moneyFundSymbol, True, price,  scaleInCash); // ScaleIn
			_TRACE("SCALE IN... Symbol: " + moneyFundSymbol + ", Price: " + price + ", ToScaleInCash: " + scaleInCash  + ", CashRemaining:" + (bo.Cash - scaleInCash));
		}
    }	  
    bo.PostProcess();	//  Do post-processing (always required)
    
    st = bo.GetPerformanceStats(0); // get stats for all trades 
    // Expectancy = (%Win * AvgProfit - %Los * AvgLos)/Avg Loss
    expectancy = st.GetValue("WinnersAvgProfit")*st.GetValue("WinnersPercent")/100 + 
                st.GetValue("LosersAvgLoss")*st.GetValue("LosersPercent")/100; 
    expectancy = expectancy/(-st.GetValue("LosersAvgLoss"));

    // Here we add custom metric to backtest report 
    bo.AddCustomMetric( "Expectancy", expectancy ); 
}   

Log:
2017-08-18_16-05-58

As written in the manual:

http://www.amibroker.com/guide/a_custombacktest.html

long ScaleTrade(long Bar, string Symbol, bool bIncrease, float Price, float PosSize, [optional] variant Deposit)

Low-level method that scales trade on any symbol. This method searches open trade list and if there is no open trade on given symbol it does nothing. Optional Deposit parameter specifies margin deposit for futures, if not given then Price parameter is used.

To scale you have to have open positition for particular symbol. Otherwise use EnterTrade.

Hi @Tomasz - Thanks. Noticed that using scaleout interferes with MaxPositions logic. So instead I am doing exit of full position. Similarly, I am entering/scaling into position only when there are no entry signals. This is not exactly as I thought but is close enough.

I am providing code below for anyone interested and has deeper AFL knowledge to take it further. Thanks again @Tomasz for your time and help.

Regards,
Durga

/***
========================================
System: Trend Switching with Money Fund
========================================
*/
// Detect watchlist
wlnumber		= GetOption( "FilterIncludeWatchlist" );
watchlist		= GetCategorySymbols( categoryWatchlist, wlnumber );
numAssets		= StrCount( watchlist, "," ) + 1;
_TRACE("!CLEAR!");

// Backtester Settings 
SetOption("InitialEquity",							100000	);
SetOption("MinShares",								100		);
SetOption("FuturesMode", 						False		);
SetOption("AllowPositionShrinking",		False		);
SetOption("ActivateStopsImmediately",	False		);
SetOption("ReverseSignalForcesExit", 		False		);
SetOption("AllowSameBarExit",					False		);
SetOption("CommissionMode", 				2				); // 2 = per trade
SetOption("CommissionAmount", 			9.95		); //per trade
SetOption("AccountMargin",						100			); // 100 = no margin
SetOption("UseCustomBacktestProc",		True		);
SetTradeDelays( 1, 1, 1, 1);
BuyPrice = SellPrice = ShortPrice = CoverPrice = Open;

// inputs
maLength 	    = Param("MA Length", 9, 3, 12, 1 );
moneyFundSymbol 	= ParamList("Money Fund",  "IEF|VFITX");
isOptimize 	= ParamToggle( "Optimize", "No|Yes", 0);
if (isOptimize)
{
    maLength 	= Optimize("MA Length", 9, 3, 12, 1 );
}
maxPositions = numAssets;
PositionSize 	=  100 /maxPositions;

// Portfolio settings
SetBacktestMode(backtestRegular );
SetOption("MaxOpenPositions", maxPositions);
SetPositionSize(PositionSize,	spsPercentOfEquity );

// signals
Buy = Cross(Close, MA(Close, maLength));
Sell = Cross(MA(Close, maLength), Close);
Buy = ExRem(Buy, Sell);
Sell = ExRem(Sell, Buy);

// ########## Custom-backtest procedure follows ##########
if( Status("action") == actionPortfolio ) 
{ 
	moneyFundClose = Foreign(moneyFundSymbol, "Close");
    bo = GetBacktesterObject();	//  Get backtester object
    bo.PreProcess();	//  Do pre-processing (always required)
    
    for(i = 0; i < BarCount; i++)	//  Loop through all bars
    {
		_TRACE("Bar: " + i + " ======================= ");
		
		// Initialize Variables
        price = moneyFundClose[i];
        cashNeeded = 0;
        extraCash = 0; 
		
        // Determine Cash needed for entry signals
		for (sig = bo.GetFirstSignal(i); sig; sig = bo.GetNextSignal(i)) // Loop through signals at this bar
		{ 
			pSize = sig.PosSize;
			if(sig.IsEntry()) cashNeeded = cashNeeded + pSize;	
			if(sig.IsExit()) cashNeeded = cashNeeded - pSize;	
		}
		_TRACE("CashAvailable: " + bo.Cash + ", CashNeeded: " + cashNeeded  + ", SignalQty: " + bo.GetSignalQty(i, 1));

		// Check if we have any open position in money fund...
		openPosInMoneyFund = False;
		for (trade = bo.GetFirstOpenPos(); trade; trade = bo.GetNextOpenPos())
        {    
			if(trade.Symbol == moneyFundSymbol)
			{
				openPosInMoneyFund = True;
				price = trade.GetPrice(i, "C");
				break;
			}
        }   
        
		// Exit trade from money fund on any buy signals.
		if((bo.GetSignalQty(i, 1) > 0) && openPosInMoneyFund) 
		{
			 bo.ExitTrade(i, moneyFundSymbol, price); // Exit Trade 
			_TRACE("EXIT.... Symbol: " + moneyFundSymbol + ", Price: " + price);
		}
		
        bo.ProcessTradeSignals( i );	//  Process trades at bar (always required)

        // Enter Money Fund if we have more than $1000 of cash left and no signals to enter
        positionCash = bo.Cash - 1000;
        if((positionCash > 0) && (bo.GetSignalQty(i, 1) == 0))  //If cash available and no signals...
        {
			if(NOT openPosInMoneyFund) 
			{
				bo.EnterTrade(i, moneyFundSymbol, True, price, positionCash); // Enter trade as there is no open position.
				_TRACE("ENTER... Symbol: " + moneyFundSymbol + ", Price: " + price + ", PositionCash: " + positionCash);
			}
			else
			{
				bo.ScaleTrade(i, moneyFundSymbol, True, price, positionCash); // Scale into trade 
				_TRACE("SCALE... Symbol: " + moneyFundSymbol + ", Price: " + price + ", PositionCash: " + positionCash);
			}
		}
    }
    bo.PostProcess();	//  Do post-processing (always required)
    
    st = bo.GetPerformanceStats(0); // get stats for all trades 
    // Expectancy = (%Win * AvgProfit - %Los * AvgLos)/Avg Loss
    expectancy = st.GetValue("WinnersAvgProfit")*st.GetValue("WinnersPercent")/100 + 
                st.GetValue("LosersAvgLoss")*st.GetValue("LosersPercent")/100; 
    expectancy = expectancy/(-st.GetValue("LosersAvgLoss"));

    // Here we add custom metric to backtest report 
    bo.AddCustomMetric( "Expectancy", expectancy ); 
}   
3 Likes

Tomasz, using the dynamic interest rate symbol is not a very good idea when it comes to defensive assets.

When you buy IEF, GLD or similar, you will have some days where your defensive assets go down in price.

Amibroker IGNORES all negative periods for dynamic interest rate symbol, using only positive days in the calculation. If the assets swing wildly in price, the result of the backtest is of zero value.

Your pair trading above works, but I would love to see some working CBT code where you can have multiple symbols in the backtest.

Thanks for posting your code, durga_public.

This is the best I've found so far.

However, it doesn't work so well while using gearing, provided if you want the "given asset" to be geared as well.

This isn't "exactly" what you might have been looking for but might accomodate with some modification whereby checking if the cash equivalent (IEF, TLT etc.) is already being held and then scaling in to more IEF/TLT as other positions drop out and there is more cash on hand in your account.

The objective of this code was to avoid bear market type situations or situations where you want to be out of stocks but perhaps rotate or switch into a cash equivalent like TLT, which would have worked nicely in the 2008 period.

First, a static variable is set in Phase 1 so that market state can be accessed in Phase 2 (CBT). Set your bullmarket to whatever rules keep you in the market and assign it to ConditionBuy variable. That sets a flag or switch whenever you would be in the stock market (0) and out of the stock market (1), with 1 indicating you can rotate into the cash equivalent.

ConditionBuy = bullmarket;

StaticVarSet("RotateCashEquiv", IIf(ConditionBuy, 0, 1));

The rest of the code is a mid level CBT. I've also added a ParamToggle to turn it on and off to see what the effect of using the Cash Equivalent is near the top of my code in Phase 1...

UseCashEquiv = ParamToggle("Use Cash Equivalent", "No|Yes", 0);

CBT follows... it's just a simple implementation that checks if you want to be out of stocks and then if you have zero open positions. This can be modified of course to suit your needs in terms of scaling in to the cash equivalent as positions drop out (or cash on hand increases - i.e. CashOnHand = bo.Cash; )

if(UseCashEquiv)
{
// CBT to check for open equity and no positions and rotate into TLT

SetCustomBacktestProc("");
if (Status("action") == actionPortfolio) 
{
    bo = GetBacktesterObject();	//  Get backtester object
    bo.PreProcess();	//  Do pre-processing (always required)
    
    
    for (i = 0; i < BarCount; i++)	//  Loop through all bars
    {
        
       
       CashOnHand = bo.Cash;
        
                 
         
			openpos = bo.GetOpenPosQty();
			Rotate = StaticVarGet("RotateCashEquiv");
			
			// if(openpos == 0 AND CashOnHand > 10000 AND Rotate == 1)
			// if(Rotate[i] == 1)
			if(Rotate[i] == 1 AND openpos[i] == 0)
			// if(openpos[i] <= 5)
			{
			TLTEntryPrice = Foreign("TLT", "C");
			bo.EnterTrade(i, "TLT", 1, TLTEntryPrice[i], CashOnHand[i]);
			}
			
			if(Rotate[i] == 0)
			{
			TLTExitPrice = Foreign("TLT", "C");
			bo.ExitTrade(i, "TLT", TLTExitPrice[i], 1);
			}
		   
		  /*      
          // CBT Low-Level
             bo.HandleStops(i);
		     bo.UpdateStats(i, 1);
		     bo.UpdateStats(i, 2);
		  */
         // }
    
        // CBT Mid-Level
         bo.ProcessTradeSignals( i );	//  Process trades at bar (always required) - Mid-level only
   	  
    }  
	bo.PostProcess();	//  Do post-processing (always required)
    
}

}

Hope that helps. It's just a very basic script as I start on my learning journey how to utilize the CBT. Regards.

The call to CheckEntrySignalsAndFindOutHowMuchCashYouNeed() just cracks me up. It proves Tomasz is not only a brilliant programmer but a comedian to boot! :smiley:

1 Like