Backtester bo.Cash and bo.Equity interpretation

When running a backtest of a simple "system" I get cash and equity which doesn't make sense to me. I would assume that cash is what is left in the account subtracting the value of open positions. Running the backtest for the following "system":

RoundLotSize = 1;

InitialEQ = 100000;
MaxPositions = 1;
PercentEquityPositionSize = 100;

SetBacktestMode(backtestRegular);

SetTradeDelays(1, 1, 1, 1);
SetOption("allowsamebarexit", False);
SetOption("AllowPositionShrinking", True);
SetOption("initialequity", InitialEQ);
SetOption("MaxOpenPositions", MaxPositions);
//SetOption("CommissionMode", 0); //SET IN BACKTESTER YOUR APLICABLE COMISSIONS
SetOption("MinShares", 1);
SetPositionSize(PercentEquityPositionSize, spsPercentOfEquity); //COMMENT OUT IF YOU WANT COMPOUNDING
SetOption("AccountMargin", 1); //CHANGE IF YOU WANT TO USE MARGIN
SetOption("HoldMinBars", 1);

SetFormulaName("TestEquityAndCash");

Buy = Sell = Short = Cover = 0;
BuyPrice = SellPrice = ShortPrice = CoverPrice = O;

//ThisIsLastBar = BarIndex() == LastValue( BarIndex() );

// Divergence
bull1 = C > MA(C, 200);
cum1 = Ref(RSI(2), -1) + RSI(2);

// Rules
Buy =  cum1 < 10;// and bull1 AND Open > 5 AND MA(V, 100) > 250000;
Sell = cum1 > 50; // OR ThisIsLastbar;
 
BuyPrice = Open;
SellPrice = Open;

SetCustomBacktestProc( "" );

if ( Status( "action" ) == actionPortfolio ) {
	resultsFolder = "C:\\tmp\\";
	positionsCSVFileName = "Positions.csv";
	fh = fopen(resultsFolder + positionsCSVFileName, "w");
	
	if (fh) {
		dt = DateTime();
		bo = GetBacktesterObject();    //  Get backtester object
		bo.PreProcess();    //  Do pre-processing (always required)
		
		for ( i = 0; i < BarCount; i++ ) { //  Loop through all bars
			bo.ProcessTradeSignals( i );  //  Process trades at bar (always required)
			
			fputs(NumToStr(dt[i], formatDateTime) + "," + "CASHPOSITION" + "," + StrFormat("%1.5f", bo.Cash) + "\n", fh);
			fputs(NumToStr(dt[i], formatDateTime) + "," + "EQUITY" + "," + StrFormat("%1.5f", bo.Equity) + "\n", fh);
					
			for ( openpos = bo.GetFirstOpenPos(); openpos; openpos = bo.GetNextOpenPos() ) {
				// openpos variable now holds Trade object - you can query it
				if (IsTrue(openpos.IsOpen)) {
					multiplier = 1;
					if (IsTrue(openpos.IsLong)) {
						multiplier = 1;
					} else {
						multiplier = -1;
					}
					
					fputs(
						NumToStr(dt[i], formatDateTime) + "," + 
						openpos.Symbol + "," + 
						StrFormat("%1.5f", multiplier*openpos.GetPositionValue()) + 
						"\n"
						, fh);
				}
			}
		}    //  End of for loop over bars

		bo.PostProcess();    //  Do post-processing (always required)
		
		fclose(fh);
    }
}

eqname = "~~~EQUITY";
if( Name() == eqname ) {
	eq = Foreign( eqname, "Close", FixUp=1 );
	Cash = Foreign( eqname, "Low", FixUp=1 );
	ret = Nz((eq - Ref(eq, -1))/Ref(eq, -1));
	
	resultsFolder = "C:\\tmp\\";
	returnEquityCashCSVFileName = "ReturnEquityCash.csv";
	
	includeHeader = True;
	
	fh = fopen(resultsFolder + returnEquityCashCSVFileName, "w");
	if (fh) {
		y = Year(); 
		m = Month(); 
		d = Day(); 
		
		if (includeHeader) {
			fputs("Date,Return,Equity,Cash\n", fh);
		}
		
		for( i = 0; i < BarCount; i++ ) 
		{
			if (eq[i] > -100000000) {
				fputs(StrFormat("%02.0f-%02.0f-%02.0f,", y[ i ], m[ i ], d[ i ] ) + StrFormat("%02.8f,",  ret[i]) + StrFormat("%02.2f,",  eq[i]) + StrFormat("%02.2f\n",  Cash[i]), fh);
			}
		}
		fclose(fh);
	}
}

I get the following "result" for "Positions.csv"

22/02/2023,CASHPOSITION,100000.00000
22/02/2023,EQUITY,100000.00000
23/02/2023,CASHPOSITION,99000.10156
23/02/2023,EQUITY,99773.40625
23/02/2023,SPY,99763.09375
24/02/2023,CASHPOSITION,99000.10156
24/02/2023,EQUITY,98707.68750
24/02/2023,SPY,98697.37500
27/02/2023,CASHPOSITION,99576.70312
27/02/2023,EQUITY,99576.70312
28/02/2023,CASHPOSITION,99576.70312
28/02/2023,EQUITY,99576.70312

