Project: Your First Complete Trading System
By the end of this session you will have a complete trading system: a written hypothesis, a rule set that passes the ambiguity test, an assumptions block, a formula that runs, a backtest report, and a page of notes recording what you concluded and what you refused to conclude.
You will also have resisted the strongest urge in this entire course. The system has two parameters. Both were chosen from convention before anything was run. Neither will be adjusted. The final section explains at length why that restraint is the most valuable thing in the project, but it is worth stating the short version now: you cannot recognise an overfitted result until you have seen an honest one produced by the same rules. This project manufactures that baseline.
What you are building
- HypothesisWritten and dated first
- RulesAmbiguity-tested
- AssumptionsCosts, fills, liquidity
- FormulaOne file, runnable
- ReportRead in a fixed order
- NotesWhat you may and may not say
What you need
Section titled “What you need”An AmiBroker installation with a daily end-of-day database, and a watch list of at least 100 symbols with a reasonable spread of liquidity. Nothing in this project needs a real-time feed, a data subscription or the Professional edition. Allow an hour: about fifteen minutes of writing, ten of setup, five of running, and the rest reading and arguing with the result.
1. The hypothesis
Section titled “1. The hypothesis”Written before anything else, and quoted here in full so that later sections can be checked against it rather than against a memory of it.
Observation. Liquid shares that spend long stretches above a slow moving average appear to keep rising for a while after crossing it.
Claim. In a universe of liquid shares, a long position opened after the close crosses above its own 200-day simple moving average, and closed on the reverse cross or after 60 bars — whichever comes first — produces a positive average result per trade after the stated costs.
Mechanism. Slow, uneven diffusion of information, and the reluctance of holders to sell winners. Both are plausible; neither is established by this test, and this formula cannot distinguish between them.
Prior. Low. The rule is one of the most widely known in the field, which means any effect that was easy to capture has had decades of attention.
Falsified by. An average result per trade that is not positive after costs; or one that is positive only in a single sub-period; or one that disappears when the moving-average length is moved a little in either direction.
Cost assumption, decided now. 0.10% slippage per side plus 0.10% commission per side, so roughly 0.40% round trip.
Notice the prior. Being explicit that you expect this to be weak changes how you will read the report, and it makes a strong result more interesting rather than less.
2. The rules
Section titled “2. The rules”| Section | Specification |
|---|---|
| Universe | The watch list this is applied to, restricted to symbols whose 50-bar average turnover (close × volume) on the signal bar is at least 5,000,000 in the quote currency |
| Data | Your end-of-day database, split- and dividend-adjusted daily bars. Record the vendor and refresh date |
| Entry | On the close of bar t: the close crosses above its own 200-bar simple moving average, the liquidity floor is met, and one position slot would be no more than 1% of the symbol’s average turnover. Event, not state |
| Exit | Whichever comes first: the close crosses back below the 200-bar average, or 60 bars have elapsed since entry. No stop |
| Sizing | 10 slots, 10% of portfolio equity each, on 100,000 initial equity, no margin |
| Execution | One bar of delay on all four signals; fill at the next bar’s open, adjusted by 0.10% against us; 0.10% commission per side |
| Selection | When more entries signal than there are free slots, prefer the most liquid candidate |
Two entries in that table deserve comment because they look like omissions and are not.
No stop. A stop is a second rule with its own parameter, and adding one now would mean two things changed at once between “no system” and “a system”. It also means every loss in this test is bounded only by the exit rule and the 60-bar clock, which will show up in the report as larger individual losses than a stopped system would produce. That is information, not a defect. Parts 28 and 34 add stops properly, and you will then be able to measure what a stop actually did, because you have this run to compare against.
A stated tie-break. When eleven symbols signal and three slots are free, something decides. Left unspecified, that something is invisible to a reader and hard to reproduce. Preferring the most liquid candidate is a choice made on grounds of fill realism rather than expected return, which keeps it out of the business of improving the result.
3. The assumptions block and the formula
Section titled “3. The assumptions block and the formula”The formula reuses the harness from the previous lesson: a header a reader can evaluate without AmiBroker, then constants, then settings, then the rules.
Complete runnable AFL
// first-system.afl// Part 27 - Project: Your First Complete Trading System//// A deliberately plain long-only trend system, written so that every decision// behind it is visible on the page. It has two numbers in it and neither of// them has been searched for. That is the point of the exercise: before you can// recognise a result that has been fitted to its data, you need to have seen// one that has not.//// ============================ HYPOTHESIS ==============================// Observation Liquid shares that spend long stretches above a slow moving// average appear to keep rising for a while after crossing it.// Claim In a universe of liquid shares, a long position opened after// the close crosses above its own 200-day simple moving average// and closed on the reverse cross or after 60 bars, whichever// comes first, produces a positive average result per trade// after the costs stated below.// Mechanism Slow, uneven diffusion of information and the reluctance of// holders to sell winners. Both are documented behaviours in the// academic literature and both are plausible here; neither is// established by this test, and this formula cannot distinguish// between them.// Falsified by An average result per trade that is not positive after costs,// or one that is positive only in a single sub-period, or one// that disappears when the moving-average length is moved a// little in either direction.// ======================================================================//// ============================ ASSUMPTIONS =============================// Data source Your end-of-day database. Record the vendor and the date// the history was last refreshed alongside the result.// Adjustment Split- and dividend-adjusted daily bars.// Interval Daily.// Universe The watch list this is applied to, further restricted by// the liquidity floor below.// Survivorship NOT controlled. If the watch list is today's index members// the answer is about survivors, which is a different and// easier question. See Part 30.// Signal timing Computed on the close of bar t from data up to bar t.// Fill The opening print of bar t+1, in full.// Trade delays 1 bar on buy, sell, short and cover.// Slippage 0.10% per side, applied by moving BuyPrice and SellPrice.// Commission 0.10% of trade value per side, CommissionMode 1.// Liquidity floor 50-day average turnover of at least 5,000,000 in the quote// currency on the signal bar.// Participation One slot capped at 1% of that average turnover.// Position sizing 10 slots, 10% of portfolio equity each.// Initial equity 100,000 in the quote currency.// Interest 0% on idle cash.// Selection When more entry signals appear than there are free slots,// the most liquid candidate is preferred. Recorded here// because otherwise the choice would be invisible.// Stops NONE. This is a decision, not an oversight: a stop is a// second rule with its own parameter, and adding one now// would mean two things changed at once. Parts 28 and 34// add stops properly. Until then, the only thing limiting a// losing trade is the exit rule and the 60-bar clock.// Short selling Not simulated.// NOT MODELLED Bid-ask spread beyond the slippage figure; market impact// beyond the participation cap; taxes; borrow costs;// partial fills; corporate actions your data may not carry.// ======================================================================//// How to run it:// Formula Editor -> paste -> Tools -> Send to Analysis// Apply to: Filter, and choose a watch list of at least 100 liquid symbols// Range: From-To dates, chosen and written down BEFORE you press Backtest// Settings: Periodicity = Daily// Then press Backtest, then Report.
// ------------------------------------------------ the two rule numbers// Chosen once, from convention, and left alone. 200 because it is the length// most widely quoted, which makes it the length least likely to have been// picked because it flattered this particular data. 60 because it is about a// quarter of a trading year and bounds how long capital sits in one idea.MaPeriod = 200;MaxHoldBars = 60;
// ---------------------------------------------------------------- costsSlippagePct = 0.10;CommissionPct = 0.10;
// ------------------------------------------------------------ portfolioStartEquity = 100000;PositionSlots = 10;PositionPct = 100 / PositionSlots;
// ------------------------------------------------------------ liquidityTurnoverWindow = 50;MinTurnover = 5000000;MaxParticipation = 0.01;
SetOption( "InitialEquity", StartEquity );SetOption( "MaxOpenPositions", PositionSlots );SetOption( "AccountMargin", 100 );SetOption( "InterestRate", 0 );SetOption( "CommissionMode", 1 );SetOption( "CommissionAmount", CommissionPct );
SetPositionSize( PositionPct, spsPercentOfEquity );SetTradeDelays( 1, 1, 1, 1 );
BuyPrice = Open * ( 1 + SlippagePct / 100 );SellPrice = Open * ( 1 - SlippagePct / 100 );
// ------------------------------------------------------------ the rulesTrendLine = MA( Close, MaPeriod );
AverageTurnover = MA( Close * Volume, TurnoverWindow );LiquidEnough = AverageTurnover >= MinTurnover;
SlotValue = StartEquity * PositionPct / 100;SmallEnough = SlotValue <= MaxParticipation * AverageTurnover;
// Both filters are applied to the entry only. An exit must never be filtered// away: if a symbol dries up while we hold it, we still want out.Buy = Cross( Close, TrendLine ) AND LiquidEnough AND SmallEnough;Sell = Cross( TrendLine, Close );
Buy = ExRem( Buy, Sell );Sell = ExRem( Sell, Buy );
// The time exit. The N-bar stop is counted by the backtester from the entry// bar, which is why it is expressed here rather than with BarsSince() on a// signal array that the backtester is about to shift by one bar anyway.// SetTradeDelays only shifts Buy/Sell/Short/Cover; an exit produced by// ApplyStop is not one of those four arrays and is not shifted.ApplyStop( stopTypeNBar, stopModeBars, MaxHoldBars );
// When more symbols signal on the same bar than there are free slots, prefer// the one where our fill assumption is most defensible. This is a liquidity// preference, not a performance preference, and it was chosen before the first// backtest was run.PositionScore = AverageTurnover;
// ------------------------------------------------------------ chart view// Applying the same file to a chart pane draws the rule on the price, so the// trades in the report can be checked against something you can see. The arrows// mark SIGNAL bars; the fills happen one bar later._SECTION_BEGIN( "First system" );
Plot( Close, "Close", colorDefault, styleCandle );Plot( TrendLine, "MA(" + MaPeriod + ")", colorBlue, styleLine | styleThick );
PlotShapes( shapeUpArrow * Buy, colorGreen, 0, Low, -20 );PlotShapes( shapeDownArrow * Sell, colorRed, 0, High, 20 );
_SECTION_END();How it works
Section titled “How it works”The two rule numbers come first, with the reason they were chosen. 200 because it is the most widely quoted length, which makes it the length least likely to have been picked because it flattered this data. 60 because it is roughly a quarter of a trading year. Neither is claimed to be a good number; both are claimed to be numbers chosen without looking.
The settings section puts every backtester option into the file. Initial equity, position
count, no margin, zero interest on idle cash, and commission as a percentage of trade value.
SetPositionSize( PositionPct, spsPercentOfEquity ) expresses the slot size as a percentage of
portfolio equity, which is the documented meaning of that constant. Ten slots at 10% each means
a fully invested portfolio when ten candidates are held, and the guide is explicit that a
position size must be set at all if you want more than one position open at a time.
The execution section applies one bar of delay to every signal and moves the fill against us by the slippage figure on both sides.
The filters apply the liquidity floor and the participation cap to the entry only. An exit
is never filtered: if a symbol dries up while we are holding it, we still want out. Both filters
are applied before ExRem(), because removing redundant signals first and filtering afterwards
can strand an exit with no matching entry.
The time exit uses the backtester’s N-bar stop, ApplyStop( stopTypeNBar, stopModeBars, 60 ),
which counts bars from the trade entry inside the engine. That is more reliable here than
counting on a signal array, because SetTradeDelays() shifts the four signal arrays and nothing
else — an exit produced by ApplyStop() is not one of those arrays, so it is generated from the
bar the trade really began on.
PositionScore carries the tie-break. The guide documents that when more entry signals
appear than MaxOpenPositions or available funds allow, AmiBroker ranks candidates by the
absolute value of this variable. Average turnover is always positive, so higher means more
liquid means preferred.
The chart section exists so that the same file can be applied to a chart pane and checked by eye. The arrows mark signal bars; the fills happen one bar later.
Key functions
Section titled “Key functions”SetPositionSize( size, method )— withspsPercentOfEquity,sizeis read as a percentage of portfolio-level equity. The other documented methods are shares, a dollar value, a percentage of the currently open position (for scaling only), and no change.SetOption( field, value )— sets an Analysis setting from the formula.fieldis a string, so a typo is silently ignored and the option simply does not take effect.ApplyStop( type, mode, amount, exitatstop, volatile, ReEntryDelay, ValidFrom, ValidTo )— here called with three arguments, matching the documented N-bar example. Part 28 covers the rest of the signature.ExRem( ARRAY1, ARRAY2 )— returns 1 on the first true value inARRAY1, then 0 untilARRAY2is true, even ifARRAY1is true again in between.
4. Running the first backtest
Section titled “4. Running the first backtest”A checklist, in order. Deviating from it is fine; not recording the deviation is not.
- Fix the range before you run anything. Choose the start and end dates and write them in your notes now. Choosing them afterwards, when you have seen how the equity curve looks, is a decision made on the data.
- Open the Formula Editor, paste the formula, save it under a name you will recognise, then Tools → Send to Analysis.
- In the Analysis window set Apply to to Filter and choose your watch list. Set Range to From-To dates and enter the dates from step 1.
- Press the Settings button and confirm Periodicity is Daily. Everything else the system needs is already set from the formula and will override the dialog, but it is worth looking once so that you know what the dialog contains.
- Press Backtest. The trade list appears in the results pane.
- Press Report for the full statistics. The Report button’s drop-down opens the Report Explorer, which keeps every historical report rather than only the last one.
- Save the report and the formula together, under the date. You will want to come back to this exact run in Part 28 and again in Part 31.
5. Reading the result without excitement
Section titled “5. Reading the result without excitement”The order in which you read a report determines what you conclude from it. Reading it in the order the eye naturally goes — biggest number first — reliably produces enthusiasm. Use this order instead, every time, and write each figure down before looking at the next.
First: how many trades. The report gives counts for all trades, long trades and short trades in three columns. If your run produced 34 trades, nothing that follows means much, whatever it says. Sample size does not appear in any single metric and it governs the reliability of all of them.
Second: Exposure %. Documented as market exposure computed bar by bar — the sum of the
per-bar exposures divided by the number of bars, where a bar’s exposure is the value of open
positions divided by portfolio equity. A system that was 12% invested on average is not
comparable to one that was 90% invested, and this figure is also the denominator of two other
metrics, so a small value inflates them dramatically.
Third: Max. system % drawdown. The largest peak-to-valley percentage decline in portfolio
equity, reported as a negative number. Read it as a question about yourself: could you have
kept trading through that, for as long as it lasted? Part 34 returns to this, and Part 29 to
the duration, which the headline figure does not show.
Fourth: Annual Return %. The compounded annual return, computed with proper compounding
over calendar days between the first and last bar. Note the label: the report row is
Annual Return %, not CAR and not CAGR, although the docs use CAR as the abbreviation in prose
and inside CAR/MaxDD.
Fifth: CAR/MaxDD. Annual Return % divided by Max. system % drawdown. This is the number
to prefer over the return alone, because it prices the return in units of the pain required to
collect it. The docs describe values above 2 as good; treat that as a convention rather than a
threshold, especially on a small trade sample.
Sixth: the trade statistics. Profit Factor (profit of winners divided by loss of losers),
Payoff Ratio (average win over average loss), Avg. Profit/Loss and Avg. Profit/Loss %
(which the docs also call expectancy in dollars and in percent), and Avg. Bars Held. Check
Avg. Bars Held against your 60-bar cap: if it is close to 60, the time exit is doing most of
the work and the reverse cross is nearly irrelevant, which is a finding about your rules.
Seventh: the equity curve, and specifically its flat periods rather than its slope.
The sentence you are allowed to write
Section titled “The sentence you are allowed to write”Draft your conclusion as one sentence with every qualifier in it. Something of this shape:
On [watch list], over [start] to [end], with a 50-bar turnover floor of 5,000,000, trading at the next open with 0.10% slippage and 0.10% commission per side, 10 equally weighted slots and no stop, these rules produced [n] trades with an
Avg. Profit/Loss %of [x],Exposure %of [y] andMax. system % drawdownof [z].
If you cannot fill in every bracket from your own notes, the missing one is a thing you did not record and should go back for. If the sentence feels deflating compared with the number you saw in the report, that is the correct emotional response and it is worth noticing.
6. Verifying it by hand
Section titled “6. Verifying it by hand”A report is only evidence if the trades in it are the trades your rules describe. Three checks, about ten minutes:
- Pick three trades at random from the trade list. For each, open the symbol’s chart with the same formula applied. The entry date should be exactly one bar after a green arrow, and the entry price should be that bar’s open plus 0.10%.
- Find a trade that lasted exactly 60 bars. There should be some, and their exits should be
on the clock rather than at a cross. If there are none at all, the N-bar stop is not in force
— check the
ApplyStop()line. - Check a rejected symbol. Find a thinly traded symbol in your watch list that produced no trades, and confirm on its chart that its price did cross the average. If it did and no trade appears, the liquidity floor is working. If it did and a trade does appear, it is not.
7. Common errors
Section titled “7. Common errors”| Symptom | Likely cause |
|---|---|
| Only one position is ever open | No position size set, so 100% of funds go into a single security. The guide states this explicitly |
| Many signals, very few trades | Slots full, or cash exhausted. In the default backtest mode, once a trade is skipped, later entries in that block are ignored until an exit appears |
| Entry prices are not the next bar’s open | SetTradeDelays() missing, or BuyPrice never assigned so the Settings trade price is in force |
| Commission appears to have no effect | Option name misspelled. SetOption() field names are strings and typos fail silently |
| Trades in symbols with tiny volume | Liquidity floor applied to the exit as well as the entry, or applied after ExRem() |
| No trades at all | Watch list not selected in Apply to; or the liquidity floor is above every symbol’s turnover; or the range predates enough history for a 200-bar average |
| Every trade lasts one bar | The entry was written as a state rather than an event, so Buy and Sell overlap |
| Results change between runs with no code change | A setting is coming from the dialog rather than the formula. Move it into the formula |
8. Extensions
Section titled “8. Extensions”Each of these is a separate experiment with its own write-up, not an improvement to this one. Record each as a new dated entry in your log, and keep this run as the baseline.
- Sensitivity, not optimisation. Run the same system at 150 and at 250 bars, and at 40 and 80 bars of hold. You are not looking for the best of the six; you are looking at whether the result is a plateau or a spike. A spike is a warning; a plateau is mild reassurance.
- Cost doubling. Set slippage and commission to 0.20% each and re-run. Note which conclusions survive.
- Universe split. Run separately on the most liquid half and the least liquid half of the watch list. If the effect lives only in the thin half, your fill assumptions are carrying the result.
- Regime split. Run on two or three non-overlapping sub-periods. One good period and two bad ones is a very different finding from three mediocre ones.
- The benchmark. Compute what simply holding the universe over the same period would have done. A rule that returns less than that, with more work and more risk, has told you something clear.
9. Why we are not optimising yet
Section titled “9. Why we are not optimising yet”The system in front of you has two numbers in it. You could try twenty values of each, run 400 backtests in a few minutes, and take the best. Every instinct says to do it. Here is what would happen if you did.
The best of many is biased upward, by an amount you cannot see. Results vary across parameter values partly because of a real effect and partly because of noise. Taking the maximum selects for both, and the selected maximum overstates what that parameter set will do on new data. The bias grows with the number of combinations tried, and nothing in the report tells you how large it is — the report of the best run looks exactly like the report of a genuinely good system. Part 31 measures this properly.
You would have no baseline left. With an optimised result and nothing to compare it to, you cannot answer the only question that matters: how much of this came from the idea and how much from the searching? An unoptimised run of the same rules, on the same universe, over the same period, answers it directly. That run only exists if you do it first.
It teaches the wrong goal. An hour spent making a number larger trains you to treat the number as the objective. The objective is a defensible belief about how a rule behaves. Those two goals point in opposite directions surprisingly often: the honest response to a weak result is usually to abandon the idea, and optimisation always offers an alternative.
The parameters are not where the leverage is anyway. Position sizing, costs, the universe and the exit structure move results far more than the length of a moving average. Part 34 demonstrates this by running one signal set through three sizing models. Tuning the average first is optimising the smallest term.
And you would spend your out-of-sample data. Every additional variation you test on the same history consumes some of its capacity to surprise you. Data you have looked at forty times can no longer tell you that you were wrong. Part 32 builds the walk-forward machinery that manages this budget; until then, the cheapest way to preserve it is not to spend it.
Recap and deliverables
Section titled “Recap and deliverables”You should now have six artefacts, all dated and stored together:
- The hypothesis, written before anything was run, with its prior and its falsifier.
- The rules table, which passed a solo ambiguity test.
- The assumptions block, with the not-modelled list filled in.
first-system.afl, unmodified.- The backtest report, saved, with the range and watch list recorded.
- Your one-sentence conclusion with every qualifier attached, plus a short list of the things the run does not settle.
That sixth item is the deliverable that distinguishes this from a hobby. A system you can describe honestly, including its limits, is one you can improve deliberately. A system attached to a number you cannot qualify is one you will keep adjusting until it stops being possible to tell whether it ever worked.
Part 28 opens the backtester properly: position sizing methods, cost modelling, stops, and the move from one symbol to a portfolio competing for one pool of capital. Part 29 reads the report in full. Part 30 is where this system gets attacked, and where you find out how much of it survives.
Check your understanding
Sources for this lesson
7 verified · checked 2026-08-31
- 01AmiBroker User's Guide — Portfolio-level backtestingamibroker.com/guide/h_portfolio.html2026-08-31
- 02AmiBroker User's Guide — Back-testing your trading ideasamibroker.com/guide/h_backtest.html2026-08-31
- 03AmiBroker User's Guide — System test report windowamibroker.com/guide/w_report.html2026-08-31
- 04AmiBroker User's Guide — Using New Analysis windowamibroker.com/guide/h_newanalysis.html2026-08-31
- 05AFL Function Reference — ApplyStopamibroker.com/guide/afl/applystop.html2026-08-31
- 06AFL Function Reference — SetPositionSizeamibroker.com/guide/afl/setpositionsize.html2026-08-31
- 07AFL Function Reference — SetOptionamibroker.com/guide/afl/setoption.html2026-08-31
Every technical claim on this page was checked against the official AmiBroker documentation on the date shown. Where the course disagrees with folklore, the source is how you can tell which one to trust.