| # FilterAttribution
|
|
|
| Shapley attribution of strategy filters in native MQL5: one unfiltered
|
| backtest that records every filter's verdict on every trade already contains
|
| the result of every filter combination, exactly, and the report shares the
|
| gain out among the filters without depending on the order they were added in.
|
|
|
| Companion code for the MQL5 article: https://www.mql5.com/en/articles/25617
|
|
|
| ## What it does
|
|
|
| Filters are usually judged one at a time: add one, rerun the backtest, and
|
| credit the filter with the change. That credit depends on what was already in
|
| the stack when the filter went in. On the six-filter test bed here, the daily
|
| trend filter can be credited with anything from -14.7 R to +65.8 R depending
|
| on the order, so the one-at-a-time story is mostly a story about the order of
|
| the experiment. Settling it by brute force takes 2^n backtests, 64 for six
|
| filters and 4,096 for twelve.
|
|
|
| None of them is needed when filters only veto trades. Run the strategy once
|
| with every filter evaluated and none enforced, and store each trade's verdicts
|
| as a bit mask in its entry comment, together with the money risked. The result
|
| of any combination is then a sum over the trades whose mask contains it. Three
|
| conditions make that sum exact rather than approximate: a vetoed signal must
|
| consume whatever the taken one would have (here, the day's first breakout),
|
| risk must be a fixed money amount rather than a share of equity, and positions
|
| must not overlap. The Expert Advisor's enforce mode exists to prove the
|
| conditions hold: optimising the filter mask over all 64 values runs 64 real
|
| backtests, and `OnTesterDeinit` compares each against the recorded table.
|
| All 64 matched in trade count and total R, with a largest difference of zero.
|
|
|
| `CFilterAttribution` turns the coalition values into Moebius dividends, and
|
| everything else is read off that one decomposition: the Shapley value (each
|
| filter's average marginal gain over every order), the leave-one-out cost (what
|
| the full stack loses without it) and the pairwise interaction index (positive
|
| when two filters work together, negative when they duplicate each other). A
|
| moving-block bootstrap gives each number an interval, the sample is split at a
|
| date to see which credit survives, and a rolling Shapley value over a trailing
|
| window of trades shows when it was earned. An independent brute-force average
|
| over all 720 orderings agrees with the library to within 1e-13.
|
|
|
| The most important number in the report is the chance level. When the base
|
| strategy loses, any veto earns credit simply by trading less, so the library
|
| computes exactly what a random veto at each filter's own pass rate would have
|
| earned, and the same for every pair's interaction. On the test bed that chance
|
| credit runs from 1.8 R to 16.3 R depending on the filter, and a third of the
|
| two trend filters' raw overlap, and half or more of two other pairs', was
|
| chance. Only the part above it is
|
| credit for choosing which trades to block.
|
|
|
| The test bed is a London opening-range breakout on GBPUSD M5, 2023 to
|
| 2026.09, with six filters: daily and H4 trend, narrow range, midweek, strong
|
| close and active market. Unfiltered it loses 81.6 R over 955 trades; all six
|
| filters make 11.0 R on 42 trades. Read against chance, removal cost and the
|
| split, the attribution keeps only the H4 trend filter, which takes the
|
| strategy to 7.0 R on 461 trades, close to breakeven, and its margin over
|
| chance was the same before and after the split. The daily trend, midweek and
|
| active-market filters earn positive credit but cost almost nothing to remove,
|
| because other filters in the stack already do their work. Re-checking the dropped
|
| filters against the kept one finds a single candidate, the strong-close
|
| filter, which adds on both sides of the split and takes the pair to 21.4 R on
|
| 356 trades. That window has now been used to choose it, so only later data can
|
| confirm it. The method found which filter carries the edge there is; it did
|
| not create a larger one.
|
|
|
| ## Layout
|
|
|
| ```
|
| Include/FilterAttribution/FilterAttribution.mqh verdict table, coalition values, Moebius dividends, Shapley value, leave-one-out and interaction with chance levels, block bootstrap, rolling credit
|
| Experts/FilterAttribution/ORB_FilterLab.mq5 London opening-range breakout with six filters; records verdicts, or enforces a mask and verifies the table against every enforced backtest
|
| Scripts/FilterAttribution/FilterAttribution.mq5 loads a saved table or the account's tagged history, prints the report with the split, selection and kept-stack checks, and draws the dashboard
|
| ```
|
|
|
| Run `ORB_FilterLab.mq5` once in the Strategy Tester in `LAB_RECORD` mode. When
|
| the test ends it rebuilds the table from its own deal history and saves it to
|
| the terminal's common data folder. Then, once, set `InpMode` to `LAB_ENFORCE`
|
| and optimise `InpMask` from 0 to 63 with the slow complete algorithm; the
|
| summary line printed to the Experts log must report zero mismatches before the
|
| table is trusted. Finally run `FilterAttribution.mq5` on a chart to print the
|
| report and draw the dashboard. Set `InpKeep` to the mask of the filters you
|
| plan to keep to see what each dropped filter adds back to them.
|
|
|
| To attribute your own filters, nothing in the library needs to change. In your
|
| Expert Advisor, evaluate every filter at entry, open the trade regardless,
|
| write the pass mask and the money risked into the order comment with
|
| `CFilterAttribution::Tag`, make sure a vetoed signal is spent exactly as a
|
| taken one would be, and size from a fixed money amount. Add an enforce mode
|
| and run the verification once before reading any attribution.
|
|
|
| ## Disclaimer
|
|
|
| Educational code. The opening-range breakout is a test bed for the
|
| attribution method, not a trading strategy: unfiltered it loses money, and the
|
| best verified result is close to breakeven, with one candidate filter that has
|
| not been tested on unseen data. The method applies only to filters that veto
|
| trades; filters that resize or reshape trades break the subset property, and
|
| the verification will report it. Past performance in the Strategy Tester does
|
| not guarantee future results. Test on your own data and broker conditions
|
| before drawing conclusions.
|