and for "ReturnEquityCash.csv"

Date,Return,Equity,Cash
2023-02-22,0.00000000,100000.00,100000.00
2023-02-23,-0.00226594,99773.41,99000.10
2023-02-24,-0.01068139,98707.69,99000.10
2023-02-27,0.00880393,99576.70,99576.70
2023-02-28,0.00000000,99576.70,99576.70

To me this doesn't seem right. Cash should be very small.

What am I not understanding here?

Thanks.

Without margin, Equity is simply Cash PLUS value of all currently open positions.

Your code is WRONG. "AccountMargin" should be set to 100, not to 1. With your current code you buy on 100x margin and use 0.01x of cash required to open position (the rest is borrowed)

Please DO READ THE MANUAL:

https://www.amibroker.com/guide/afl/setoption.html

It clearly says:

AccountMargin (in old versios it was 'MarginRequirement') - account margin requirement (as in settings), 100 = no margin

Thanks. This was helpful. I had a hunch it is a setting somewhere but could not have spot it myself.

One more question on interpretation of cash.

Is there a way to use margin only after cash is 0?

As I understand now, if I use SetOption("AccountMargin", 100); then this means that I need to provide 100% funds to enter/finance the trade. If there are not sufficient funds, trade will not be entered. If I want that the trade is entered with borrowed funds I need to set SetOption("AccountMargin", 50); to something lower than 100. In the case of setting it to 50 this means that 50 percent of every(!) position is financed by "margin loan", 50% by own provided funds regardless if there is ample amount of own funds available.

Which then brings me to bo.cash or L of "~~~EQUITY" symbol. In this case cash is a bit unintuitive for me. I would assume that trades would be entered using own funds until cash hits 0 and only from there on with borrowed funds depending on the level of leverage you want to set/use.

My question is how to use Amibroker/AFL (to write the code) that would treat cash in a way that borrowing would only happen after own funds are used (cash=0)? Are there any code samples? I need this to be able to export "cash" in a way that would represent using borrowed funds after own funds are depleted.

Thanks.

Actually it depends on broker. Various brokers worldwide handle situation differently.

From accounting point of view it is better/ clearer to do things like AmiBroker is doing
but only charge interest only when there is an 'excess' of funds invested above your actual cash levels.
It makes it a whole lot easier and clearer and simpler to explain to do the thing THE SAME WAY always, instead of suddenly switching calculation from one to another.
Also there is less variables to track and less errors to be made.

Thank you very much for your answer.

My question was actually aimed towards how I can write an AFL code in CBT that would calculate cash in a way that until cash is depleted (bo.cash == 0) there is no borrowing. I need to be able to export this newly calculated cash array.

My first silly attempt to calculate it is shown below (also attached). How to make it work?

SetCustomBacktestProc( "" );

if ( Status( "action" ) == actionPortfolio ) {
	strategyResultsFolder = "C:\\tmp\\";
	cashCSVFileName = "Cash.csv";
	
	fh = fopen(strategyResultsFolder + cashCSVFileName, "w");
	
	if (fh) {
		dt = DateTime();
		bo = GetBacktesterObject();    //  Get backtester object
		bo.PreProcess();    //  Do pre-processing (always required)
		
		fputs("Date,PositionName,Position\n", fh);
		cashArray = Null;
		for ( i = 0; i < BarCount; i++ ) { //  Loop through all bars
			bo.ProcessTradeSignals( i );  //  Process trades at bar (always required)
			
			if (i > 0) {
				cashArray[i] = cashArray[i - 1];
			} else {
				cashArray[i] = GetOption("InitialEquity");
			}
						
			for ( openpos = bo.GetFirstOpenPos(); openpos; openpos = bo.GetNextOpenPos() ) {
				// openpos variable now holds Trade object - you can query it
				if (IsTrue(openpos.IsOpen)) {
					multiplier = 1;
					if (IsTrue(openpos.IsLong)) {
						multiplier = 1;
					} else {
						multiplier = -1;
					}
					
					// TODO: This is of course not correct. Here is a lot of "accounting" nuances to take care of. Not only open trades contribute... How to make this correct?
					cashArray[i] = cashArray[i] - multiplier*openpos.GetPositionValue();
				}
			}
			
			fputs(NumToStr(dt[i], formatDateTime) + "," + "CASHPOSITION" + "," + StrFormat("%1.5f", bo.Cash) + "\n", fh);
			fputs(NumToStr(dt[i], formatDateTime) + "," + "CASHPOSITION_ALT" + "," + StrFormat("%1.5f", cashArray[i]) + "\n", fh);
			fputs(NumToStr(dt[i], formatDateTime) + "," + "EQUITY" + "," + StrFormat("%1.5f", bo.Equity) + "\n", fh);
		}    //  End of for loop over bars

		bo.PostProcess();    //  Do post-processing (always required)
		
		fclose(fh);
    }
}

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