CBT independent trade specific entry/exit pairs

I want to set up entry logic that is directly associated with it's associated exit logic. I have used '-nnn' in place of 'PositionScore". Is 'Position Score' used properly or is there a better means?

Day One >> Enter MSFT-200, Enter MSFT-300
Day Two >> Exit MSFT-300, Enter MSFT-500
Day Three >> Exit MSFT-200, Exit MSFT-500

Any mistakes or suggestions about syntax or style, like a Case statement or other more direct or efficient execution is appreciated.

Questions-

  • Is the 'AND watchlist' is needed for the exit logic?
  • Once implemented, does CBT do all Amibroker processing? In other words, the chart uses CBT logic as well as back testing, optimization, etc.

Thank you any and all.

_N( Title = StrFormat( "{{NAME}} - {{INTERVAL}}  {{DATE}} Close> $%.2f,", C ) );
P = ParamField( "Price field", 3 );
Plot( P, "Price", ParamColor( "Color", colorDefault ), styleBar | ParamStyle( "Style" ) | GetPriceStyle() );
_SECTION_END();

_SECTION_BEGIN( "M4" );
M4_L_WL = InWatchListName("M4_L_30");
M4_S_WL = InWatchListName("M4_S_30");
prcM4 = ParamField( "Price field", 3 );
fM4 = Param( "M4 MA Period", 5, 1, 20, 1 ); 
fstM4 = MA( prcM4, fM4 );
Buy_M4 = prcM4 <= fstM4 AND M4_L_WL;
Short_M4 = prcM4 >= fstM4 AND M4_S_WL;
barM4_L = Param("M4 Long Exit Bars", 3, 0, 10, 1);
barM4_S = Param("M4 Short Exit Bars", 4, 0, 10, 1);
_SECTION_END();

_SECTION_BEGIN( "MV" );
MV_L_WL = InWatchListName("MV_L_30");
MV_S_WL = InWatchListName("MV_S_30");
prcMV = ParamField( "Price field", 3 );
fMV = Param( "MV MA Period", 5, 1, 20, 1 ); 
fstMV = MA( prcMV, fMV );
Buy_MV = prcMV <= fstMV AND MV_L_WL;
Short_MV = prcMV >= fstMV AND MV_S_WL;
barMV_L = Param("MV Long Exit Bars", 5, 0, 10, 1);
barMV_S = Param("MV Short Exit Bars", 6, 0, 10, 1);
_SECTION_END();

_SECTION_BEGIN( "Trade Logic" ); //%//%//%//%//%//%//%/%//%//%//%//%//%//%//%//%///%//%
//	Each symbol is processed by trade specific logic.
Buy = Buy_M4 OR Buy_MV;
Short = Short_M4 OR Short_MV;
PositionScore = IIf( Buy_M4, 200,
                IIf( Buy_MV, 300,
                IIf( Short_M4, 500,
                IIf( Short_MV, 600, 0 ))));
Sell = Cover = 0;
BuyPrice = SellPrice = ShortPrice = CoverPrice = P;
maxpos = Param("Max Open Positions", 1000, 0, 10000, 1);
SetOption( "MaxOpenPositions", maxpos );
SetOption( "InitialEquity", 10000000 );
SetPositionSize( 1, spsPercentOfEquity );
SetTradeDelays( 0, 0, 0, 0 );
SetOption("UseCustomBacktestProc", 1);

if( Status("action") == actionPortfolio )
{
    bo = GetBacktesterObject();
    
    if( !IsNull(bo) )
    {
        bo.PreProcess();
        exitCount = 0;
        for( bar = 1; bar < BarCount; bar++ )
        {
            bo.UpdateStats( bar, 0 );
            bo.HandleStops( bar );
            bo.ProcessTradeSignals( bar );
            for( pos = bo.GetFirstOpenPos(); pos; pos = bo.GetNextOpenPos() )
            {
                signalType = pos.Score;
                exitPrice = pos.GetPrice( bar, "C" );
                BarsInTrade = pos.BarsInTrade;
                if( signalType == 200 )
                {
                    if( BarsInTrade >= barM4_L )
                    {
                        bo.ExitTrade( bar, pos.Handle, exitPrice, "M4_L Time" );
                        exitCount++;
                    }
                }
                else if( signalType == 300 )
                {
                    if( BarsInTrade >= barMV_L )
                    {
                        bo.ExitTrade( bar, pos.Handle, exitPrice, "MV_L Time" );
                        exitCount++;
                    }
                }
                else if( signalType == 500 )
                {
                    if( BarsInTrade >= barM4_S )
                    {
                        bo.ExitTrade( bar, pos.Handle, exitPrice, "M4_S Time" );
                        exitCount++;
                    }
                }
                else if( signalType == 600 )
                {
                    if( BarsInTrade >= barMV_S )
                    {
                        bo.ExitTrade( bar, pos.Handle, exitPrice, "MV_S Time" );
                        exitCount++;
                    }
                }
            }
            bo.UpdateStats( bar, 2 );
        }
        bo.PostProcess();
    }
}
_SECTION_END();

