Пайплайны Codex: от Python к MQL5 для выбора индикаторов — анализ ETF FXI за несколько кварталов
Введение
В предыдущей статье мы рассмотрели, как можно анализировать различные заранее заданные пары индикаторов и отбирать из них наиболее эффективные варианты для торговли ETF VGT. Тогда наша задача заключалась в том, чтобы подобрать два взаимодополняющих индикатора, представляющих набор сигнальных паттернов, чтобы их можно было как можно быстрее применить на практике. В той статье мы, собственно говоря, не особо подробно остановились на том, как изначально был выбран набор из 5 пар взаимодополняющих индикаторов. Поэтому в данной статье мы постараемся разобраться в этом вопросе.
К выбору технических индикаторов в любой торговой системе зачастую подходят без достаточной методической строгости, в результате чего то, какие индикаторы будут в итоге использоваться, может определяться личными предпочтениями, отдельными случаями из практики или предвзятыми интерпретациями исторических данных. Такой неструктурированный и почти произвольный подход может привести к ошибке выжившего, ретроспективному подтверждению, структурному переобучению и в целом подорвать аналитическую строгость. Для получения надежных сигналов в различных рыночных режимах всегда предпочтительнее использовать тщательно выстроенный процесс.
Необходимость наличия такой системы становится ещё более очевидной, если речь идёт о таком ETF, как FXI. Его поквартальная динамика отражает меняющиеся условия волатильности, изменения структуры моментума, а также колебания ликвидности. Индикаторы, которые выглядят многообещающими в одном рыночном режиме, могут легко оказаться неэффективными в другом. Поэтому мы стремимся исправить эту ситуацию, предлагая строгую последовательность шагов на базе Python, которая завершается применением в советнике, собранном с помощью Мастера MQL5.
ETF FXI и его поквартальная динамика
Этот ETF, отслеживающий динамику акций китайских компаний с большой капитализацией, часто демонстрирует определенные повторяющиеся структурные особенности, которые можно объяснить макроэкономическими событиями, мерами государственного регулирования, глобальными настроениями к риску или даже циклами ликвидности. Эти особенности проявляются в виде чередующихся режимов волатильности, неоднородной структуры моментума, а также периодических колебаний между трендовым движением и возвратом к среднему. При проведении анализа за несколько кварталов такие смены рыночных режимов действительно приобретают значение. Эффективность того или иного индикатора в одном рыночном режиме может сойти на нет в следующем, а это означает, что нам нужен подход к анализу за несколько кварталов, позволяющий наблюдателю разделить динамику цены FXI на набор «дискретных окон».
Такая сегментация помогла бы выделить различия в силе тренда и амплитуде колебаний. Это может служить основой для оценки способности индикаторов генерировать сигналы, поскольку каждый из них анализируется в различных рыночных режимах. Чтобы обеспечить надлежащий контекст, рассматриваемый ниже пайплайн разбивает ценовые данные ETF FXI на 4-часовом таймфрейме на кварталы и даёт наглядное представление их поведения по кварталам.
Прежде чем углубиться в эту тему, мы представим общий обзор данного ETF с момента его создания 5 октября 2004 года. С момента выпуска было проведено дробление акций, что объясняет падение цены в 2008 году, но в остальном ETF, по-видимому, ведёт себя как валютная пара на рынке Форекс, колеблясь вокруг ценовой отметки 40 долларов.

