// project-portfolio-system.afl
// Part 28 - Project: A Realistic Portfolio Backtest
//
// The simple trend system from the Part 27 project, taken to portfolio level
// with every constraint and every cost written into the formula rather than
// left in a dialog box. Run it as a portfolio BACKTEST over a watch list.
//
// ============================ ASSUMPTIONS ==================================
// Read these before the code. If any one of them is wrong for your market, your
// broker or your account, the result below is a simulation of somebody else's
// trading, not of yours.
//
//   Universe      Whatever watch list you point the Analysis window at, exactly
//                 as it stands today. If it was built from a current index
//                 membership list, it contains survivorship bias and this test
//                 cannot remove it.
//   Period        Whatever Range you set. State it whenever you quote a result.
//   Bars          Daily, unadjusted for anything your data source did not
//                 already adjust. Splits and dividends are your data vendor's
//                 problem and they become yours.
//   Signal timing Rules are evaluated on the close of the signal bar. Nothing
//                 in the formula reads a bar that had not printed.
//   Fill price    The next bar's open, moved against us by SlippagePercent.
//   Commission    CommissionPercent of trade value, charged on entry and exit.
//   Financing     Interest on idle cash set to zero. No borrow costs, no
//                 dividends received, no currency effects.
//   Liquidity     A turnover floor keeps out names that could not absorb a
//                 position, and MaxPercentOfTurnover caps how much of a day's
//                 average turnover one position may represent. Neither is a
//                 substitute for measuring market impact.
//   Stops         Maximum-loss stop only, checked against the bar's High-Low
//                 range but EXITED ON THE NEXT BAR'S OPEN (ExitAtStop = 2). The
//                 backtest is therefore not awarded a fill at the stop price.
//   Sizing        Equal weight across PosQty slots, from current portfolio
//                 equity, whole shares only.
//   Selection     When signals outnumber slots, the most liquid candidate wins.
//                 That is a choice with a known side effect - see section 6.
// ===========================================================================

// --------------------------------------------------------- 1. the parameters
PosQty            = 10;        // maximum simultaneous positions
StartingEquity    = 100000;
SlippagePercent   = 0.05;      // per side, percent of price
CommissionPercent = 0.10;      // per trade, percent of trade value
MaxLossPercent    = 10;        // maximum-loss stop distance

RegimePeriod      = 200;       // long-term trend filter
TriggerPeriod     = 50;        // entry and exit average

MinTurnover       = 2000000;   // 50-bar average of Close * Volume
MinPriceLevel     = 2;         // ignore anything trading below this
MinPositionValue  = 1000;      // below this the trade is not worth entering

// ----------------------------------------------------------- 2. the account
SetOption( "InitialEquity",           StartingEquity );
SetOption( "MaxOpenPositions",        PosQty );
SetOption( "AllowPositionShrinking",  True );
SetOption( "MinPosValue",             MinPositionValue );
SetOption( "AccountMargin",           100 );    // 100 = fully funded, no margin
SetOption( "InterestRate",            0 );      // idle cash earns nothing here
SetOption( "CommissionMode",          1 );      // 1 = percent of trade
SetOption( "CommissionAmount",        CommissionPercent );
SetOption( "ActivateStopsImmediately", False );
SetOption( "ReverseSignalForcesExit", True );
SetOption( "AllowSameBarExit",        True );

RoundLotSize = 1;               // whole shares
SetBacktestMode( backtestRegular );

SetPositionSize( 100 / PosQty, spsPercentOfEquity );
SetTradeDelays( 1, 1, 1, 1 );

// ------------------------------------------------------------- 3. the fills
// Slippage belongs in the price, not in the commission: it is a worse price,
// not a fee. Remember that AmiBroker clamps any assigned price back inside the
// bar's High-Low range, so on a bar that opened at its high the extra half of
// the spread is silently given back to you.
BuyPrice   = Open * ( 1 + SlippagePercent / 100 );
SellPrice  = Open * ( 1 - SlippagePercent / 100 );
ShortPrice = Open * ( 1 - SlippagePercent / 100 );
CoverPrice = Open * ( 1 + SlippagePercent / 100 );

// ----------------------------------------------------------- 4. the universe
// Applied to the signal bar, so it uses only what was known before the order.
Turnover  = MA( Close * Volume, 50 );
Tradeable = Turnover >= MinTurnover
        AND Close >= MinPriceLevel
        AND Volume > 0;

// ------------------------------------------------------------- 5. the rules
// Regime: only take longs while the instrument is above its long-term average.
// Trigger: the close crossing above its medium-term average.
// Exit: the same average crossed the other way, or the stop, whichever first.
RegimeMa  = MA( Close, RegimePeriod );
TriggerMa = MA( Close, TriggerPeriod );

Regime = Close > RegimeMa;

Buy  = Cross( Close, TriggerMa ) AND Regime AND Tradeable;
Sell = Cross( TriggerMa, Close );

// The maximum-loss stop. ExitAtStop = 2 means: watch the whole bar, but leave at
// the NEXT bar's trade price. A gap through the level therefore costs what a gap
// costs, instead of being handed back to us at the level we nominated.
ApplyStop( stopTypeLoss, stopModePercent, MaxLossPercent, 2 );

// ------------------------------------------------------------- 6. the rank
// Liquidity as the tie-break: prefer the candidate we could most plausibly have
// traded. The side effect is that this systematically prefers the largest names
// in the universe, which is a real bias and must be stated whenever the result
// is quoted. Section 8 of the lesson suggests two alternatives worth testing.
PositionScore = Turnover;

// -------------------------------------------------- 7. the liquidity ceiling
// A position must not be a large share of what actually traded. This is a crude
// version of the constraint - it caps the value at entry against average
// turnover rather than modelling impact - but a crude version applied is worth
// far more than a sophisticated one skipped.
MaxPercentOfTurnover = 5;
TurnoverCeiling      = MaxPercentOfTurnover / 100 * Turnover;

// Cash we would allocate per slot, expressed in currency, using the starting
// equity as the reference. It does not compound, so it is deliberately
// conservative: as the account grows, the ceiling stays where it was.
SlotValue = StartingEquity / PosQty;
Buy       = Buy AND SlotValue <= TurnoverCeiling;

// ------------------------------------------------------ 8. before you believe
// Checklist, in order:
//   1. Settings -> General: periodicity Daily, initial equity matches
//      StartingEquity, margin 100, interest 0.
//   2. Settings -> Portfolio: max open positions matches PosQty. The formula
//      overrides this, but a mismatch means one of the two is a leftover.
//   3. Settings -> Trades: buy/sell price fields are irrelevant here because
//      the formula assigns all four price arrays. Confirm the delays are 1.
//   4. Report: turn "Include trade list in the report" on for this run.
//   5. Run once with the result list on Detailed log. Confirm that on at least
//      one bar more candidates appeared than slots, and that the taken ones are
//      the top-ranked ones.
//   6. Hand-check one trade against the chart, price by price, before reading
//      any statistic in the report.