I just realized I am using a simple static time to exit in my example code. I need to know how to exit using a moving average or other calculated data. Basically how to access any calculated field used to determine an Exit. Thanks!!

I misworded my request. If this CBT code is to run in real time, market data is always changing. How can CBT access the real time data to calculate exit signal criteria from what current data IB can supply, not only for charting but trading.

Thanks

A CBT replaces the default AmiBroker behavior during the second phase of a backtest. It does not affect charting, nor analyses that only have one phase, like scans and explorations.

What you're trying to achieve is possible, but it's not a trivial exercise. The basic steps go something like this:

  1. Generate entry signals in Phase 1 as usual.

  2. Make the entry type available to Phase 2, either via the PositionScore as you have done or via a symbol-specific static variable.

  3. During Phase 1, generate exit signal arrays for each entry type, but do not use these to set the Sell/Cover arrays. Instead store them in static variables, one per entry type per symbol.

  4. During Phase 2, process all the open positions on each bar. Determine the entry type, retrive the appropriate exit signal array, and exit the position if needed.

That's close to the code you provided, but there are a few things wrong in your example. First, the "reason" parameter for bo.ExitTrade is usually a number. I have never tried to use a string like you did in your code.

Second, you can't use a standard for loop with bo.GetFirstOpenPos and bo.GetNextOpenPos. The loop will terminate as soon as one trade is closed. Basically, you need to retrieve the next trade before (possibly) closing the current one.

Thanks, I just now understood the two pass approach more completely. It is an elegant but complex solutions to portfolio testing and processing. I have been at this for hours. I will work on this tomorrow. I believe I understand what you propose.

This code is developed around low-level CBT. Thee questions come to mind but anything you may see that seems amiss, please comment.

  1. Is the GetFirstSignal/GetNextSignal price map pattern the correct way to source entry prices in low-level CBT, or is there a documented alternative?
  2. Is there a cleaner way to initialize EntryBar/ExitBar arrays without the pre-backtest watchlist loop?
  3. Does anyone have experience with bo.UpdateStats( bar, 2 ) and can confirm that 1 is the source of Profit Table Error 61 in pure low-level CBT?

_SECTION_BEGIN( "PARAMETERS" );
_N( Title = StrFormat( "{{NAME}} - {{INTERVAL}} {{DATE}} Close> $%.2f", C ) );

P = ParamField( "Price field", 3 );
Plot( P, "Price", ParamColor( "Color", colorDefault ), styleBar | ParamStyle( "Style" ) | GetPriceStyle() );

barBB_L = Param( "BB Long Exit Bars", 4, 0, 10, 1 );
barBB_S = Param( "BB Short Exit Bars", 4, 0, 10, 1 );
fBB = Param( "BB MA Period", 5, 1, 20, 1 );

StaticVarSet( "_barBB_L", barBB_L );
StaticVarSet( "_barBB_S", barBB_S );
StaticVarSet( "_fBB", fBB );

_SECTION_END();

_SECTION_BEGIN( "BB SIGNALS" );

fst = MA( P, fBB );

Buy_BB = Cross( P, fst );
Shrt_BB = Cross( fst, P );

BB_L_Exit = Cross( fst, P );
BB_S_Exit = Cross( P, fst );

StaticVarSet( Name() + "_BB_L_Buy", Buy_BB );
StaticVarSet( Name() + "_BB_S_Short", Shrt_BB );
StaticVarSet( Name() + "_BB_L_Exit", BB_L_Exit );
StaticVarSet( Name() + "_BB_S_Exit", BB_S_Exit );