При сегментации ценовых данных FXI, которые можно получить из MetaTrader 5 с помощью модуля Python для MetaTrader 5, ниже представлен фрагмент кода Python, демонстрирующий, как можно выполнить такую сегментацию. Эти сегменты представляют собой новый «формат данных», который играет ключевую роль в пайплайне, используемом нами ниже.
# python libraries import MetaTrader5 as mt5 import pandas as pd if not mt5.initialize(login=XXXXXXXXX, server="XXXXXXXX", password="XXXXXXXXX"): print("initialize() failed, error code =", mt5.last_error()) quit() # set start and end dates for history data from datetime import timedelta, datetime # end_date = datetime.now() end_date = datetime(2025, 12, 1, 0) start_date = datetime(2020, 1, 1, 0) # get rates fxi_rates = mt5.copy_rates_range("#FXI", mt5.TIMEFRAME_H4, start_date, end_date) df = pd.DataFrame(fxi_rates) df["time"] = pd.to_datetime(df["time"], unit="s") df.set_index("time", inplace=True) # enforce column structure df = df.rename(columns={ "open": "open", "high": "high", "low": "low", "close": "close" }) # assign quarterly labels df["quarter"] = df.index.to_period("Q") # preview distribution quarter_counts = df["quarter"].value_counts().sort_index() print(quarter_counts)
Выполнение этого короткого кода позволяет определить границы кварталов, как показано в приведённых ниже журналах вывода.
quarter
2020Q2 68
2020Q3 128
2020Q4 167
2021Q1 170
2021Q2 126
….
2025Q2 127
2025Q3 130
2025Q4 104
Freq: Q-DEC, Name: count, dtype: int64 Приведённое выше разбиение задаёт временную структуру, благодаря которой все оценки индикаторов и оценки паттернов помещаются в систему, удобную для сравнения. Дискретные и сопоставимые квартальные окна.
Подготовка набора данных в Python
Чтобы выстроить основу для оценки индикаторов, нам необходимо подготовить исходные наборы данных с максимально возможной точностью, поскольку даже самые, казалось бы, незначительные ошибки могут иметь каскадные последствия. Используемая нами 4-часовая структура OHLC ETF FXI служит основой для всех последующих расчётов. Прежде чем применять какую-либо логику индикаторов или паттернов, необходимо проверить и обеспечить целостность, согласованность и временную синхронизацию данных. Предварительная обработка обеспечивает точность сегментации, предотвращает искажение расчётов форвардной доходности из-за отсутствующих или неправильно упорядоченных данных, а также гарантирует, что каждый индикатор получает чистое или относительно однородное входное пространство.
Этап подготовки набора данных состоит из четырёх операций:
- Проверка согласованности полей OHLC
- Повторная синхронизация временных меток для поддержания стандартного 4-часового интервала
- Обработка пропущенных наблюдений или разрывов; а также
- Присвоение квартальных меток для аналитической сегментации
В совокупности эти операции обеспечивают стабильную временную структуру, в рамках которой наша оценочная таблица индикаторов может формироваться с минимальными искажениями из-за нерегулярностей в исходных данных. Мы реализуем эти процедуры в следующем скрипте на Python. В нём описывается стандартный рабочий процесс предварительной обработки данных, импортируемых модулем MetaTrader 5.
# python libraries import MetaTrader5 as mt5 import pandas as pd import numpy as np from datetime import datetime, time from pandas.tseries.holiday import USFederalHolidayCalendar ... # ------------------------------------------------------------ # Build base DataFrame # ------------------------------------------------------------ df = pd.DataFrame(fxi_rates) # MT5 times are in seconds since epoch df["time"] = pd.to_datetime(df["time"], unit="s") df.set_index("time", inplace=True) df = df.sort_index() # Confirm essential OHLC columns exist required_cols = {"open", "high", "low", "close"} if not required_cols.issubset(df.columns): missing = required_cols - set(df.columns) raise ValueError(f"Dataset missing required columns: {missing}") # ------------------------------------------------------------ # Check interval consistency and resample to uniform 4h grid # ------------------------------------------------------------ time_diff = df.index.to_series().diff().dropna() irregularities = time_diff[time_diff != pd.Timedelta(hours=4)] print("Irregular intervals detected:\n", irregularities.head()) # Use lowercase "h" to avoid FutureWarning df_resampled = df.resample("4h").ffill() # ------------------------------------------------------------ # Attach quarterly segmentation # ------------------------------------------------------------ df_resampled["quarter"] = df_resampled.index.to_period("Q") # ------------------------------------------------------------ # Market-hours mask + U.S. holidays detection # ------------------------------------------------------------ # Assume MT5 timestamps are in UTC. If your server is NOT UTC, # adjust this localize step accordingly. idx_utc = df_resampled.index.tz_localize("UTC") # Convert to New York time (U.S. Eastern, with DST handled) idx_ny = idx_utc.tz_convert("America/New_York") # Weekday mask (Mon–Fri) is_us_weekday = idx_ny.weekday < 5 # 0=Mon, 4=Fri # Session time mask (09:30–16:00 New York time) ny_times = idx_ny.time session_start = time(9, 30) session_end = time(16, 0) is_session_time = np.array( [(t >= session_start) and (t <= session_end) for t in ny_times], dtype=bool ) # U.S. holiday calendar (federal holidays; close approximation) cal = USFederalHolidayCalendar() holidays = cal.holidays( start=idx_ny.min().date(), end=idx_ny.max().date() ) # Normalize times to dates and check membership in holiday list is_holiday = pd.Series(idx_ny.normalize()).isin(holidays).to_numpy() # Final market mask: weekday, in session, not holiday market_mask = is_us_weekday & is_session_time & (~is_holiday) # Apply mask df_market = df_resampled[market_mask].copy() print("Resampled full data shape:", df_resampled.shape) print("Market-hours non-holiday data shape:", df_market.shape) print(df_market.head()) print(df_market.tail())
Открытость языка Python способствовала созданию множества модулей и библиотек. Поэтому в приведённом выше коде мы также добавляем: маску торговых часов для регулярной сессии США (09:30–16:00 по нью-йоркскому времени) и определение праздничных дней в США с использованием USFederalHolidayCalendar. Приведённый выше фрагмент кода даёт следующие выходные данные, которые здесь сокращены, чтобы выделить обнаруженные нерегулярные интервалы.
Irregular intervals detected: time 2020-05-14 12:00:00 0 days 20:00:00 2020-05-15 12:00:00 0 days 20:00:00 2020-05-18 12:00:00 2 days 20:00:00 2020-05-19 12:00:00 0 days 20:00:00 2020-05-20 12:00:00 0 days 20:00:00 Name: time, dtype: timedelta64[ns] Resampled full data shape: (12153, 8) Market-hours non-holiday data shape: (2896, 8)
Таким образом, контрольный список проверки целостности, составленный на основе приведённого выше запуска, выглядит следующим образом.
| Пункт | Статус | Подробности |
|---|---|---|
| Получение данных из MetaTrader 5 | OK | Бары FXI H4 успешно получены |
| Преобразование даты и времени | OK | Временные метки Unix преобразованы в формат pandas datetime |
| Сортировка индекса | ОК | Данные отсортированы в хронологическом порядке |
| Обязательные столбцы OHLC присутствуют | ОК | Open, High, Low и Close присутствуют |
| Обнаружение нерегулярных интервалов | Предупреждение | 20-часовые разрывы вместо 24-часовых, выявленные при переходах через ночь, выходные и праздничные дни |
| Применена передискретизация | ОК | Применена 4-часовая сетка, форма выходных данных = 12153 |
| Локализация часового пояса | ОК | Применовано преобразование UTC — America/NY |
| Применена маска будних дней | ОК | Сохранены только дни с понедельника по пятницу |
| Применена маска времени сессии | ОК | Сохранены бары, соответствующие сессии с 9:30 до 16:00 |
| Определение праздничных дней | OK | Федеральные праздники США исключены |
| Итоговые часы работы рынка | OK | Размерность полученных данных = 2896 строк |
| Сегментация по кварталам | OK | Столбец квартала назначен |
Теперь, когда наш датафрейм входных данных «очищен», мы готовы перейти к следующим шагам.
Пайплайн рекомендаций по индикаторам
В идеале фреймворк для формального выбора индикатора должен строиться на основных принципах модульности, воспроизводимости и определённой аналитической прозрачности. Представленный здесь пайплайн призван соответствовать этим требованиям. Мы выносим расчёт индикаторов, формирование паттернов, оценку форвардной доходности и присвоение баллов в отдельные компоненты, которые можно повторно использовать с различными классами активов помимо протестированного FXI. Наш модульный подход, по крайней мере на бумаге, гарантирует, что каждый компонент можно независимо модернизировать или отлаживать без ущерба для целостности общего процесса оценки.
Приведённая выше схема иллюстрирует наш подход на концептуальном уровне. Этот пайплайн построен вокруг трёх основных абстракций. Первой из них является спецификация индикаторов, в которой мы описываем математические формулы, используемые для расчёта значений индикаторов, а также детерминированные правила, задающие бычьи, медвежьи или нейтральные сигнальные паттерны. Вторая абстракция — это «движок» поквартальной оценки. Здесь мы применяем эти спецификации к заданному квартальному сегменту данных OHLC, а затем вычисляем форвардную доходность на заданном временном горизонте, чтобы сформировать метрики эффективности для каждого паттерна. Третьим уровнем абстракции является уровень ранжирования и формирования рекомендаций. На этом этапе мы агрегируем результаты по всем индикаторам, а затем сортируем их по эмпирической эффективности, чтобы сформировать набор рекомендаций.
Разделение этих этапов важно и критически необходимо. Отделяя вычислительную логику от оценки, аналитик избегает распространённой ошибки — смешения определения паттернов с измерением эффективности, что обычно приводит к смещениям. Кроме того, эта структура хорошо масштабируется: новые индикаторы можно добавлять через уровень спецификации индикаторов, что позволяет расширять набор состояний паттернов и горизонтов доходности без существенной перестройки пайплайна. Таким образом, пайплайн представляет собой долгосрочный аналитический актив, а не одноразовый скрипт.
Поэтому мы реализуем это на Python, как показано ниже. В приложенном коде есть основной блок, демонстрирующий, как этот фрагмент пайплайна можно использовать для получения метрик эффективности по индикатору, которым в данном конкретном случае является простая скользящая средняя.
# python libraries import MetaTrader5 as mt5 from dataclasses import dataclass from typing import Callable, Dict, Tuple import pandas as pd import numpy as np from datetime import datetime, time from pandas.tseries.holiday import USFederalHolidayCalendar # --------------------------------------------------------------- # 1) Helper functions you referenced but didn't define # --------------------------------------------------------------- def compute_forward_returns(df: pd.DataFrame, horizons: Tuple[int, ...]) -> pd.DataFrame: """ Compute forward returns over the given horizons. Assumes df has a 'close' column. Returns columns like 'fwd_1', 'fwd_3', etc. """ out = {} close = df["close"] for h in horizons: # forward return: (close[t+h] / close[t]) - 1 out[f"fwd_{h}"] = close.shift(-h) / close - 1.0 return pd.DataFrame(out, index=df.index) def score_indicator(pattern_states: pd.DataFrame, forward_returns: pd.DataFrame) -> Dict[str, float]: """ Very simple scoring function: For each pattern (bullish/bearish/flat) and each horizon, compute the mean forward return where that pattern is True. """ metrics = {} for pattern_col in pattern_states.columns: # bullish, bearish, flat mask = pattern_states[pattern_col].astype(bool) for fwd_col in forward_returns.columns: # fwd_1, fwd_3, ... key = f"{pattern_col}_{fwd_col}_mean" if mask.any(): metrics[key] = forward_returns.loc[mask, fwd_col].mean() else: metrics[key] = np.nan return metrics # --------------------------------------------------------------- # 2) Indicator Specification: Compute function + Pattern rules # --------------------------------------------------------------- @dataclass class IndicatorSpec: name: str compute: Callable[[pd.DataFrame], pd.Series] patterns: Callable[[pd.DataFrame, pd.Series], pd.DataFrame] # patterns returns a DataFrame with columns: bullish, bearish, flat # --------------------------------------------------------------- # 3) Quarterly Evaluation Engine: Apply indicator to one quarter # --------------------------------------------------------------- def evaluate_indicator(df_q: pd.DataFrame, spec: IndicatorSpec, horizons=(1, 3, 6)): indicator_series = spec.compute(df_q) pattern_states = spec.patterns(df_q, indicator_series) forward_returns = compute_forward_returns(df_q, horizons) metrics = score_indicator(pattern_states, forward_returns) metrics["indicator"] = spec.name return metrics # --------------------------------------------------------------- # 4) Pipeline Coordinator: Apply all indicators across all quarters # --------------------------------------------------------------- class IndicatorPipeline: def __init__(self, specs: Dict[str, IndicatorSpec], horizons=(1, 3, 6)): self.specs = specs self.horizons = horizons def run(self, df: pd.DataFrame): df = df.copy() # df.index must be a DatetimeIndex df["quarter"] = df.index.to_period("Q") results = [] for q, df_q in df.groupby("quarter"): for spec in self.specs.values(): res = evaluate_indicator(df_q, spec, self.horizons) res["quarter"] = str(q) results.append(res) return pd.DataFrame(results) # --------------------------------------------------------------- # 5) Example Indicator: SMA-20 Trend (bullish / bearish / flat) # --------------------------------------------------------------- def compute_sma20(df: pd.DataFrame) -> pd.Series: """ 20-period simple moving average of 'close'. """ return df["close"].rolling(window=20, min_periods=1).mean() def sma20_patterns(df: pd.DataFrame, sma: pd.Series) -> pd.DataFrame: """ Define pattern states based on where price sits vs SMA. - bullish: close > sma - bearish: close < sma - flat: close == sma (or within tiny epsilon) """ close = df["close"] eps = 1e-8 bullish = close > sma * (1 + eps) bearish = close < sma * (1 - eps) flat = ~(bullish | bearish) return pd.DataFrame( { "bullish": bullish, "bearish": bearish, "flat": flat, }, index=df.index, ) sma20_spec = IndicatorSpec( name="sma20_trend", compute=compute_sma20, patterns=sma20_patterns, ) # --------------------------------------------------------------- # 6) Demo: Make fake OHLC data and run the pipeline # --------------------------------------------------------------- if __name__ == "__main__": # ------------------------------------------------------------ # Initialize MetaTrader 5 # ------------------------------------------------------------ if not mt5.initialize(login=XXXXXXXX, server="XXXXXXX", password="XXXXX"): print("initialize() failed, error code =", mt5.last_error()) quit() # ------------------------------------------------------------ # Set start and end dates for history data # ------------------------------------------------------------ end_date = datetime(2025, 12, 1, 0) start_date = datetime(2020, 1, 1, 0) # Get H4 rates for FXI (CFD symbol "#FXI") fxi_rates = mt5.copy_rates_range("#FXI", mt5.TIMEFRAME_H4, start_date, end_date) if fxi_rates is None or len(fxi_rates) == 0: print("No data returned from MT5.") mt5.shutdown() quit() # ------------------------------------------------------------ # Build base DataFrame # ------------------------------------------------------------ df = pd.DataFrame(fxi_rates) # MT5 times are in seconds since epoch df["time"] = pd.to_datetime(df["time"], unit="s") df.set_index("time", inplace=True) df = df.sort_index() # Confirm essential OHLC columns exist required_cols = {"open", "high", "low", "close"} if not required_cols.issubset(df.columns): missing = required_cols - set(df.columns) raise ValueError(f"Dataset missing required columns: {missing}") # Build pipeline with one or more indicators specs = { "sma20": sma20_spec, # you can add more IndicatorSpec entries here } pipeline = IndicatorPipeline(specs=specs, horizons=(1, 3, 5)) results = pipeline.run(df) # Show the per-quarter stats pd.set_option("display.width", 200) pd.set_option("display.max_columns", None) print(results)
Это не полная реализация, а концептуальный каркас, на основе которого будут строиться следующие разделы. Функциональные компоненты данного пайплайна можно обобщить следующим образом, как показано в этой таблице.
| Название компонента | Роль в пайплайне | Выходные данные |
|---|---|---|
| Спецификация индикатора | Определяет, как рассчитывается индикатор и как формируются паттерны | Датафрейм состояний паттернов |
| Движок поквартальной оценки | Оценивает паттерны индикаторов с использованием метрик форвардной доходности | Доля успешных сигналов, средняя доходность, оценки индикаторов |
| Координатор пайплайна | Выполняет все индикаторы по всем кварталам | Единый набор данных с квартальными результатами |
| Движок ранжирования | Сортирует индикаторы по эффективности за каждый квартал | Список рекомендуемых индикаторов для каждого квартала |
Теперь, когда это сделано, мы можем перейти к следующему этапу, на котором формально определим используемые индикаторы, представим логику паттернов и зададим порядок расчёта оценочных метрик — всё это на основе набора данных FXI за несколько кварталов.
Уровень спецификации индикаторов
При оценке индикаторов нам необходим явный механизм, определяющий внутреннюю логику каждого индикатора. Эта логика состоит из двух неразделимых компонентов. Математические вычисления, которые дают числовые состояния индикатора, и детерминированные правила паттернов для классификации каждого паттерна как бычьего, медвежьего или нейтрального. Точность и согласованность этого уровня определяют качество всего последующего анализа, поскольку определённые состояния напрямую поступают в оценки форвардной доходности. В конечном итоге это влияет на поквартальные рейтинги индикаторов.
Чтобы обеспечить единообразный анализ разнообразного набора индикаторов — трендовых, осцилляторных или структурных, — уровень спецификации построен как формализованный объект, содержащий как вычислительные функции, так и функции вывода паттернов. Благодаря такой инкапсуляции мы предотвращаем перекрёстное смешение логики индикаторов и процедур оценки, обеспечивая математическую автономность индикаторов. Это также обеспечивает расширяемость, поскольку дополнительные индикаторы можно добавлять в пайплайн, определяя единый объект спецификации без внесения изменений в остальную часть системы.
Таким образом, данный уровень спецификации представляет собой основополагающий «контракт», связывающий вычисления с логикой интерпретации. Каждый индикатор отвечает за формирование собственного числового ряда и классификацию состояний паттернов. Со своей стороны, пайплайн отвечает за оценку этих состояний. Мы реализуем это на Python следующим образом.
# python libraries import MetaTrader5 as mt5 from dataclasses import dataclass from typing import Callable, Dict, Tuple import pandas as pd import numpy as np from datetime import datetime, time from pandas.tseries.holiday import USFederalHolidayCalendar # --------------------------------------------------------------- # Your IndicatorSpec + RSI Implementation # --------------------------------------------------------------- @dataclass class IndicatorSpec: name: str compute: Callable[[pd.DataFrame], pd.Series] patterns: Callable[[pd.DataFrame, pd.Series], pd.DataFrame] def compute_rsi(df: pd.DataFrame, period: int = 14) -> pd.Series: delta = df["close"].diff() gain = delta.clip(lower=0).rolling(period).mean() loss = (-delta.clip(upper=0)).rolling(period).mean() rs = gain / (loss + 1e-9) return 100 - (100 / (1 + rs)) def rsi_patterns(df: pd.DataFrame, rsi: pd.Series) -> pd.DataFrame: bullish = (rsi < 30) bearish = (rsi > 70) flat = ~(bullish | bearish) return pd.DataFrame({ "bullish": bullish.astype(int), "bearish": bearish.astype(int), "flat": flat.astype(int), }) rsi_14_spec = IndicatorSpec( name="RSI_14", compute=lambda df: compute_rsi(df, 14), patterns=rsi_patterns ) # --------------------------------------------------------------- # Helper Functions for Returns & Evaluation # --------------------------------------------------------------- def compute_forward_returns(df: pd.DataFrame, horizons=(1, 3, 6)): out = {} close = df["close"] for h in horizons: out[f"fwd_{h}"] = close.shift(-h) / close - 1 return pd.DataFrame(out) def score_indicator(patterns: pd.DataFrame, forward_returns: pd.DataFrame) -> Dict[str, float]: metrics = {} for p in patterns.columns: mask = patterns[p] == 1 for fwd in forward_returns.columns: key = f"{p}_{fwd}_mean" if mask.any(): metrics[key] = forward_returns.loc[mask, fwd].mean() else: metrics[key] = 0# np.nan return metrics # --------------------------------------------------------------- # Evaluation Engine # --------------------------------------------------------------- def evaluate_indicator(df_q: pd.DataFrame, spec: IndicatorSpec, horizons=(1, 3, 6)): indicator_series = spec.compute(df_q) pattern_states = spec.patterns(df_q, indicator_series) forward_returns = compute_forward_returns(df_q, horizons) metrics = score_indicator(pattern_states, forward_returns) metrics["indicator"] = spec.name return metrics # --------------------------------------------------------------- # Pipeline Coordinator # --------------------------------------------------------------- class IndicatorPipeline: def __init__(self, specs: Dict[str, IndicatorSpec], horizons=(1, 3, 6)): self.specs = specs self.horizons = horizons def run(self, df: pd.DataFrame): df = df.copy() df["quarter"] = df.index.to_period("Q") results = [] for q, df_q in df.groupby("quarter"): for spec in self.specs.values(): res = evaluate_indicator(df_q, spec, self.horizons) res["quarter"] = str(q) results.append(res) return pd.DataFrame(results) # --------------------------------------------------------------- # DEMO: Run the pipeline with synthetic data # --------------------------------------------------------------- if __name__ == "__main__": # ------------------------------------------------------------ # Initialize MetaTrader 5 # ------------------------------------------------------------ if not mt5.initialize(login=XXXXX, server="XXXXXXXXX", password="XXXXXXX"): print("initialize() failed, error code =", mt5.last_error()) quit() # ------------------------------------------------------------ # Set start and end dates for history data # ------------------------------------------------------------ end_date = datetime(2025, 12, 1, 0) start_date = datetime(2020, 1, 1, 0) # Get H4 rates for FXI (CFD symbol "#FXI") fxi_rates = mt5.copy_rates_range("#FXI", mt5.TIMEFRAME_H4, start_date, end_date) if fxi_rates is None or len(fxi_rates) == 0: print("No data returned from MT5.") mt5.shutdown() quit() # ------------------------------------------------------------ # Build base DataFrame # ------------------------------------------------------------ df = pd.DataFrame(fxi_rates) # MT5 times are in seconds since epoch df["time"] = pd.to_datetime(df["time"], unit="s") df.set_index("time", inplace=True) df = df.sort_index() # Confirm essential OHLC columns exist required_cols = {"open", "high", "low", "close"} if not required_cols.issubset(df.columns): missing = required_cols - set(df.columns) raise ValueError(f"Dataset missing required columns: {missing}") specs = {"RSI14": rsi_14_spec} pipeline = IndicatorPipeline(specs=specs, horizons=(1, 3, 5)) result = pipeline.run(df) print(result)
Приведённая выше иллюстрация с использованием RSI служит шаблоном для других индикаторов, позволяя создать согласованный, модульный и формально определённый фреймворк, объединяющий более широкий пайплайн оценки. Ниже приведена таблица с кратким описанием требований спецификации, необходимых для нашего пайплайна.
| Компонент спецификации | Описание | Требуемые выходные данные |
|---|---|---|
| name | уникальный идентификатор индикатора | строка |
| compute() | функция, формирующая ряд индикатора на основе данных OHLC | pd.Series, выровненная по индексу набора данных |
| patterns() | функция, определяющая бычьи, медвежьи и нейтральные состояния | pd.DataFrame с 3 бинарными столбцами |
| Модульность | Изолированная и многократно используемая логика индикатора | Единообразно для всех индикаторов |
| Расширяемость | Чтобы добавить новые индикаторы, достаточно добавить новый объект | Изменения пайплайна не требуются |
Логика паттернов для индикаторов
Фреймворк для оценки индикаторов должен задавать состояния паттернов в формате, который является детерминированным и единообразным для всего набора рассматриваемых индикаторов. Логика паттернов часто служит связующим звеном между исходными значениями и практическими сигналами, которые пайплайн оценивает с помощью анализа форвардной доходности. Без структурированного определения паттернов индикатор будет выдавать результаты, которые по-прежнему имеют числовой смысл, но с практической точки зрения остаются неоднозначными. Это затруднило бы оценку.
Построение паттерна должно учитывать специфику конкретного индикатора. Осцилляторы моментума обычно опираются на интерпретации, связанные с пороговыми уровнями; трендовые индикаторы, в свою очередь, требуют оценки направленности наклона или некоторой формы сравнения пересечений; при этом индикаторы на основе волатильности обычно зависят от расширения или сужения полос. В некоторых случаях, особенно когда индикаторы изначально не предназначены для подачи направленных сигналов, необходимо учитывать ценовое движение, чтобы с учётом структурных условий сформировать бычью или медвежью интерпретацию. Цель здесь состоит в том, чтобы сформировать согласованное представление состояний по всем индикаторам и тем самым упростить интерпретацию результатов движка поквартальной оценки.
Для каждого индикатора логика паттернов должна соответствовать трём важным принципам.
- Явный детерминизм — чтобы исключить неоднозначность классификации
- Временная когерентность, то есть сигналы должны следовать в точном хронологическом порядке исходных ценовых данных
- Совместимость — чтобы каждый паттерн выдавал булевы или двоичные состояния в формате, указывающем на бычий, медвежий или нейтральный прогноз.
Таким образом, полученная матрица состояний паттернов становится основой, на которой пайплайн оценивает качество прогнозов. Мы приводим следующие примеры, охватывающие три широкие категории индикаторов. Осциллятор моментума — RSI; трендовый индикатор — MACD; и индикатор волатильности — Полосы Боллинджера, дополненные некоторым смещением на основе ценового движения. Этот код призван показать, как разные типы индикаторов можно объединить в единую схему «бычий/медвежий/нейтральный», необходимую нам для проведения оценок.
# python libraries import MetaTrader5 as mt5 from dataclasses import dataclass from typing import Callable, Dict, Tuple import pandas as pd import numpy as np from datetime import datetime, time from pandas.tseries.holiday import USFederalHolidayCalendar # ------------------------------------------------------------------------- # Import your full indicator library (CITED) # ------------------------------------------------------------------------- from IndicatorsAll import ( RSI, MACD, Bollinger_Bands, ADX_Wilder, FRAMA, Parabolic_SAR, VIDYA, TRIX, Alligator, Gator_Oscillator, Awesome_Oscillator, Bill_Williams_MFI, ATR, StdDev, CCI, Accelerator_Oscillator, Bill_Williams_Fractals, Williams_R, Ichimoku, Envelopes ) # ------------------------------------------------------------------------- # 1. Pattern Logic Layer (Your Provided Logic) # ------------------------------------------------------------------------- def rsi_patterns(df: pd.DataFrame, rsi: pd.Series) -> pd.DataFrame: bullish = (rsi < 30).astype(int) bearish = (rsi > 70).astype(int) flat = ((rsi >= 30) & (rsi <= 70)).astype(int) return pd.DataFrame({"bullish": bullish, "bearish": bearish, "flat": flat}) def macd_patterns(df: pd.DataFrame, macd_hist: pd.Series) -> pd.DataFrame: bullish = (macd_hist > 0).astype(int) bearish = (macd_hist < 0).astype(int) flat = (macd_hist == 0).astype(int) return pd.DataFrame({"bullish": bullish, "bearish": bearish, "flat": flat}) def bollinger_patterns(df: pd.DataFrame, bb_width: pd.Series) -> pd.DataFrame: price = df["close"] slope = price.diff() contraction = (bb_width < bb_width.rolling(20).mean()).astype(int) bullish = ((contraction == 0) & (slope > 0)).astype(int) bearish = ((contraction == 0) & (slope < 0)).astype(int) flat = (contraction == 1).astype(int) return pd.DataFrame({"bullish": bullish, "bearish": bearish, "flat": flat}) # ------------------------------------------------------------------------- # 2. Quarterly Evaluation Engine # ------------------------------------------------------------------------- def compute_forward_returns(df: pd.DataFrame, horizon: int = 3): return df["close"].shift(-horizon) / df["close"] - 1 def evaluate_indicator(patterns: pd.DataFrame, forward: pd.Series) -> dict: results = {} for state in ["bullish", "bearish", "flat"]: mask = patterns[state] == 1 if mask.sum() == 0: results[state] = {"count": 0, "avg_return": 0.0} else: results[state] = { "count": int(mask.sum()), "avg_return": forward[mask].mean() } return results # ------------------------------------------------------------------------- # 3. Ranking Layer # ------------------------------------------------------------------------- def score_indicator(results: dict) -> float: bull = results["bullish"]["avg_return"] bear = -results["bearish"]["avg_return"] flat = -0.2 * abs(results["flat"]["avg_return"]) return bull + bear + flat # ------------------------------------------------------------------------- # 4. Main Pipeline Demonstration # ------------------------------------------------------------------------- def main(): # ------------------------------------------------------------ # Initialize MetaTrader 5 # ------------------------------------------------------------ if not mt5.initialize(login=XXXXX, server="XXXXXXXX", password="XXX"): print("initialize() failed, error code =", mt5.last_error()) quit() # ------------------------------------------------------------ # Set start and end dates for history data # ------------------------------------------------------------ end_date = datetime(2025, 12, 1, 0) start_date = datetime(2020, 1, 1, 0) # Get H4 rates for FXI (CFD symbol "#FXI") fxi_rates = mt5.copy_rates_range("#FXI", mt5.TIMEFRAME_H4, start_date, end_date) if fxi_rates is None or len(fxi_rates) == 0: print("No data returned from MT5.") mt5.shutdown() quit() # ------------------------------------------------------------ # Build base DataFrame # ------------------------------------------------------------ df = pd.DataFrame(fxi_rates) # MT5 times are in seconds since epoch df["time"] = pd.to_datetime(df["time"], unit="s") df.set_index("time", inplace=True) df = df.sort_index() # Confirm essential OHLC columns exist required_cols = {"open", "high", "low", "close"} if not required_cols.issubset(df.columns): missing = required_cols - set(df.columns) raise ValueError(f"Dataset missing required columns: {missing}") # --------------------------------------------------- # Compute indicators USING YOUR MODULE (cited) # --------------------------------------------------- df_rsi = RSI(df) df_macd = MACD(df) df_bb = Bollinger_Bands(df) # Create final working copy df_all = df.copy() df_all["RSI"] = df_rsi["RSI"] df_all["MACD_hist"] = df_macd["MACD_hist"] df_all["BB_width"] = df_bb["BB_width"] # --------------------------------------------------- # Pattern Logic from imported indicators # --------------------------------------------------- pat_rsi = rsi_patterns(df_all, df_all["RSI"]) pat_macd = macd_patterns(df_all, df_all["MACD_hist"]) pat_bb = bollinger_patterns(df_all, df_all["BB_width"]) # --------------------------------------------------- # Evaluation Layer # --------------------------------------------------- forward3 = compute_forward_returns(df_all, horizon=3) eval_rsi = evaluate_indicator(pat_rsi, forward3) eval_macd = evaluate_indicator(pat_macd, forward3) eval_bb = evaluate_indicator(pat_bb, forward3) # --------------------------------------------------- # Ranking Layer # --------------------------------------------------- scores = { "RSI": score_indicator(eval_rsi), "MACD_Hist": score_indicator(eval_macd), "BB_Width": score_indicator(eval_bb), } ranking = sorted(scores.items(), key=lambda x: x[1], reverse=True) print("\n========== Indicator Ranking ==========\n") for name, score in ranking: print(f"{name:12s} → Score: {score:.4f}") print("\n========== Detailed Evaluations ==========\n") print("RSI:", eval_rsi) print("MACD:", eval_macd) print("BB Width:", eval_bb) # ------------------------------------------------------------------------- # Run Script # ------------------------------------------------------------------------- if __name__ == "__main__": main()
Форвардная доходность и горизонты оценки
Расчет форвардных доходностей является основой этой системы оценки индикаторов. Этот пайплайн не может определить, обладают ли состояния паттернов для лонга или шорта какой-либо прогностической ценностью, если каждый сигнал не сопоставляется напрямую с последующим движением цены. Форвардные доходности позволяют установить эту связь, показывая, насколько изменяется цена после срабатывания паттерна. Кроме того, мы учитываем разные прогнозные горизонты, чтобы система охватывала как краткосрочные, так и среднесрочные эффекты; в результате ни один индикатор не получает необоснованного преимущества и не штрафуется несправедливо из-за оценки только с одной временной перспективы.

