Building a Forex Expert Advisor with Ensemble Intelligence: From Chaos to Consistency

Building a Forex Expert Advisor with Ensemble Intelligence: From Chaos to Consistency

12 April 2026, 18:43
Evgeniy Scherbina
0
48

About four months ago, I dove deep into the world of ensembles for building Forex expert advisors. Day and night, I ran experiments, chasing a simple but powerful idea. Combine multiple timeframes, multiple folds, and different perspectives into one unified system that produces a consensus decision. At first, it felt almost too elegant to fail.

The core intuition was straightforward: logits that correctly detect their regime should overpower the noise from weaker signals. When aggregated, these logits form a clearer directional bias. In the subplot “TF logits (avg)” (shown below), you can see how different timeframes frequently diverge—some pointing up, others down—creating moments where the consensus becomes strong enough to justify a trade or avoid one entirely.


TF-LOGITS


But very quickly, I ran into a classic and frustrating problem: unknown future periods. Imagine two test windows—August to December and November to March. Both span five months. You train one model up to August, and another up to November. And here’s the catch: when one performs well, the other fails. When the second succeeds, the first collapses. This instability persisted for months.

I tried everything. Fixed folds, where the dataset is split into equal chunks—no success. Expanding folds, where each new fold includes all previous data—still no luck. Then regime-based folds, isolating volatile, trending, or flat markets—again, no consistency. Each approach seemed promising, yet ultimately failed to generalize.

Then came a critical realization. Why should folds be tied to specific dates at all? Why anchor everything to August or November as if those points define the future? What if a model simply isn’t ready by that date? That assumption alone could break the entire system.

The breakthrough was simple, yet powerful: instead of anchoring folds to fixed endpoints, I began selecting them within a rolling window—say, six months. Each subsequent fold would slightly overlap with the previous one, touching its boundary and extending further into the past. This created a smooth transition between folds, rather than abrupt, artificial splits.

Each fold has a fixed duration and moves forward in time with a constant step—one week. This results in hundreds of folds: over 500 on the 6-hour timeframe, more than 400 on the 8-hour timeframe, and so on. The diversity and continuity of these folds became the key.

Below is a minimal piece of code that demonstrates this “simple magic” of generating weekly rolling folds:

def make_weekly_folds(
        self, span_train_months, span_val_months,
        log_path='empty',
        pass_num=0,
        fold_filter=None
    ):

    self._ensure()

    train_sec = span_train_months * SEC_IN_MONTH
    val_sec = span_val_months * SEC_IN_MONTH

    T0 = float(self._T.min())
    T1 = float(self._T.max())

    folds = []
    fold_id = 1

    if log_path != 'empty':
        f = open(log_path, "w", encoding="utf-8")

    # Align timestamps to Monday 00:00 for consistent weekly stepping
    def to_monday(ts):
        d = dt.datetime.fromtimestamp(ts)
        d = d.replace(hour=0, minute=0, second=0, microsecond=0)
        return (d - dt.timedelta(days=d.weekday())).timestamp()

    current_start = to_monday(T0)
    if current_start < T0:
        current_start += 7 * 86400

    prev_train_start = None

    while True:
        train_start = current_start
        train_end = train_start + train_sec
        val_start = train_end
        val_end = val_start + val_sec

        # Stop if validation exceeds dataset bounds
        if val_end > T1:
            break

        fold = self._make_fold(
            fold_id,
            train_start, train_end,
            val_start, val_end
        )

        if fold:
            if fold_filter is None or fold_id in fold_filter:
                folds.append(fold)

            if prev_train_start is None:
                shift_days_info = "N/A"
            else:
                shift_days_val = (train_start - prev_train_start) / 86400
                shift_days_info = f"{shift_days_val:.1f}d"

            def ts_to_str(ts):
                return dt.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M")

            if log_path != 'empty':
                line = (
                    f"PASS-{pass_num} FOLD-{fold_id} | "
                    f"SHIFT: {shift_days_info} | "
                    f"TRAIN: {ts_to_str(train_start)} -> {ts_to_str(train_end)} | "
                    f"VAL: {ts_to_str(val_start)} -> {ts_to_str(val_end)}\n"
                )
                f.write(line)

            fold_id += 1

        prev_train_start = train_start

        # Fixed step: move forward by one week
        current_start = to_monday(current_start + 7 * 86400)

    return folds

This approach finally allowed me to build ensembles that survive unknown periods with solid profitability. Importantly, the same methodology is applied consistently across both training and ensemble construction.


Test August through December


In the August–December test, the advisor stays flat during the first month, then starts capturing strong profits. In the November–March test, performance is smoother: it begins collecting gains almost immediately and scales up over time.


Test November through March


A notable pattern is the preference for USDJPY, which, as we know, was highly volatile during this period. That volatility created exactly the kind of opportunity the ensemble was designed to detect and exploit.

The above results come from the LSTM Ensemble with default settings.

For a balanced setup targeting around 10% drawdown and 400+ USD profit over five months, I recommend:

  • Deposit: 1500 USD
  • Max trades: 2
  • Range: 100
  • Risk: 0.5

At this point, I’m seriously considering starting a GitHub repository or even a YouTube channel. The topic has grown far beyond a single post. There’s also a meta-model layer, additional neural architectures, and cross-symbol training (gold + major FX pairs). There’s a lot of room to expand—and I’m just getting started.