Buy = Buy_BB;
Short = Shrt_BB;
Sell = 0;
Cover = 0;

BuyPrice = P;
ShortPrice = P;
SellPrice = P;
CoverPrice = P;

_SECTION_END();

_SECTION_BEGIN( "Custom Backtest" );

SetBacktestMode( backtestRegularRawMulti );
SetOption( "MaxOpenPositions", 1000 );
SetOption( "InitialEquity", 10000000 );
SetPositionSize( 1, spsPercentOfEquity );
SetTradeDelays( 0, 0, 0, 0 );
SetOption( "UseCustomBacktestProc", 1 );

if( Status( "action" ) == actionPortfolio )
{
bo = GetBacktesterObject();

if( !IsNull( bo ) )
{
    bo.PreProcess();

    barBB_L = StaticVarGet( "_barBB_L" );
    barBB_S = StaticVarGet( "_barBB_S" );

    for( bar = 1; bar < BarCount; bar++ )
    {
        bo.HandleStops( bar );

        for( pos = bo.GetFirstOpenPos(); pos; pos = bo.GetNextOpenPos() )
        {
            signalType  = abs( pos.Score );
            BarsInTrade = pos.BarsInTrade;
            exitPrice   = pos.GetPrice( bar, "C" );
            shouldExit  = 0;
            exitReason  = "";
            exitBarKey  = "";

            if( signalType == 100 )
            {
                exitArr = Nz( StaticVarGet( pos.Symbol + "_BB_L_Exit" ) );
                if( exitArr[bar] OR BarsInTrade >= barBB_L )
                {
                    shouldExit = 1;
                    if( exitArr[bar] ) exitReason = "BB_L Price";
                    else               exitReason = "BB_L Time";
                    exitBarKey = pos.Symbol + "_BB_L_ExitBar";
                }
            }
            else if( signalType == 400 )
            {
                exitArr = Nz( StaticVarGet( pos.Symbol + "_BB_S_Exit" ) );
                if( exitArr[bar] OR BarsInTrade >= barBB_S )
                {
                    shouldExit = 1;
                    if( exitArr[bar] ) exitReason = "BB_S Price";
                    else               exitReason = "BB_S Time";
                    exitBarKey = pos.Symbol + "_BB_S_ExitBar";
                }
            }

            if( shouldExit )
            {
                if( exitBarKey != "" )
                {
                    ebArr = Nz( StaticVarGet( exitBarKey ) );
                    ebArr[bar] = 1;
                    StaticVarSet( exitBarKey, ebArr );
                }
                bo.ExitTrade( bar, pos.Handle, exitPrice, exitReason );
            }
        }

        for( sig = bo.GetFirstSignal( bar ); sig; sig = bo.GetNextSignal( bar ) )
        {
            if( sig.IsEntry() )
            {
                sym = sig.Symbol;

                entryArr = Nz( StaticVarGet( sym + "_BB_L_Buy"   ) );
                shrtArr  = Nz( StaticVarGet( sym + "_BB_S_Short" ) );
                bLong    = entryArr[bar];
                bShort   = shrtArr[bar];

                if( bLong OR bShort )
                {
                    eScore = IIf( bLong, 100, 400 );
                    ePrice = sig.Price;

                    if( ePrice > 0 )
                    {
                        bo.EnterTrade( bar, sym, bLong, ePrice, -1, eScore );

                        if( bLong )
                            entryBarKey = sym + "_BB_L_EntryBar";
                        else
                            entryBarKey = sym + "_BB_S_EntryBar";

                        ebArr = Nz( StaticVarGet( entryBarKey ) );
                        ebArr[bar] = 1;
                        StaticVarSet( entryBarKey, ebArr );
                    }
                }
            }
        }

        bo.UpdateStats( bar, 2 );
    }

    bo.PostProcess();
}

}

_SECTION_END();

_SECTION_BEGIN( "Signal Display" );

colorBB_entry = ParamColor( "BB Entry Color", colorWhite );
colorBB_exit = ParamColor( "BB Exit Color", colorRed );