С формальной точки зрения, форвардная доходность на горизонте h измеряет изменение в процентах между ценой закрытия в момент времени t и ценой закрытия в момент времени t + h. Эти метрики всегда следует рассчитывать без смещения из-за заглядывания в будущее, опираясь исключительно на будущие наблюдения относительно момента появления сигнала. После их определения матрица форвардных доходностей станет эталонным набором данных, относительно которого будут оцениваться все сигнальные паттерны для лонга, шорта и нейтрального состояния.
В данном контексте для оценки мы выбрали интервалы в 1, 3 и 6 баров. Они призваны обеспечить сбалансированную основу для немедленного подтверждения, краткосрочного продолжения движения и среднесрочной устойчивости. Эти горизонты гарантируют, что даже при наличии индикаторов с разной частотой срабатывания у нас сохраняется основа для их сравнения. Например, моментум-осцилляторы часто показывают хорошие результаты на коротких горизонтах, тогда как трендовые индикаторы, как правило, эффективны на более длинных горизонтах. С другой стороны, индикаторы волатильности, как правило, нейтральны к временному горизонту, поскольку они ориентированы на фазы расширения и сжатия рынка. Таким образом, значения форвардной доходности являются важным опорным элементом, позволяющим проводить объективное оценивание. Для вычисления этих форвардных доходностей в Python используется следующий код:
# python libraries import MetaTrader5 as mt5 from dataclasses import dataclass from typing import Callable, Dict, Tuple import pandas as pd import numpy as np from datetime import datetime, time from pandas.tseries.holiday import USFederalHolidayCalendar # ------------------------------------------------------------------- # Forward return calculator (your function) # ------------------------------------------------------------------- def compute_forward_returns(df: pd.DataFrame, horizons=(1, 3, 6)) -> pd.DataFrame: close = df["close"] forward_data = {} for h in horizons: forward_data[f"fwd_ret_{h}"] = (close.shift(-h) / close) - 1.0 return pd.DataFrame(forward_data, index=df.index) # ------------------------------------------------------------------- # Simple pattern definition for demonstration # (e.g., bullish when price is rising, bearish when falling) # ------------------------------------------------------------------- def simple_price_patterns(df: pd.DataFrame) -> pd.DataFrame: slope = df["close"].diff() bullish = (slope > 0).astype(int) bearish = (slope < 0).astype(int) flat = (slope == 0).astype(int) return pd.DataFrame({ "bullish": bullish, "bearish": bearish, "flat": flat }) # ------------------------------------------------------------------- # Evaluation: Compare pattern states with forward returns # ------------------------------------------------------------------- def evaluate_patterns(patterns: pd.DataFrame, forward: pd.DataFrame) -> dict: results = {} for state in ["bullish", "bearish", "flat"]: mask = patterns[state] == 1 state_results = {} for col in forward.columns: if mask.sum() == 0: state_results[col] = np.nan else: state_results[col] = forward.loc[mask, col].mean() results[state] = state_results return results # ------------------------------------------------------------------- # MAIN EXAMPLE: Demonstrate usage of forward-return calculator # ------------------------------------------------------------------- def main(): # ------------------------------------------------------------ # Initialize MetaTrader 5 # ------------------------------------------------------------ if not mt5.initialize(login=XXXXX, server="XXX", password="XXXXXXXX"): print("initialize() failed, error code =", mt5.last_error()) quit() # ------------------------------------------------------------ # Set start and end dates for history data # ------------------------------------------------------------ end_date = datetime(2025, 12, 1, 0) start_date = datetime(2020, 1, 1, 0) # Get H4 rates for FXI (CFD symbol "#FXI") fxi_rates = mt5.copy_rates_range("#FXI", mt5.TIMEFRAME_H4, start_date, end_date) if fxi_rates is None or len(fxi_rates) == 0: print("No data returned from MT5.") mt5.shutdown() quit() # ------------------------------------------------------------ # Build base DataFrame # ------------------------------------------------------------ df = pd.DataFrame(fxi_rates) # MT5 times are in seconds since epoch df["time"] = pd.to_datetime(df["time"], unit="s") df.set_index("time", inplace=True) df = df.sort_index() # Confirm essential OHLC columns exist required_cols = {"open", "high", "low", "close"} if not required_cols.issubset(df.columns): missing = required_cols - set(df.columns) raise ValueError(f"Dataset missing required columns: {missing}") # ----------------------------------------- # Compute forward returns (horizons 1,3,6) # ----------------------------------------- fwd = compute_forward_returns(df, horizons=(1, 3, 6)) print("\n=== Forward Returns (Example) ===\n") print(fwd.head()) # ----------------------------------------- # Compute simple pattern states # (your real pipeline uses indicator patterns) # ----------------------------------------- patterns = simple_price_patterns(df) # ----------------------------------------- # Evaluate patterns against forward returns # ----------------------------------------- evaluation = evaluate_patterns(patterns, fwd) print("\n=== Pattern–Forward Return Evaluation ===\n") for state, metrics in evaluation.items(): print(f"{state.upper()}:") for horizon, value in metrics.items(): print(f" {horizon}: {value:.6f}") print() # ------------------------------------------------------------------- # Run example # ------------------------------------------------------------------- if __name__ == "__main__": main()
Если мы запустим приведенный выше демонстрационный скрипт, то получим следующие выходные данные. Они представлены ниже в виде таблицы.
| Бычий | |
|---|---|
| fwd-ret-1 | -0,000209 |
| fwd-ret-3 | -0,000181 |
| fwd-ret-6 | -0,000068 |
| Медвежий | |
|---|---|
| fwd-ret-1 | 0,000325 |
| fwd-ret-3 | 0,000640 |
| fwd-ret-6 | 0,000998 |
| Нейтральный | |
|---|---|
| fwd-ret-1 | 0,000896 |
| fwd-ret-3 | 0,001127 |
| fwd-ret-6 | 0,001660 |
Эти выходные данные, представленные в таблице выше, призваны показать базовую структуру, которую движок скоринга будет использовать для оценки прогнозной значимости каждого состояния паттерна. Например, в случае бычьего паттерна мы оцениваем его, измеряя долю положительных значений форвардной доходности по трём временным горизонтам. Медвежьи паттерны проверяются относительно отрицательной форвардной доходности, тогда как нейтральные паттерны оцениваются с учётом абсолютной величины доходности.
Оценка индикаторов
После того как у нас есть состояния паттернов и соответствующие им форвардные доходности, нам необходимо построить формальный метод оценки, который количественно измеряет способность каждого индикатора прогнозировать в рамках заданного квартала. Этот механизм позволит преобразовать качественные суждения, такие как «этот индикатор, по-видимому, надёжен», в более явные числовые характеристики, что даст возможность объективно сравнивать несколько индикаторов.
Задача подсчёта баллов формулируется следующим образом. Во-первых, для каждого индикатора и каждого квартального сегмента фреймворк должен оценивать, насколько хорошо его состояния паттерна — бычье, медвежье и нейтральное — согласуются с последующим поведением цены. Мы проводим эту оценку, сравнивая матрицу состояний паттернов, то есть набор двоичных сигналов, с матрицей форвардной доходности, то есть набором реализованных доходностей на нескольких временных горизонтах. В результате мы получаем набор статистик эффективности, обобщающих точность каждого индикатора в прогнозировании направления, среднюю прибыльность и то, как часто генерируются сигналы, то есть частоту сигналов.
В результате мы получаем совокупный балл, который формируется путем контролируемого объединения этих трёх метрик. Например, наш пайплайн может придавать больший вес доле успешных сигналов/показателю точности, одновременно учитывая величину средней доходности как вторичную корректировку с меньшим весом. Это позволяет обеспечить, чтобы индикаторы получали более высокую оценку как за стабильность, так и за прибыльность. При этом это не будет стимулировать переобучение. Если мы остановимся на выбранных выше трёх индикаторах и продолжим использовать наш уже написанный модуль паттернов индикаторов под названием IndicatorsAll.py, то можем реализовать систему оценки следующим образом.
# python libraries import MetaTrader5 as mt5 from dataclasses import dataclass from typing import Callable, Dict, Tuple import pandas as pd import numpy as np from datetime import datetime, time from pandas.tseries.holiday import USFederalHolidayCalendar # --------------------------------------------------------- # Import indicators from your module # --------------------------------------------------------- from IndicatorsAll import RSI, MACD, Bollinger_Bands # :contentReference[oaicite:1]{index=1} # --------------------------------------------------------- # Indicator scoring function (as given) # --------------------------------------------------------- def score_indicator(patterns: pd.DataFrame, fwd_rets: pd.DataFrame, min_signals: int = 10) -> dict: """ Compute predictive power metrics for one indicator within a single quarter. patterns: DataFrame with columns ['bullish', 'bearish', 'flat'] (0/1 or bool) fwd_rets: DataFrame with columns such as ['fwd_ret_1', 'fwd_ret_3', ...] """ results = {} for state in ["bullish", "bearish"]: mask = patterns[state] == 1 num_signals = int(mask.sum()) results[f"{state}_count"] = num_signals if num_signals < min_signals: results[f"{state}_hit_rate"] = np.nan results[f"{state}_mean_ret"] = np.nan continue expected_sign = 1 if state == "bullish" else -1 state_hits = [] state_mean_rets = [] for col in fwd_rets.columns: r = fwd_rets.loc[mask, col].dropna() if r.empty: continue hits = (np.sign(r) == expected_sign).astype(float).mean() state_hits.append(hits) state_mean_rets.append(r.mean()) if state_hits: results[f"{state}_hit_rate"] = float(np.mean(state_hits)) results[f"{state}_mean_ret"] = float(np.mean(state_mean_rets)) else: results[f"{state}_hit_rate"] = np.nan results[f"{state}_mean_ret"] = np.nan bullish_hr = results.get("bullish_hit_rate", np.nan) bearish_hr = results.get("bearish_hit_rate", np.nan) bullish_ret = results.get("bullish_mean_ret", 0.0) bearish_ret = results.get("bearish_mean_ret", 0.0) valid_hit_rates = [x for x in [bullish_hr, bearish_hr] if not np.isnan(x)] hit_component = float(np.mean(valid_hit_rates)) if valid_hit_rates else 0.0 magnitude_component = float((abs(bullish_ret) + abs(bearish_ret)) / 2.0) final_score = hit_component + 0.5 * magnitude_component results["score"] = final_score return results # --------------------------------------------------------- # Forward returns from CLOSE ONLY (no indicator info) # --------------------------------------------------------- def compute_forward_returns(df: pd.DataFrame, horizons=(1, 3, 6)) -> pd.DataFrame: close = df["close"] forward_data = {} for h in horizons: forward_data[f"fwd_ret_{h}"] = (close.shift(-h) / close) - 1.0 return pd.DataFrame(forward_data, index=df.index) # --------------------------------------------------------- # Pattern logic USING REAL INDICATOR VALUES # --------------------------------------------------------- def rsi_patterns(df: pd.DataFrame, rsi: pd.Series) -> pd.DataFrame: bullish = (rsi < 30).astype(int) bearish = (rsi > 70).astype(int) flat = ((rsi >= 30) & (rsi <= 70)).astype(int) return pd.DataFrame({"bullish": bullish, "bearish": bearish, "flat": flat}) def macd_patterns(df: pd.DataFrame, macd_hist: pd.Series) -> pd.DataFrame: bullish = (macd_hist > 0).astype(int) bearish = (macd_hist < 0).astype(int) flat = (macd_hist == 0).astype(int) return pd.DataFrame({"bullish": bullish, "bearish": bearish, "flat": flat}) def bollinger_patterns(df: pd.DataFrame, bb_width: pd.Series) -> pd.DataFrame: price = df["close"] price_slope = price.diff() contraction = (bb_width < bb_width.rolling(20).mean()).astype(int) bullish = ((contraction == 0) & (price_slope > 0)).astype(int) bearish = ((contraction == 0) & (price_slope < 0)).astype(int) flat = (contraction == 1).astype(int) return pd.DataFrame({"bullish": bullish, "bearish": bearish, "flat": flat}) # --------------------------------------------------------- # MAIN – Full example with RSI, MACD, Bollinger Bands # --------------------------------------------------------- def main(): # ------------------------------------------------------------ # Initialize MetaTrader 5 # ------------------------------------------------------------ if not mt5.initialize(login=XXXXXXXX, server="XXXXX", password="XXX"): print("initialize() failed, error code =", mt5.last_error()) quit() # ------------------------------------------------------------ # Set start and end dates for history data # ------------------------------------------------------------ end_date = datetime(2025, 12, 1, 0) start_date = datetime(2020, 1, 1, 0) # Get H4 rates for FXI (CFD symbol "#FXI") fxi_rates = mt5.copy_rates_range("#FXI", mt5.TIMEFRAME_H4, start_date, end_date) if fxi_rates is None or len(fxi_rates) == 0: print("No data returned from MT5.") mt5.shutdown() quit() # ------------------------------------------------------------ # Build base DataFrame # ------------------------------------------------------------ df = pd.DataFrame(fxi_rates) # MT5 times are in seconds since epoch df["time"] = pd.to_datetime(df["time"], unit="s") df.set_index("time", inplace=True) df = df.sort_index() # Confirm essential OHLC columns exist required_cols = {"open", "high", "low", "close"} if not required_cols.issubset(df.columns): missing = required_cols - set(df.columns) raise ValueError(f"Dataset missing required columns: {missing}") # ------------------------------------------------------------ # Check interval consistency and resample to uniform 4h grid # ------------------------------------------------------------ time_diff = df.index.to_series().diff().dropna() irregularities = time_diff[time_diff != pd.Timedelta(hours=4)] print("Irregular intervals detected:\n", irregularities.head()) # Use lowercase "h" to avoid FutureWarning df_resampled = df.resample("4h").ffill() # ------------------------------------------------------------ # Attach quarterly segmentation # ------------------------------------------------------------ df_resampled["quarter"] = df_resampled.index.to_period("Q") # ------------------------------------------------------------ # Market-hours mask + U.S. holidays detection # ------------------------------------------------------------ # Assume MT5 timestamps are in UTC. If your server is NOT UTC, # adjust this localize step accordingly. idx_utc = df_resampled.index.tz_localize("UTC") # Convert to New York time (U.S. Eastern, with DST handled) idx_ny = idx_utc.tz_convert("America/New_York") # Weekday mask (Mon–Fri) is_us_weekday = idx_ny.weekday < 5 # 0=Mon, 4=Fri # Session time mask (09:30–16:00 New York time) ny_times = idx_ny.time session_start = time(9, 30) session_end = time(16, 0) is_session_time = np.array( [(t >= session_start) and (t <= session_end) for t in ny_times], dtype=bool ) # U.S. holiday calendar (federal holidays; close approximation) cal = USFederalHolidayCalendar() holidays = cal.holidays( start=idx_ny.min().date(), end=idx_ny.max().date() ) # Normalize times to dates and check membership in holiday list is_holiday = pd.Series(idx_ny.normalize()).isin(holidays).to_numpy() # Final market mask: weekday, in session, not holiday market_mask = is_us_weekday & is_session_time & (~is_holiday) # Apply mask df_market = df_resampled[market_mask].copy() # 2) Compute real indicators from IndicatorsAll df_rsi = RSI(df_market) # adds 'RSI' df_macd = MACD(df_market) # adds 'MACD_hist' df_bb = Bollinger_Bands(df_market) # adds 'BB_width' df_all = df_market.copy() df_all["RSI"] = df_rsi["RSI"] df_all["MACD_hist"] = df_macd["MACD_hist"] df_all["BB_width"] = df_bb["BB_width"] # 3) Build pattern states from real indicator values pat_rsi = rsi_patterns(df_all, df_all["RSI"]) pat_macd = macd_patterns(df_all, df_all["MACD_hist"]) pat_bb = bollinger_patterns(df_all, df_all["BB_width"]) # 4) Forward returns from close only fwd_rets = compute_forward_returns(df_all, horizons=(1, 3, 6)) # 5) Score each indicator rsi_score = score_indicator(pat_rsi, fwd_rets) macd_score = score_indicator(pat_macd, fwd_rets) bb_score = score_indicator(pat_bb, fwd_rets) # 6) Ranking scores = { "RSI": rsi_score["score"], "MACD_Hist": macd_score["score"], "BB_Width": bb_score["score"], } ranking = sorted(scores.items(), key=lambda x: x[1], reverse=True) print("\n=== Indicator Ranking (Synthetic Demo) ===\n") for name, score in ranking: print(f"{name:10s} → Score: {score:.6f}") print("\n=== Detailed RSI Score ===") print(rsi_score) print("\n=== Detailed MACD Score ===") print(macd_score) print("\n=== Detailed Bollinger Score ===") print(bb_score) if __name__ == "__main__": main()
При запуске приведённого выше скрипта мы получаем следующий вывод
Irregular intervals detected: time 2020-05-14 12:00:00 0 days 20:00:00 2020-05-15 12:00:00 0 days 20:00:00 2020-05-18 12:00:00 2 days 20:00:00 2020-05-19 12:00:00 0 days 20:00:00 2020-05-20 12:00:00 0 days 20:00:00 Name: time, dtype: timedelta64[ns] === Indicator Ranking (Synthetic Demo) === RSI → Score: 0.485652 MACD_Hist → Score: 0.427450 BB_Width → Score: 0.382466 === Detailed RSI Score === {'bullish_count': 271, 'bullish_hit_rate': 0.5006150061500615, 'bullish_mean_ret': 0.003797074603388901, 'bearish_count': 331, 'bearish_hit_rate': 0.46827794561933533, 'bearish_mean_ret': -0.0010230853860337735, 'score': 0.4856515158820541} === Detailed MACD Score === {'bullish_count': 1434, 'bullish_hit_rate': 0.41668801974563136, 'bullish_mean_ret': 0.00020654619989694508, 'bearish_count': 1460, 'bearish_hit_rate': 0.4378995433789954, 'bearish_mean_ret': 0.000417578535087511, 'score': 0.4274498127460595} === Detailed Bollinger Score === {'bullish_count': 434, 'bullish_hit_rate': 0.37634408602150543, 'bullish_mean_ret': -0.0006985368250969227, 'bearish_count': 458, 'bearish_hit_rate': 0.38756175169369245, 'bearish_mean_ret': 0.001353899613706402, 'score': 0.3824660279672998}
Ранжирование и выбор индикаторов по кварталам
У нас есть все индикаторы с их оценками квартальной прогнозной эффективности; далее следует диагностический этап, на котором мы преобразуем эти исходные метрики в структурированные поквартальные рейтинги. Ранжирование играет важную роль в определении эффективности индикаторов, опираясь на полученные выше результаты эмпирической оценки, а не на субъективное мнение. Эта иерархия, в свою очередь, определяет, каким индикаторам следует уделить больше внимания, а какие следует понизить в приоритете.
В целом процесс составления рейтинга основывается на трёх основных принципах.
- Поквартальная независимость. Каждый квартал рассматривается как самостоятельная аналитическая среда. Рейтинги, рассчитанные на основе данных предыдущих кварталов, никак не влияют на последующие кварталы. Эта независимость означает, что пайплайн выявляет подлинные смены рыночного режима — например, переходы от трендовых рынков к рынкам, торгующимся в диапазоне, — вместо того чтобы навязывать непрерывность там, где её нет.
- Ранжирование по совокупному баллу. Индикаторы сортируются исключительно по совокупному баллу, который объединяет точность/долю успешных сигналов, среднюю форвардную доходность и количество сгенерированных сигналов.
- Пороговое значение надёжности сигнала. Для индикаторов, у которых в пределах квартала недостаточно бычих и медвежьих сигналов, применяется штраф: они исключаются из рейтинга. Чтобы получить рейтинги по заданному набору индикаторов в разбивке по кварталам, мы воспользуемся приведенным ниже фрагментом кода.
# python libraries import MetaTrader5 as mt5 from dataclasses import dataclass from typing import Callable, Dict, Tuple import pandas as pd import numpy as np from datetime import datetime, time from pandas.tseries.holiday import USFederalHolidayCalendar # --------------------------------------------------------- # Import indicators from your module # --------------------------------------------------------- from IndicatorsAll import RSI, MACD, Bollinger_Bands # --------------------------------------------------------- # Import quarterly ranking helper from fxi-8 # (make sure the file is named fxi_8.py, not fxi-8.py) # --------------------------------------------------------- from fxi_8 import build_quarterly_rankings # --------------------------------------------------------- # Indicator scoring function (as given) # --------------------------------------------------------- def score_indicator(patterns: pd.DataFrame, fwd_rets: pd.DataFrame, min_signals: int = 10) -> dict: """ Compute predictive power metrics for one indicator within a single quarter. patterns: DataFrame with columns ['bullish', 'bearish', 'flat'] (0/1 or bool) fwd_rets: DataFrame with columns such as ['fwd_ret_1', 'fwd_ret_3', ...] """ results = {} for state in ["bullish", "bearish"]: mask = patterns[state] == 1 num_signals = int(mask.sum()) results[f"{state}_count"] = num_signals if num_signals < min_signals: results[f"{state}_hit_rate"] = np.nan results[f"{state}_mean_ret"] = np.nan continue expected_sign = 1 if state == "bullish" else -1 state_hits = [] state_mean_rets = [] for col in fwd_rets.columns: r = fwd_rets.loc[mask, col].dropna() if r.empty: continue hits = (np.sign(r) == expected_sign).astype(float).mean() state_hits.append(hits) state_mean_rets.append(r.mean()) if state_hits: results[f"{state}_hit_rate"] = float(np.mean(state_hits)) results[f"{state}_mean_ret"] = float(np.mean(state_mean_rets)) else: results[f"{state}_hit_rate"] = np.nan results[f"{state}_mean_ret"] = np.nan bullish_hr = results.get("bullish_hit_rate", np.nan) bearish_hr = results.get("bearish_hit_rate", np.nan) bullish_ret = results.get("bullish_mean_ret", 0.0) bearish_ret = results.get("bearish_mean_ret", 0.0) valid_hit_rates = [x for x in [bullish_hr, bearish_hr] if not np.isnan(x)] hit_component = float(np.mean(valid_hit_rates)) if valid_hit_rates else 0.0 magnitude_component = float((abs(bullish_ret) + abs(bearish_ret)) / 2.0) final_score = hit_component + 0.5 * magnitude_component results["score"] = final_score return results # --------------------------------------------------------- # Forward returns from CLOSE ONLY (no indicator info) # --------------------------------------------------------- def compute_forward_returns(df: pd.DataFrame, horizons=(1, 3, 6)) -> pd.DataFrame: close = df["close"] forward_data = {} for h in horizons: forward_data[f"fwd_ret_{h}"] = (close.shift(-h) / close) - 1.0 return pd.DataFrame(forward_data, index=df.index) # --------------------------------------------------------- # Pattern logic USING REAL INDICATOR VALUES # --------------------------------------------------------- def rsi_patterns(df: pd.DataFrame, rsi: pd.Series) -> pd.DataFrame: bullish = (rsi < 30).astype(int) bearish = (rsi > 70).astype(int) flat = ((rsi >= 30) & (rsi <= 70)).astype(int) return pd.DataFrame({"bullish": bullish, "bearish": bearish, "flat": flat}) def macd_patterns(df: pd.DataFrame, macd_hist: pd.Series) -> pd.DataFrame: bullish = (macd_hist > 0).astype(int) bearish = (macd_hist < 0).astype(int) flat = (macd_hist == 0).astype(int) return pd.DataFrame({"bullish": bullish, "bearish": bearish, "flat": flat}) def bollinger_patterns(df: pd.DataFrame, bb_width: pd.Series) -> pd.DataFrame: price = df["close"] price_slope = price.diff() contraction = (bb_width < bb_width.rolling(20).mean()).astype(int) bullish = ((contraction == 0) & (price_slope > 0)).astype(int) bearish = ((contraction == 0) & (price_slope < 0)).astype(int) flat = (contraction == 1).astype(int) return pd.DataFrame({"bullish": bullish, "bearish": bearish, "flat": flat}) # --------------------------------------------------------- # MAIN – Full example with RSI, MACD, Bollinger Bands # --------------------------------------------------------- def main(): # ------------------------------------------------------------ # Initialize MetaTrader 5 # ------------------------------------------------------------ if not mt5.initialize(login=XXXXXXXX, server="XXXXX", password="XXX"): print("initialize() failed, error code =", mt5.last_error()) return # ------------------------------------------------------------ # Set start and end dates for history data # ------------------------------------------------------------ end_date = datetime(2025, 12, 1, 0) start_date = datetime(2020, 1, 1, 0) # Get H4 rates for FXI (CFD symbol "#FXI") fxi_rates = mt5.copy_rates_range("#FXI", mt5.TIMEFRAME_H4, start_date, end_date) if fxi_rates is None or len(fxi_rates) == 0: print("No data returned from MT5.") mt5.shutdown() return # ------------------------------------------------------------ # Build base DataFrame # ------------------------------------------------------------ df = pd.DataFrame(fxi_rates) # MT5 times are in seconds since epoch df["time"] = pd.to_datetime(df["time"], unit="s") df.set_index("time", inplace=True) df = df.sort_index() # Confirm essential OHLC columns exist required_cols = {"open", "high", "low", "close"} if not required_cols.issubset(df.columns): missing = required_cols - set(df.columns) raise ValueError(f"Dataset missing required columns: {missing}") # ------------------------------------------------------------ # Check interval consistency and resample to uniform 4h grid # ------------------------------------------------------------ time_diff = df.index.to_series().diff().dropna() irregularities = time_diff[time_diff != pd.Timedelta(hours=4)] print("Irregular intervals detected:\n", irregularities.head()) # Use lowercase "h" to avoid FutureWarning df_resampled = df.resample("4h").ffill() # ------------------------------------------------------------ # Attach quarterly segmentation # ------------------------------------------------------------ df_resampled["quarter"] = df_resampled.index.to_period("Q") # ------------------------------------------------------------ # Market-hours mask + U.S. holidays detection # ------------------------------------------------------------ # Assume MT5 timestamps are in UTC. If your server is NOT UTC, # adjust this localize step accordingly. idx_utc = df_resampled.index.tz_localize("UTC") # Convert to New York time (U.S. Eastern, with DST handled) idx_ny = idx_utc.tz_convert("America/New_York") # Weekday mask (Mon–Fri) is_us_weekday = idx_ny.weekday < 5 # 0=Mon, 4=Fri # Session time mask (09:30–16:00 New York time) ny_times = idx_ny.time session_start = time(9, 30) session_end = time(16, 0) is_session_time = np.array( [(t >= session_start) and (t <= session_end) for t in ny_times], dtype=bool ) # U.S. holiday calendar (federal holidays; close approximation) cal = USFederalHolidayCalendar() holidays = cal.holidays( start=idx_ny.min().date(), end=idx_ny.max().date() ) # Normalize times to dates and check membership in holiday list is_holiday = pd.Series(idx_ny.normalize()).isin(holidays).to_numpy() # Final market mask: weekday, in session, not holiday market_mask = is_us_weekday & is_session_time & (~is_holiday) # Apply mask df_market = df_resampled[market_mask].copy() # ------------------------------------------------------------ # Compute real indicators from IndicatorsAll # ------------------------------------------------------------ df_rsi = RSI(df_market) # expects 'RSI' df_macd = MACD(df_market) # expects 'MACD_hist' df_bb = Bollinger_Bands(df_market) # expects 'BB_width' df_all = df_market.copy() df_all["RSI"] = df_rsi["RSI"] df_all["MACD_hist"] = df_macd["MACD_hist"] df_all["BB_width"] = df_bb["BB_width"] # ------------------------------------------------------------ # Per-quarter scoring for each indicator # ------------------------------------------------------------ rows = [] indicator_specs = { "RSI": ("RSI", rsi_patterns), "MACD_Hist": ("MACD_hist", macd_patterns), "BB_Width": ("BB_width", bollinger_patterns), } for quarter, df_q in df_all.groupby("quarter"): fwd_q = compute_forward_returns(df_q, horizons=(1, 3, 6)) for ind_name, (col_name, pattern_fn) in indicator_specs.items(): patterns_q = pattern_fn(df_q, df_q[col_name]) score_dict = score_indicator(patterns_q, fwd_q) rows.append({ "quarter": str(quarter), "indicator": ind_name, "score": score_dict["score"], "bullish_count": score_dict["bullish_count"], "bearish_count": score_dict["bearish_count"], "bullish_hit_rate": score_dict.get("bullish_hit_rate"), "bearish_hit_rate": score_dict.get("bearish_hit_rate"), "bullish_mean_ret": score_dict.get("bullish_mean_ret"), "bearish_mean_ret": score_dict.get("bearish_mean_ret"), }) results_df = pd.DataFrame(rows) print("\n=== Raw per-quarter indicator scores ===\n") print(results_df.head()) # ------------------------------------------------------------ # Build per-quarter rankings using fxi-8 helper # ------------------------------------------------------------ quarterly_rankings = build_quarterly_rankings(results_df) print("\n=== Quarterly Indicator Rankings ===\n") for q, ranking in quarterly_rankings.items(): print(f"{q}:") for ind_name, score in ranking: print(f" {ind_name:10s} → {score:.6f}") print() # Example: top 3 indicators for a specific quarter, if present target_q = "2023Q2" if target_q in quarterly_rankings: print(f"\nTop 3 indicators for {target_q}:") for ind_name, score in quarterly_rankings[target_q][:3]: print(f" {ind_name:10s} → {score:.6f}") # ------------------------------------------------------------ # Save per-quarter scoring results for later processing # ------------------------------------------------------------ csv_path = "fxi_quarterly_scores.csv" results_df.to_csv(csv_path, index=False) print(f"\nSaved quarterly indicator scores → {csv_path}\n") # ------------------------------------------------------------ # Shutdown MT5 # ------------------------------------------------------------ mt5.shutdown() if __name__ == "__main__": main()
Если запустить этот код и получить на выходе CSV-файл с рейтингом наших трёх индикаторов, мы получим следующую матрицу эффективности индикаторов по кварталам.
Таким образом, благодаря этому механизму квартального ранжирования пайплайн способен преобразовывать исходные прогнозные метрики в рекомендации, которые мы можем использовать на практике. Данный рейтинг/матрица действительно служит аналитическим фильтром, благодаря которому для дальнейшей разработки отбираются только те индикаторы, по которым есть эмпирические подтверждения.
Выводы по квартальным оценочным таблицам
Мы провели пробный запуск всего с тремя индикаторами, охватывающими различные аспекты рынка ETF FXI, и результаты, пожалуй, получились дисциплинированными и основанными на данных. Благодаря отображению позиций индикаторов по кварталам наш пайплайн позволяет выявить некоторые структурные тенденции, которые могут остаться незамеченными при анализе агрегированных данных за определенный период.
Однако результаты нашего матричного анализа, по-видимому, показывают, что для ETF FXI индикаторы тренда демонстрируют более высокую эффективность в большинстве рыночных режимов. Это следует из того, что гистограмма MACD неоднократно занимала первое место: хотя пул индикаторов был весьма небольшим, она всё же превосходила RSI и полосы Боллинджера. Это может означать, что внутриквартальная структура FXI действительно благоприятствует устойчивым направленным микротрендам, несмотря на несколько периодов у этого ETF, когда ценовое движение выглядит неопределённым. Итак, очевидно, что если бы мы действовали «наобум» и отбросили индикаторы тренда, основываясь лишь на беглом осмотре графика FXI, то наши результаты явно оказались бы хуже.
Индикатор RSI способен занимать первое место лишь в нескольких отдельных кварталах — если точнее, в двух, — а два остальных случая приходятся на второе и третье места. Индикатор RSI также попадает в рейтинг лишь 4 раза, поскольку в большинстве кварталов нам не удалось сгенерировать достаточное количество сигналов. Этого вполне следовало ожидать, поскольку осцилляторы, как правило, дают преимущество в прогнозировании только тогда, когда рынок находится в диапазонах, благоприятных для возврата к среднему, но их эффективность резко снижается в условиях устойчивых трендов.
Сигнал ширины индикатора «Полосы Боллинджера», похоже, закрепляется на втором месте после MACD. Можно утверждать, что его позиция в рейтинге улучшается в кварталах, когда наблюдается сжатие волатильности или переходы между режимами, что позволяет предположить, что он демонстрирует высокую эффективность в условиях структурных точек разворота. Также стоит отметить, что в кварталах, когда RSI или полосы Боллинджера выходили на первое место, рынки колебались без различимой структуры. Это еще раз подтверждает целесообразность использования MACD и других индикаторов следования за трендом как предпочтительных инструментов для работы с FXI при долгосрочной торговле. Итак, окончательный рейтинг индикаторов, которые следует использовать при торговле FXI, составленный на основе нашего пятилетнего анализа на 4-часовом таймфрейме, выглядит следующим образом:
- Индикаторы следования за трендом / гистограмма MACD;
- Индикаторы расширения/сжатия волатильности, такие как полосы Боллинджера; и
- Моментум-осцилляторы, такие как RSI: с учетом крайне малого количества сгенерированных нами сигналов это был избирательный, селективный инструмент.
Теперь перейдем к заключительному этапу, на котором мы постараемся воплотить эти идеи в виде простого советника для дальнейшего тестирования.
Перенос в MQL5
Выбор индикаторов для использования в MQL5, конечно, будет зависеть от результатов ранжирования, как мы показали на примере пула из трёх индикаторов. Большинство индикаторов уже реализовано на языке MQL5; более того, даже файлы пользовательских классов сигналов для сборки советника с помощью Мастера MQL5 уже доступны «из коробки». Из трёх индикаторов, которые мы тестировали, это справедливо для всех, за исключением индикатора «Полосы Боллинджера». Таким образом, мы можем реализовать собственную, весьма простую версию индикатора «Полосы Боллинджера» следующим образом.
//+------------------------------------------------------------------+ //| SignalBollinger.mqh | //| Copyright 2000-2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #include <Expert\ExpertSignal.mqh> // wizard description start … class CSignalBollinger : public CExpertSignal { protected: CiBands m_bb; // object-indicator //--- adjusted parameters int m_ma_period; // the "period of averaging" parameter of the indicator int m_ma_shift; // the "time shift" parameter of the indicator ENUM_MA_METHOD m_ma_method; // the "method of averaging" parameter of the indicator ENUM_APPLIED_PRICE m_ma_applied; // the "object of averaging" parameter of the indicator double m_deviation; // the "deviation" parameter of the indicator double m_limit_in; // threshold sensitivity of the 'rollback zone' double m_limit_out; // threshold sensitivity of the 'break through zone' //--- "weights" of market models (0-100) int m_pattern_0; // model 0 "price is near the necessary border of the envelope" int m_pattern_1; // model 1 "price crossed a border of the envelope" public: CSignalBollinger(void); ~CSignalBollinger(void); //--- methods of setting adjustable parameters void PeriodMA(int value) { m_ma_period=value; } void Shift(int value) { m_ma_shift=value; } void Method(ENUM_MA_METHOD value) { m_ma_method=value; } void Applied(ENUM_APPLIED_PRICE value) { m_ma_applied=value; } void Deviation(double value) { m_deviation=value; } void LimitIn(double value) { m_limit_in=value; } void LimitOut(double value) { m_limit_out=value; } //--- methods of adjusting "weights" of market models void Pattern_0(int value) { m_pattern_0=value; } void Pattern_1(int value) { m_pattern_1=value; } //--- method of verification of settings virtual bool ValidationSettings(void); //--- method of creating the indicator and timeseries virtual bool InitIndicators(CIndicators *indicators); //--- methods of checking if the market models are formed virtual int LongCondition(void); virtual int ShortCondition(void); protected: //--- method of initialization of the indicator bool InitMA(CIndicators *indicators); //--- methods of getting data double Upper(int ind) { return(m_bb.Upper(ind)); } double Lower(int ind) { return(m_bb.Lower(ind)); } }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CSignalBollinger::CSignalBollinger(void) : m_ma_period(45), m_ma_shift(0), m_ma_method(MODE_SMA), m_ma_applied(PRICE_CLOSE), m_deviation(0.15), m_limit_in(0.2), m_limit_out(0.2), m_pattern_0(90), m_pattern_1(70) { //--- initialization of protected data m_used_series=USE_SERIES_OPEN+USE_SERIES_HIGH+USE_SERIES_LOW+USE_SERIES_CLOSE; } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CSignalBollinger::~CSignalBollinger(void) { } //+------------------------------------------------------------------+ //| Validation settings protected data. | //+------------------------------------------------------------------+ bool CSignalBollinger::ValidationSettings(void) { //--- validation settings of additional filters if(!CExpertSignal::ValidationSettings()) return(false); //--- initial data checks if(m_ma_period<=0) { printf(__FUNCTION__+": period MA must be greater than 0"); return(false); } //--- ok return(true); } … //+------------------------------------------------------------------+ //| "Voting" that price will grow. | //+------------------------------------------------------------------+ int CSignalBollinger::LongCondition(void) { int result=0; int idx =StartIndex(); double close=Close(idx); double upper=Upper(idx); double lower=Lower(idx); double width=upper-lower; //--- if the model 0 is used and price is in the rollback zone, then there is a condition for buying if(IS_PATTERN_USAGE(0) && close<lower+m_limit_in*width && close>lower-m_limit_out*width) result=m_pattern_0; //--- if the model 1 is used and price is above the rollback zone, then there is a condition for buying if(IS_PATTERN_USAGE(1) && close>upper+m_limit_out*width) result=m_pattern_1; //--- return the result return(result); } //+------------------------------------------------------------------+ //| "Voting" that price will fall. | //+------------------------------------------------------------------+ int CSignalBollinger::ShortCondition(void) { int result =0; int idx =StartIndex(); double close=Close(idx); double upper=Upper(idx); double lower=Lower(idx); double width=upper-lower; //--- if the model 0 is used and price is in the rollback zone, then there is a condition for selling if(IS_PATTERN_USAGE(0) && close>upper-m_limit_in*width && close<upper+m_limit_out*width) result=m_pattern_0; //--- if the model 1 is used and price is above the rollback zone, then there is a condition for selling if(IS_PATTERN_USAGE(1) && close<lower-m_limit_out*width) result=m_pattern_1; //--- return the result return(result); } //+------------------------------------------------------------------+
После реализации индикатора «Полосы Боллинджера» один из возможных подходов в нашей реализации мог бы заключаться в том, чтобы просто собрать в Мастере MQL5 советник, выбирающий все три индикатора, причём вес каждого индикатора мог бы быть пропорционален числу кварталов, в которых этот индикатор занимал первое место. Хотя этот подход и кажется «логичным», он, конечно же, может столкнуться с трудностями в тех случаях, когда используется множество индикаторов, и при этом более важную роль будет играть субъективное суждение трейдера о том, какие из них следует учитывать. Читатель, разумеется, может проверить этот подход отдельно на каждом активе, чтобы определить, что работает лучше всего. Однако для наших целей заголовок советника, собранного с помощью Мастера MQL5, будет выглядеть следующим образом.
//+------------------------------------------------------------------+ //| FXI.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| Include | //+------------------------------------------------------------------+ #include <Expert\Expert.mqh> //--- available signals #include <Expert\Signal\SignalMACD.mqh> #include <Expert\Signal\SignalBollinger.mqh> #include <Expert\Signal\SignalRSI.mqh> //--- available trailing #include <Expert\Trailing\TrailingNone.mqh> //--- available money management #include <Expert\Money\MoneyFixedMargin.mqh>
Заключение
Надеемся, что подход, представленный в этой статье, показал строгий, воспроизводимый и расширяемый метод выбора индикаторов. Наш метод основан на эмпирическом анализе данных за несколько кварталов. Сочетая надежность и скорость Python в аналитике со средой исполнения MQL5, мы представляем пайплайн Codex, который задает единый рабочий процесс для поиска, проверки и использования технических индикаторов. Несколько важных характеристик выделяют наш подход как потенциально надежную основу для дальнейших исследований и разработок.
- Оценка с учетом рыночного режима. Используя квартальную сегментацию, мы создаем основу для того, чтобы эффективность индикаторов анализировалась в контекстно значимых периодах. Мы можем фиксировать изменения волатильности и моментума, а также отсекать структурный шум.
- Формальная логика паттернов. Приняв детерминированные состояния паттерна — бычье, медвежье и нейтральное, — мы вводим широко понятную терминологию, упрощающую сравнение множества различных типов индикаторов.
- Оценка с ориентацией на будущее. Проводя оценку, основанную исключительно на доходности на последующих барах, мы устраняем ретроспективное смещение и добиваемся того, чтобы индикаторы получали оценку только за свою прогнозную способность.
- Практическая реализация направленных сигналов. В MQL5 мы завершаем пайплайн, предоставляя готовые к применению компоненты для алгоритмической торговли.
Со временем, по мере обновления данных и включения в анализ новых кварталов, пайплайн Codex всегда можно запускать повторно, чтобы обновлять рейтинги ранее рассмотренного пула индикаторов, а затем уточнять соответствующую логику MQL5. Такой циклический подход делает этот метод долгосрочным аналитическим инструментом для адаптации не только к меняющейся рыночной структуре ETF FXI, но и к любому активу, для которого структурированный поиск индикаторов может дать полезные результаты. Ведь при беглом просмотре ценового графика FXI мало кто предположил бы, что предпочтительным инструментом окажутся трендовые индикаторы.
| Название | Описание |
|---|---|
| SignalBollinger.mqh | Пользовательский класс сигнала для индикатора «Полосы Боллинджера» |
| FXI.mq5 | Основной файл советника, собранного с помощью Мастера MQL5 |
| fxi-py all.zip | ZIP-архив с упомянутым кодом на Python |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20550
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Нейросети в трейдинге: Адаптация прогноза при смене рыночного режима (OMPB)
Моделирование рынка: Position View (XX)
Алгоритм оптимизации на основе приспособленности — Fitness Dependent Optimizer (FDO)
Реализация шаблона "Декоратор" в MQL5: Добавление логирования, измерения времени выполнения и фильтрации к любому индикатору без изменения исходного кода
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования

