// cost-sensitivity-backtest.afl
// Part 30 - Unrealistic Fills, Costs and Liquidity
//
// PURPOSE
//   A deliberately ordinary long-only breakout system whose only interesting
//   feature is that every execution assumption is a named parameter. Run it
//   once with the costs at zero and once with them at a pessimistic setting,
//   and read the difference. The difference is the part of the result that was
//   never yours.
//
// ============================ ASSUMPTIONS =============================
//   Universe        whatever the Analysis window is applied to, with whatever
//                   survivorship problems that list carries. State it in your
//                   research log; the formula cannot know it.
//   Periodicity     Daily.
//   Decision bar    the bar on which the breakout condition becomes true,
//                   evaluated on that bar's CLOSE.
//   Fill            the NEXT bar's Open, because SetTradeDelays is 1 on all
//                   four signals and BuyPrice/SellPrice are built from Open.
//   Slippage        SlippagePct is added to every entry price and subtracted
//                   from every exit price, in percent. This is a stand-in for
//                   half-spread plus market impact. It is a guess. Make it a
//                   pessimistic guess.
//   Commission      percent of trade value, both legs, via CommissionMode 1.
//   Liquidity       a symbol is only tradable on a signal bar if its 50-bar
//                   average turnover, measured up to and including that bar,
//                   clears MinTurnover, and its close clears MinPrice.
//   Participation   NOT modelled here. Set "Limit trade size as % of entry bar
//                   volume" on the Settings -> Portfolio tab, or use the
//                   companion formula participation-capped-sizing.afl.
//   Stops           max-loss stop with ExitAtStop = 2: the High-Low range is
//                   checked, and the exit happens on the NEXT bar at the
//                   regular trade price. That models "the stop was breached, I
//                   found out, I got out at the next open" - not "I was filled
//                   at exactly my stop price". Confirm the actual exit prices
//                   in the trade list before you trust them.
//   Interest        set the annual interest rate to 0 in Settings unless you
//                   really do intend to credit idle cash.
// ======================================================================
//
// This formula produces a simulation of a rule over history. It is not a
// forecast, and no result it produces is an expectation of future returns.

SetOption( "InitialEquity", Param( "Initial equity", 100000, 10000, 1000000, 10000 ) );

BreakoutPeriod = Param( "Breakout lookback (bars)", 50,  10, 250, 5 );
ExitPeriod     = Param( "Exit lookback (bars)",     20,   5, 100, 5 );
TrendPeriod    = Param( "Trend filter MA (bars)",  200,  20, 300, 10 );
StopLossPct    = Param( "Max loss stop (%)",         8,   1,  30, 0.5 );
PosQty         = Param( "Max open positions",       10,   1,  50, 1 );

// --- the three assumptions the lesson is about -----------------------
CommissionPct  = Param( "Commission per trade (%)", 0.10, 0, 1.00, 0.01 );
SlippagePct    = Param( "Slippage per fill (%)",    0.15, 0, 2.00, 0.01 );
MinTurnover    = Param( "Min 50-bar turnover",  2000000, 0, 50000000, 250000 );
MinPrice       = Param( "Min close price",            5, 0, 100, 1 );

// ---------------------------------------------------------------------
// 1. Portfolio and cost settings, set from the formula so that the report
//    and the formula can never disagree about what was assumed.
// ---------------------------------------------------------------------
SetOption( "MaxOpenPositions", PosQty );
SetOption( "AllowPositionShrinking", True );
SetOption( "CommissionMode", 1 );          // 1 = percent of trade value
SetOption( "CommissionAmount", CommissionPct );
SetOption( "ActivateStopsImmediately", True );  // entries are on the open

PositionSize = -100 / PosQty;              // equal weight across open slots

// ---------------------------------------------------------------------
// 2. Execution model. Delays of 1 bar mean the backtester internally shifts
//    Buy/Sell/Short/Cover by one bar, so a signal decided on today's close is
//    acted on tomorrow. The price arrays are NOT shifted by the delay setting,
//    which is why they are written in terms of the fill bar's own Open.
// ---------------------------------------------------------------------
SetTradeDelays( 1, 1, 1, 1 );

BuyPrice   = Open * ( 1 + SlippagePct / 100 );
SellPrice  = Open * ( 1 - SlippagePct / 100 );
ShortPrice = Open * ( 1 - SlippagePct / 100 );
CoverPrice = Open * ( 1 + SlippagePct / 100 );

// AmiBroker clamps any assigned trade price into the bar's High-Low range.
// A slipped open that lands outside the bar therefore fills at High or Low
// rather than at the impossible price - which is the behaviour we want.

// ---------------------------------------------------------------------
// 3. The rule itself. Every input is known at the close of the decision bar:
//    the breakout level uses bars strictly BEFORE the current one, so a bar
//    cannot break out of a range that includes its own high.
// ---------------------------------------------------------------------
Turnover      = MA( Close * Volume, 50 );
Tradable      = Turnover >= MinTurnover AND Close >= MinPrice AND Volume > 0;

BreakoutLevel = Ref( HHV( High, BreakoutPeriod ), -1 );
ExitLevel     = Ref( LLV( Low,  ExitPeriod ),     -1 );
Trend         = Close > MA( Close, TrendPeriod );

Buy  = Close > BreakoutLevel AND Trend AND Tradable;
Sell = Close < ExitLevel;

// Rank competing entries by liquidity, so that when more symbols signal than
// there are slots, the more tradable ones are preferred. PositionScore is
// ranked on its ABSOLUTE value by default, so keep it strictly positive.
PositionScore = Turnover;

// ---------------------------------------------------------------------
// 4. Risk exit. ExitAtStop = 2 checks the High-Low range but executes on the
//    NEXT bar at the regular trade price, so a gap through the stop is paid
//    for at the gapped price rather than at the level you nominated.
// ---------------------------------------------------------------------
ApplyStop( stopTypeLoss, stopModePercent, StopLossPct, 2 );

// ---------------------------------------------------------------------
// 5. Chart view, so the same file can be inspected visually.
// ---------------------------------------------------------------------
_SECTION_BEGIN( "Cost sensitivity view" );
Plot( Close, "Close", colorDefault, styleCandle );
Plot( BreakoutLevel, "Breakout level", colorGreen, styleLine );
Plot( ExitLevel, "Exit level", colorRed, styleLine );
PlotShapes( Buy * shapeUpArrow, colorGreen, 0, Low );
PlotShapes( Sell * shapeDownArrow, colorRed, 0, High );
_SECTION_END();