BB_L_EntryBar = Nz( StaticVarGet( Name() + "_BB_L_EntryBar" ) );
BB_S_EntryBar = Nz( StaticVarGet( Name() + "_BB_S_EntryBar" ) );
BB_L_ExitBar = Nz( StaticVarGet( Name() + "_BB_L_ExitBar" ) );
BB_S_ExitBar = Nz( StaticVarGet( Name() + "_BB_S_ExitBar" ) );

increment = Param( "Increment", 15, 1, 200, 1 );
yOff = Param( "y Offset", 15, 1, 200, 1 );

for( i = 0; i < BarCount; i++ )
{
stackAbove = yOff;
stackBelow = yOff;

if( BB_L_EntryBar[i] )
{
    PlotText( "B", i, High[i], colorBB_entry, colorDefault,  stackAbove );
    stackAbove += increment;
}
if( BB_L_ExitBar[i] )
{
    PlotText( "b", i, High[i], colorBB_exit,  colorDefault,  stackAbove );
    stackAbove += increment;
}
if( BB_S_EntryBar[i] )
{
    PlotText( "B", i, Low[i],  colorBB_entry, colorDefault, -stackBelow );
    stackBelow += increment;
}
if( BB_S_ExitBar[i] )
{
    PlotText( "b", i, Low[i],  colorBB_exit,  colorDefault, -stackBelow );
    stackBelow += increment;
}

}

_SECTION_END();

When posting the formula, please make sure that you use Code Tags (using </> code button) as explained here: How to use this site.

Using code button

Code tags are required so formulas can be properly displayed and copied without errors.

I do not find code tags on the editor as shown in your response. I now see the code tags are in the + icon.

_SECTION_BEGIN( "PARAMETERS" );
_N( Title = StrFormat( "{{NAME}} - {{INTERVAL}}  {{DATE}} Close> $%.2f", C ) );

P = ParamField( "Price field", 3 );
Plot( P, "Price", ParamColor( "Color", colorDefault ), styleBar | ParamStyle( "Style" ) | GetPriceStyle() );

barBB_L = Param( "BB Long  Exit Bars", 4, 0, 10, 1 );
barBB_S = Param( "BB Short Exit Bars", 4, 0, 10, 1 );
fBB     = Param( "BB MA Period",       5, 1, 20, 1 );

StaticVarSet( "_barBB_L", barBB_L );
StaticVarSet( "_barBB_S", barBB_S );
StaticVarSet( "_fBB",     fBB );

_SECTION_END();


_SECTION_BEGIN( "BB SIGNALS" );

fst = MA( P, fBB );

Buy_BB  = Cross( P, fst );
Shrt_BB = Cross( fst, P );

BB_L_Exit = Cross( fst, P );
BB_S_Exit = Cross( P, fst );

StaticVarSet( Name() + "_BB_L_Buy",   Buy_BB );
StaticVarSet( Name() + "_BB_S_Short", Shrt_BB );
StaticVarSet( Name() + "_BB_L_Exit",  BB_L_Exit );
StaticVarSet( Name() + "_BB_S_Exit",  BB_S_Exit );

Buy   = Buy_BB;
Short = Shrt_BB;
Sell  = 0;
Cover = 0;

BuyPrice   = P;
ShortPrice = P;
SellPrice  = P;
CoverPrice = P;

_SECTION_END();


_SECTION_BEGIN( "Custom Backtest" );

SetBacktestMode( backtestRegularRawMulti );
SetOption( "MaxOpenPositions",      1000 );
SetOption( "InitialEquity",   10000000 );
SetPositionSize( 1, spsPercentOfEquity );
SetTradeDelays( 0, 0, 0, 0 );
SetOption( "UseCustomBacktestProc", 1 );

if( Status( "action" ) == actionPortfolio )
{
    bo = GetBacktesterObject();

    if( !IsNull( bo ) )
    {
        bo.PreProcess();

        barBB_L = StaticVarGet( "_barBB_L" );
        barBB_S = StaticVarGet( "_barBB_S" );

        for( bar = 1; bar < BarCount; bar++ )
        {
            bo.HandleStops( bar );

            for( pos = bo.GetFirstOpenPos(); pos; pos = bo.GetNextOpenPos() )
            {
                signalType  = abs( pos.Score );
                BarsInTrade = pos.BarsInTrade;
                exitPrice   = pos.GetPrice( bar, "C" );
                shouldExit  = 0;
                exitReason  = "";
                exitBarKey  = "";

                if( signalType == 100 )
                {
                    exitArr = Nz( StaticVarGet( pos.Symbol + "_BB_L_Exit" ) );

                    if( exitArr[bar] OR BarsInTrade >= barBB_L )
                    {
                        shouldExit = 1;

                        if( exitArr[bar] ) exitReason = "BB_L Price";
                        else               exitReason = "BB_L Time";

                        exitBarKey = pos.Symbol + "_BB_L_ExitBar";
                    }
                }
                else if( signalType == 400 )
                {
                    exitArr = Nz( StaticVarGet( pos.Symbol + "_BB_S_Exit" ) );

                    if( exitArr[bar] OR BarsInTrade >= barBB_S )
                    {
                        shouldExit = 1;

                        if( exitArr[bar] ) exitReason = "BB_S Price";
                        else               exitReason = "BB_S Time";

                        exitBarKey = pos.Symbol + "_BB_S_ExitBar";
                    }
                }

                if( shouldExit )
                {
                    if( exitBarKey != "" )
                    {
                        ebArr = Nz( StaticVarGet( exitBarKey ) );
                        ebArr[bar] = 1;
                        StaticVarSet( exitBarKey, ebArr );
                    }

                    bo.ExitTrade( bar, pos.Handle, exitPrice, exitReason );
                }
            }

            for( sig = bo.GetFirstSignal( bar ); sig; sig = bo.GetNextSignal( bar ) )
            {
                if( sig.IsEntry() )
                {
                    sym = sig.Symbol;

                    entryArr = Nz( StaticVarGet( sym + "_BB_L_Buy" ) );
                    shrtArr  = Nz( StaticVarGet( sym + "_BB_S_Short" ) );
                    bLong    = entryArr[bar];
                    bShort   = shrtArr[bar];

                    if( bLong OR bShort )
                    {
                        eScore = IIf( bLong, 100, 400 );
                        ePrice = sig.Price;

                        if( ePrice > 0 )
                        {
                            bo.EnterTrade( bar, sym, bLong, ePrice, -1, eScore );

                            if( bLong )
                                entryBarKey = sym + "_BB_L_EntryBar";
                            else
                                entryBarKey = sym + "_BB_S_EntryBar";

                            ebArr = Nz( StaticVarGet( entryBarKey ) );
                            ebArr[bar] = 1;
                            StaticVarSet( entryBarKey, ebArr );
                        }
                    }
                }
            }

            bo.UpdateStats( bar, 2 );
        }

        bo.PostProcess();
    }
}

_SECTION_END();


_SECTION_BEGIN( "Signal Display" );

colorBB_entry = ParamColor( "BB Entry Color", colorWhite );
colorBB_exit  = ParamColor( "BB Exit Color",  colorRed );

BB_L_EntryBar = Nz( StaticVarGet( Name() + "_BB_L_EntryBar" ) );
BB_S_EntryBar = Nz( StaticVarGet( Name() + "_BB_S_EntryBar" ) );
BB_L_ExitBar  = Nz( StaticVarGet( Name() + "_BB_L_ExitBar" ) );
BB_S_ExitBar  = Nz( StaticVarGet( Name() + "_BB_S_ExitBar" ) );

increment = Param( "Increment", 15, 1, 200, 1 );
yOff      = Param( "y Offset",  15, 1, 200, 1 );

for( i = 0; i < BarCount; i++ )
{
    stackAbove = yOff;
    stackBelow = yOff;

    if( BB_L_EntryBar[i] )
    {
        PlotText( "B", i, High[i], colorBB_entry, colorDefault,  stackAbove );
        stackAbove += increment;
    }

    if( BB_L_ExitBar[i] )
    {
        PlotText( "b", i, High[i], colorBB_exit,  colorDefault,  stackAbove );
        stackAbove += increment;
    }

    if( BB_S_EntryBar[i] )
    {
        PlotText( "B", i, Low[i],  colorBB_entry, colorDefault, -stackBelow );
        stackBelow += increment;
    }

    if( BB_S_ExitBar[i] )
    {
        PlotText( "b", i, Low[i],  colorBB_exit,  colorDefault, -stackBelow );
        stackBelow += increment;
    }
}

_SECTION_END();

  1. Using AmiBroker's built-in Buy/Sell/Short/Cover signal arrays and their associated XxxPrice arrays is definitely the most efficient way to get signal and price data from Phase 1 to Phase 2. Unless there's a compelling reason to do something else, you should stick with this.

  2. What do you mean by "pre-backtest watchlist loop"? If you are referring to Phase 1 of the backtest, then that is the correct way to do your per-symbol processing including generating signals and prices. Is there a particular concern that you have?

  3. As stated in the documentation, if you're writing a low-level CBT then you should always call bo.UpdateStats(bar,2) after you've completed all your other processing for the bar. This gives AmiBroker the opportunity to do its internal housekeeping.

The only time I've seen errors while generating Report Charts is when there are no trades generated by the backtest or you've made a programming error. If you are experiencing an error that you can't figure out, you can post the steps to reproduce it here.

I realize that this code is just an example, or perhaps an exercise to help you become more familiar with writing a CBT. However, it does seem like you're doing a lot of extra work just to keep track of things that AmiBroker already handles like time-based stops and long trades vs. short trades.

Thank you mradtke for the response. This is an exercise for a tradable system. It is being done in low-level CBT to ensure individual trade processing.

1. I understand .handle property is the means to identify each trade independently of any other in low-level CBT. I have several different entry and exit signals which may or may not occur on the same bar. The question is, is GetFirstSignal/GetNextSignal the way to access price data for these trades.

2. To clarify the watchlist question with specific code. Entry and exit decisions are per-instrument, driven by watchlists. CBT writes EntryBar and ExitBar arrays as StaticVars at bo.EnterTrade() and bo.ExitTrade() so a Signal Display section can place markers on the chart showing where trades were actually taken.
These StaticVar arrays must be initialized to zero before the bar loop runs, otherwise the first backtest run reads uninitialized keys (Null). The initialization currently iterates the active watchlist before the bar loop:

// Inside if( Status("action") == actionPortfolio ), before bar loop:
listName   = StaticVarGetText( "_WL_BB_L" );
allSymbols = CategoryGetSymbols( categoryWatchlist, 
             CategoryFind( listName, categoryWatchlist ) );
zeroArr = 0 * Close;
idx = 0;
sym = StrExtract( allSymbols, idx );
while( sym != "" )
{
    StaticVarSet( sym + "_BB_L_ExitBar",  zeroArr );
    StaticVarSet( sym + "_BB_L_EntryBar", zeroArr );
    // ... repeat for each signal type
    idx++;
    sym = StrExtract( allSymbols, idx );
}

The watchlist name is passed from Phase 1 via StaticVarSetText().

This works correctly. The question is whether this is the cleanest approach for initializing per-symbol StaticVar arrays in low-level CBT, or whether there is a more idiomatic pattern for this. Specifically:

  1. Is CategoryGetSymbols / StrExtract iteration the standard way to loop through a watchlist inside CBT Phase 2?

  2. Is there a lighter-weight alternative for zeroing StaticVar arrays that will be written during the bar loop — or is explicit pre-initialization the correct practice?

The system uses watchlists to control which instruments are active for each signal type. The watchlist drives instrument selection and is essential infrastructure.

3. I discovered that TimeInsideBar = 1 creates this error 61 in the Profit Table code, while TimeInsideBar = 2 does not and processes the trades properly.

Thanks again!

Not sure what you're trying to ask with your first question. With the built-in arrays previously mentioned, you can only have one entry signal and/or one exit signal per symbol per bar. It's hard to offer much advice here without knowing more about what you're trying to build.

Regarding the second question, you should always do as much work as possible in Phase 1. Why? Because Phase 1 is multi-threaded while Phase 2 is single threaded AND you have access to a longer history of data for calculating indicators and such. In Phase 1 you could do something like this:

if (InWatchlistName(_WL_BB_L))
{
   StaticVarSet(Name() + "_BB_L_ExitBar",  0);
   StaticVarSet(Name() + "_BB_L_EntryBar", 0);
}

// Repeat for other watchlists of interest

Note that you do not need to create an array of zero values. AmiBroker converts scalars to arrays when there is a need to do so.

For question 3, you should be able to call bo.UpdateStats(bar,1) 0-N times for each bar. You MUST call bo.UpdateStats(bar,2) exactly once per bar.