Тестер стратегий для Python и MetaTrader 5 (Часть 02): Работа с барами, тиками и реализация встроенных функций в симуляторе
Оглавление
- Введение
- Обработка исторических тиков MetaTrader 5
- Обработка исторических баров MetaTrader 5
- Перегрузка функций MetaTrader 5
- symbol_info_tick
- symbol_info
- copy_rates_from
- copy_rates_from_pos
- copy_rates_range
- copy_ticks_from
- copy_ticks_range
- orders_total
- orders_get
- positions_total
- positions_get
- history_orders_total
- history_orders_get
- deals_total
- history_deals_get
- account_info
- order_calc_profit
- order_calc_margin
- Заключительные мысли
Введение
В предыдущей статье мы обсудили и создали в Python класс симулятора под названием TradeSimulator, который в значительной степени опирался на информацию из MetaTrader 5, такую как тики, данные баров, информация о символах и многое другое.
Первая статья заложила основу того, что необходимо для имитации клиента MetaTrader 5 и его тестера стратегий, то есть симулятора. В этой статье мы добавим данные тиков и баров, а также функции, аналогичные тем, которые предоставляет модуль Python–MetaTrader 5, в симулятор. Это приблизит нас ещё на один шаг к воспроизведению всего, что делает и предоставляет MetaTrader 5.
Обработка исторических тиков MetaTrader 5
Тики — это наиболее детализированные ценовые обновления финансового инструмента в реальном времени, отражающие каждое отдельное изменение цены, движение bid/ask и торговый объём.
В отличие от OHLC-баров — Open, High, Low, Close — тики предоставляют данные с точностью до миллисекунд.
Возможно, вы знакомы с функцией OnTick из языка программирования MQL5. Это основная функция MQL5-ботов, которая вызывается при поступлении нового тика.
Терминал MetaTrader 5 в значительной степени опирается на тиковые данные при открытии, сопровождении и закрытии сделок. Без тиков на этой платформе невозможны никакие торговые операции.
С учётом этого нам нужно уметь получать и обрабатывать тики так же, как это делает терминал.
Модуль Python–MetaTrader 5 предоставляет различные способы получения тиков. Один из них — использование функции copy_ticks_range:
copy_ticks_range( symbol, // symbol name date_from, // date the ticks are requested from date_to, // date, up to which the ticks are requested flags // combination of flags defining the type of requested ticks )
Попробуем собрать тиковые данные из MetaTrader 5.
def fetch_ticks(start_datetime: datetime, end_datetime: datetime, symbol: str): ticks = mt5.copy_ticks_range(symbol, start_datetime, end_datetime, mt5.COPY_TICKS_ALL) print(f"Fetched {len(ticks)} ticks for {symbol} from {start_datetime} to {end_datetime}") print(ticks[:5]) # Print first 5 ticks for inspection return ticks
Пример.
import MetaTrader5 as mt5 from datetime import datetime, timezone if __name__ == "__main__": if not mt5.initialize(): print(f"Failed to Initialize MetaTrader5. Error = {mt5.last_error()}") mt5.shutdown() quit() symbol = "EURUSD" start_dt = datetime(2025, 1, 1, 0, 0, tzinfo=timezone.utc) end_dt = datetime(2025, 12, 1, 1, 0, tzinfo=timezone.utc) fetch_ticks(start_dt, end_dt, symbol)
Вывод.
Fetched 2814462 ticks for EURUSD from 2025-01-01 00:00:00+00:00 to 2025-12-01 01:00:00+00:00 [(1758499200, 1.17403, 1.17603, 0., 0, 1758499200161, 134, 0.) (1758499247, 1.17405, 1.17605, 0., 0, 1758499247468, 134, 0.) (1758499500, 1.17346, 1.17546, 0., 0, 1758499500116, 134, 0.) (1758499505, 1.173 , 1.175 , 0., 0, 1758499505869, 134, 0.) (1758499510, 1.17307, 1.17487, 0., 0, 1758499510079, 134, 0.)]
Как видно, всего за 11 месяцев нам удалось получить 2,8 миллиона тиковых записей. Мы также можем проверить их размер в мегабайтах. Это даст приблизительную оценку того, сколько памяти RAM потребляет один такой запрос тиков.
# calculate tick array size in megabytes size_in_bytes = ticks.nbytes size_in_mb = size_in_bytes / (1024 * 1024) print(f"Tick array size: {size_in_mb:.2f} MB")
Вывод.
Tick array size: 161.04 MB Как видно, данные всего за 11 месяцев занимают около 0,1 ГБ. Теперь представьте, что в нашем симуляторе, то есть тестере стратегий, пользователь решит протестировать мультивалютного бота по 12 символам за 20 лет. Насколько это нагрузит память и общую производительность?
Нам необходимо найти оптимальный подход к обработке такого объёма данных без чрезмерного потребления памяти и при приемлемой общей производительности.
Polars DataFrames — одно из лучших решений для подобных ситуаций.
Polars прост в использовании и очень быстр; его streaming API позволяет разработчикам эффективно обрабатывать большие наборы данных, включая наборы данных, превышающие объём доступной памяти, например 100 ГБ и больше.
Поскольку мы больше не будем использовать массивы NumPy для хранения всего объёма данных, нам также необходимо разделить процесс сбора данных на более мелкие и менее затратные по памяти фрагменты тиковых данных.
def ticks_to_polars(ticks): return pl.DataFrame({ "time": ticks["time"], "bid": ticks["bid"], "ask": ticks["ask"], "last": ticks["last"], "volume": ticks["volume"], "time_msc": ticks["time_msc"], "flags": ticks["flags"], "volume_real": ticks["volume_real"], }) def fetch_historical_ticks(start_datetime: datetime, end_datetime: datetime, symbol: str): # first of all, we have to ensure the symbol is valid and can be used for requesting data if not utils.ensure_symbol(symbol=symbol): print(f"Symbol {symbol} not available") return current = start_datetime.replace(day=1, hour=0, minute=0, second=0) while True: month_start, month_end = utils.month_bounds(current) # Cap last month to end_date if ( month_start.year == end_datetime.year and month_start.month == end_datetime.month ): month_end = end_datetime # Stop condition if month_start > end_datetime: break print(f"Processing ticks {month_start:%Y-%m-%d} -> {month_end:%Y-%m-%d}") # --- fetch data here --- ticks = mt5.copy_ticks_range( symbol, month_start, month_end, mt5.COPY_TICKS_ALL ) if ticks is None or len(ticks) == 0: config.simulator_logger.critical(f"Failed to Get ticks. Error = {mt5.last_error()}") current = (month_start + timedelta(days=32)).replace(day=1) # Advance to next month safely continue df = ticks_to_polars(ticks) df = df.with_columns([ pl.from_epoch("time", time_unit="s").dt.replace_time_zone("utc").alias("time") ]) df = df.with_columns([ pl.col("time").dt.year().alias("year"), pl.col("time").dt.month().alias("month"), ]) df.write_parquet( os.path.join(config.TICKS_HISTORY_DIR, symbol), partition_by=["year", "month"], mkdir=True ) if config.debug: print(df.head(-10)) # Advance to next month safely current = (month_start + timedelta(days=32)).replace(day=1)
Таким образом, вместо того чтобы собирать все тики сразу с помощью copy_ticks_range, мы итеративно собираем тики за каждый месяц и сохраняем информацию в отдельные файлы.
df.write_parquet(
os.path.join(config.TICKS_HISTORY_DIR, symbol),
partition_by=["year", "month"],
mkdir=True
) Выведем данные, чтобы посмотреть, что содержит объект DataFrame.
print(df.head(-10)) # optional, see what data looks like
Вывод.
2025-12-24 16:41:44,138 | CRITICAL | simulator.log20251224 | fetch_historical_ticks 52 --> Failed to Get ticks. Error = (1, 'Success') Processing ticks 2025-07-01 -> 2025-07-31 2025-12-24 16:41:44,139 | CRITICAL | simulator.log20251224 | fetch_historical_ticks 52 --> Failed to Get ticks. Error = (1, 'Success') Processing ticks 2025-08-01 -> 2025-08-31 2025-12-24 16:41:44,140 | CRITICAL | simulator.log20251224 | fetch_historical_ticks 52 --> Failed to Get ticks. Error = (1, 'Success') Processing ticks 2025-09-01 -> 2025-09-30 shape: (434_916, 10) ┌─────────────────────────┬─────────┬─────────┬──────┬───┬───────┬─────────────┬──────┬───────┐ │ time ┆ bid ┆ ask ┆ last ┆ … ┆ flags ┆ volume_real ┆ year ┆ month │ │ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │ │ datetime[μs, UTC] ┆ f64 ┆ f64 ┆ f64 ┆ ┆ u32 ┆ f64 ┆ i32 ┆ i8 │ ╞═════════════════════════╪═════════╪═════════╪══════╪═══╪═══════╪═════════════╪══════╪═══════╡ │ 2025-09-22 00:00:00 UTC ┆ 1.17403 ┆ 1.17603 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 9 │ │ 2025-09-22 00:00:47 UTC ┆ 1.17405 ┆ 1.17605 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 9 │ │ 2025-09-22 00:05:00 UTC ┆ 1.17346 ┆ 1.17546 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 9 │ │ 2025-09-22 00:05:05 UTC ┆ 1.173 ┆ 1.175 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 9 │ │ 2025-09-22 00:05:10 UTC ┆ 1.17307 ┆ 1.17487 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 9 │ │ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │ │ 2025-09-30 23:58:44 UTC ┆ 1.17335 ┆ 1.17343 ┆ 0.0 ┆ … ┆ 4 ┆ 0.0 ┆ 2025 ┆ 9 │ │ 2025-09-30 23:58:45 UTC ┆ 1.17335 ┆ 1.17342 ┆ 0.0 ┆ … ┆ 4 ┆ 0.0 ┆ 2025 ┆ 9 │ │ 2025-09-30 23:58:46 UTC ┆ 1.17335 ┆ 1.17343 ┆ 0.0 ┆ … ┆ 4 ┆ 0.0 ┆ 2025 ┆ 9 │ │ 2025-09-30 23:58:47 UTC ┆ 1.17335 ┆ 1.17342 ┆ 0.0 ┆ … ┆ 4 ┆ 0.0 ┆ 2025 ┆ 9 │ │ 2025-09-30 23:58:50 UTC ┆ 1.17334 ┆ 1.1734 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 9 │ └─────────────────────────┴─────────┴─────────┴──────┴───┴───────┴─────────────┴──────┴───────┘ Processing ticks 2025-10-01 -> 2025-10-31 shape: (1_401_674, 10) ┌─────────────────────────┬─────────┬─────────┬──────┬───┬───────┬─────────────┬──────┬───────┐ │ time ┆ bid ┆ ask ┆ last ┆ … ┆ flags ┆ volume_real ┆ year ┆ month │ │ 2025-10-01 00:00:01 UTC ┆ 1.17337 ┆ 1.17506 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 10 │ │ 2025-10-01 00:00:02 UTC ┆ 1.17337 ┆ 1.17402 ┆ 0.0 ┆ … ┆ 4 ┆ 0.0 ┆ 2025 ┆ 10 │ │ 2025-10-01 00:00:02 UTC ┆ 1.17337 ┆ 1.17389 ┆ 0.0 ┆ … ┆ 4 ┆ 0.0 ┆ 2025 ┆ 10 │ │ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │ │ 2025-10-31 23:56:43 UTC ┆ 1.15368 ┆ 1.15368 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 10 │ │ 2025-10-31 23:56:52 UTC ┆ 1.15369 ┆ 1.15369 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 10 │ │ 2025-10-31 23:56:52 UTC ┆ 1.15371 ┆ 1.15371 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 10 │ │ 2025-10-31 23:56:53 UTC ┆ 1.1537 ┆ 1.1537 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 10 │ │ 2025-10-31 23:56:53 UTC ┆ 1.15371 ┆ 1.15371 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 10 │ └─────────────────────────┴─────────┴─────────┴──────┴───┴───────┴─────────────┴──────┴───────┘ Processing ticks 2025-11-01 -> 2025-11-30 shape: (976_714, 10) ┌─────────────────────────┬─────────┬─────────┬──────┬───┬───────┬─────────────┬──────┬───────┐ │ time ┆ bid ┆ ask ┆ last ┆ … ┆ flags ┆ volume_real ┆ year ┆ month │ │ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │ │ datetime[μs, UTC] ┆ f64 ┆ f64 ┆ f64 ┆ ┆ u32 ┆ f64 ┆ i32 ┆ i8 │ ╞═════════════════════════╪═════════╪═════════╪══════╪═══╪═══════╪═════════════╪══════╪═══════╡ │ 2025-11-03 00:00:00 UTC ┆ 1.1528 ┆ 1.15365 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 11 │ │ 2025-11-03 00:01:00 UTC ┆ 1.1528 ┆ 1.15365 ┆ 0.0 ┆ … ┆ 130 ┆ 0.0 ┆ 2025 ┆ 11 │ │ 2025-11-03 00:01:00 UTC ┆ 1.1528 ┆ 1.15365 ┆ 0.0 ┆ … ┆ 4 ┆ 0.0 ┆ 2025 ┆ 11 │ │ 2025-11-03 00:01:21 UTC ┆ 1.15295 ┆ 1.15365 ┆ 0.0 ┆ … ┆ 130 ┆ 0.0 ┆ 2025 ┆ 11 │ │ 2025-11-03 00:01:25 UTC ┆ 1.15282 ┆ 1.15365 ┆ 0.0 ┆ … ┆ 130 ┆ 0.0 ┆ 2025 ┆ 11 │ │ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │ │ 2025-11-28 23:55:12 UTC ┆ 1.15948 ┆ 1.16018 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 11 │ │ 2025-11-28 23:55:13 UTC ┆ 1.15955 ┆ 1.16017 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 11 │ │ 2025-11-28 23:55:36 UTC ┆ 1.15948 ┆ 1.16018 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 11 │ │ 2025-11-28 23:55:37 UTC ┆ 1.15953 ┆ 1.16017 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 11 │ │ 2025-11-28 23:55:54 UTC ┆ 1.15954 ┆ 1.16024 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 11 │ │ time ┆ bid ┆ ask ┆ last ┆ … ┆ flags ┆ volume_real ┆ year ┆ month │ │ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │ │ datetime[μs, UTC] ┆ f64 ┆ f64 ┆ f64 ┆ ┆ u32 ┆ f64 ┆ i32 ┆ i8 │ ╞═════════════════════════╪═════════╪═════════╪══════╪═══╪═══════╪═════════════╪══════╪═══════╡ │ 2025-12-01 00:00:00 UTC ┆ 1.15936 ┆ 1.15969 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 12 │ │ 2025-12-01 00:00:06 UTC ┆ 1.15934 ┆ 1.15962 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 12 │ │ 2025-12-01 00:00:11 UTC ┆ 1.15935 ┆ 1.15997 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 12 │ │ 2025-12-01 00:00:15 UTC ┆ 1.15936 ┆ 1.15979 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 12 │ │ 2025-12-01 00:00:21 UTC ┆ 1.15936 ┆ 1.15964 ┆ 0.0 ┆ … ┆ 4 ┆ 0.0 ┆ 2025 ┆ 12 │ │ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │ │ 2025-12-01 00:59:57 UTC ┆ 1.15964 ┆ 1.16005 ┆ 0.0 ┆ … ┆ 4 ┆ 0.0 ┆ 2025 ┆ 12 │ │ 2025-12-01 00:59:57 UTC ┆ 1.15972 ┆ 1.16012 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 12 │ │ 2025-12-01 00:59:57 UTC ┆ 1.15967 ┆ 1.16005 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 12 │ │ 2025-12-01 00:59:57 UTC ┆ 1.15971 ┆ 1.16009 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 12 │ │ 2025-12-01 00:59:57 UTC ┆ 1.15965 ┆ 1.16005 ┆ 0.0 ┆ … ┆ 134 ┆ 0.0 ┆ 2025 ┆ 12 │ └─────────────────────────┴─────────┴─────────┴──────┴───┴───────┴─────────────┴──────┴───────┘ January 2024: shape: (0, 10) ┌───────────────────┬─────┬─────┬──────┬───┬───────┬─────────────┬──────┬───────┐ │ time ┆ bid ┆ ask ┆ last ┆ … ┆ flags ┆ volume_real ┆ year ┆ month │ │ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │ │ datetime[μs, UTC] ┆ f64 ┆ f64 ┆ f64 ┆ ┆ u32 ┆ f64 ┆ i32 ┆ i8 │ ╞═══════════════════╪═════╪═════╪══════╪═══╪═══════╪═════════════╪══════╪═══════╡ └───────────────────┴─────┴─────┴──────┴───┴───────┴─────────────┴──────┴───────┘ shape: (1, 2) ┌───────────────────┬───────────────────┐ │ time_min ┆ time_max │ │ --- ┆ --- │ │ datetime[μs, UTC] ┆ datetime[μs, UTC] │ ╞═══════════════════╪═══════════════════╡ │ null ┆ null │ └───────────────────┴───────────────────┘
Одна из самых удобных особенностей метода Polars под названием write_parquet заключается в том, что при передаче значения в аргумент partition_by он использует полученные столбцы как группы и сохраняет данные в отдельные подпапки.
После сбора тиков по двум инструментам.
if __name__ == "__main__": if not mt5.initialize(): print(f"Failed to Initialize MetaTrader5. Error = {mt5.last_error()}") mt5.shutdown() quit() symbol = "EURUSD" start_dt = datetime(2025, 1, 1, 0, 0, tzinfo=timezone.utc) end_dt = datetime(2025, 12, 1, 1, 0, tzinfo=timezone.utc) fetch_historical_ticks(start_datetime=start_dt, end_datetime=end_dt, symbol=symbol) fetch_historical_ticks(start_datetime=start_dt, end_datetime=end_dt, symbol= "GBPUSD") path = os.path.join(config.TICKS_HISTORY_DIR, symbol) lf = pl.scan_parquet(path) jan_2024 = ( lf .filter( (pl.col("year") == 2024) & (pl.col("month") == 1) ) .collect(engine="streaming") ) print("January 2024:\n", jan_2024.head(-10)) print( jan_2024.select([ pl.col("time").min().alias("time_min"), pl.col("time").max().alias("time_max") ]) ) mt5.shutdown()
Ниже показано, как выглядят выходные папки.
(venv) c:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>tree History Folder PATH listing Volume serial number is 2CFE-3A78 C:\USERS\OMEGA JOCTAN\ONEDRIVE\DOCUMENTS\PYMETATESTER\HISTORY ├───Bars │ ├───EURUSD │ │ └───M5 └───Ticks ├───EURUSD │ └───year=2025 │ ├───month=10 │ ├───month=11 │ ├───month=12 │ └───month=9 └───GBPUSD └───year=2025 ├───month=10 ├───month=11 ├───month=12 └───month=9
К сожалению, мне не удалось получить все тиковые данные, которые я запрашивал, то есть с 1 января по 1 декабря 2025 года. Похоже, что нельзя получить больше тиков, чем доступно в вашем терминале MetaTrader 5. В данном случае у моего брокера было лишь несколько месяцев тиковых данных, и именно их я постоянно получал.
From: C:\Users\Omega\AppData\Roaming\MetaQuotes\Terminal\010E047102812FC0C18890992854220E\bases\<broker name>\ticks\EURUSD

Обработка исторических баров MetaTrader 5
В отличие от тиков, бары основаны на таймфреймах. С барами работать проще, чем с тиками. Аналогично тому, как мы собирали тики, нам нужно похожим образом собрать данные баров.
Сначала необходимо убедиться, что символ доступен, и выбрать его в MarketWatch перед запросом его баров.
Внутри utils.py:
def ensure_symbol(symbol: str) -> bool: info = mt5.symbol_info(symbol) if info is None: print(f"Symbol {symbol} not found") return False if not info.visible: if not mt5.symbol_select(symbol, True): print(f"Failed to select symbol {symbol}") return False return True
Затем мы собираем данные, начиная с первого и заканчивая последним днём месяца.
def fetch_historical_bars(symbol: str, timeframe: int, start_datetime: datetime, end_datetime: datetime): """ Fetch historical bar data for a given symbol and timeframe, forward in time. Saves data to a single Parquet file in append mode. """ if not utils.ensure_symbol(symbol=symbol): print(f"Symbol {symbol} not available") return current = start_datetime.replace(day=1, hour=0, minute=0, second=0) while True: month_start, month_end = utils.month_bounds(current) # Cap last month to end_date if ( month_start.year == end_datetime.year and month_start.month == end_datetime.month ): month_end = end_datetime # Stop condition if month_start > end_datetime: break print(f"Processing {month_start:%Y-%m-%d} -> {month_end:%Y-%m-%d}") # --- fetch data here --- rates = mt5.copy_rates_range( symbol, timeframe, month_start, month_end ) if rates is None and len(rates)==0: config.simulator_logger.warning(f"Failed to Get bars from MetaTrader5") current = (month_start + timedelta(days=32)).replace(day=1) # Advance to next month safely continue df = bars_to_polars(rates)
Мы сохраняем данные баров в соответствующие Parquet-файлы, разделённые по месяцам и годам в виде подпапок.
df = df.with_columns([ pl.from_epoch("time", time_unit="s").dt.replace_time_zone("utc").alias("time") ]) df = df.with_columns([ pl.col("time").dt.year().alias("year"), pl.col("time").dt.month().alias("month"), ]) tf_name = utils.TIMEFRAMES_REV[timeframe] df.write_parquet( os.path.join(config.BARS_HISTORY_DIR, symbol, tf_name), partition_by=["year", "month"], mkdir=True ) if config.is_debug: print(df.head(-10)) # Advance to next month safely current = (month_start + timedelta(days=32)).replace(day=1)
Например, бары, собранные по трём символам за 10 месяцев.
if __name__ == "__main__": if not mt5.initialize(): print(f"Failed to Initialize MetaTrader5. Error = {mt5.last_error()}") mt5.shutdown() quit() start_date = datetime(2022, 1, 1, tzinfo=timezone.utc) end_date = datetime(2025, 1, 10, tzinfo=timezone.utc) fetch_historical_bars("XAUUSD", mt5.TIMEFRAME_M1, start_date, end_date) fetch_historical_bars("EURUSD", mt5.TIMEFRAME_H1, start_date, end_date) fetch_historical_bars("GBPUSD", mt5.TIMEFRAME_M5, start_date, end_date) # read polaris dataframe and print the head for both symbols symbol = "GBPUSD" timeframe = utils.TIMEFRAMES_REV[mt5.TIMEFRAME_M5] path = os.path.join(config.BARS_HISTORY_DIR, symbol, timeframe) lf = pl.scan_parquet(path) jan_2024 = ( lf .filter( (pl.col("year") == 2024) & (pl.col("month") == 1) ) .collect(engine="streaming") ) print("January 2024:\n", jan_2024.head(-10)) print( jan_2024.select([ pl.col("time").min().alias("time_min"), pl.col("time").max().alias("time_max") ]) ) mt5.shutdown()
Вывод.
(venv) c:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>tree History Folder PATH listing Volume serial number is 2CFE-3A78 C:\USERS\OMEGA JOCTAN\ONEDRIVE\DOCUMENTS\PYMETATESTER\HISTORY ├───Bars │ ├───EURUSD │ │ ├───H1 │ │ │ ├───year=2022 │ │ │ │ ├───month=1 │ │ │ │ ├───month=10 │ │ │ │ ├───month=11 │ │ │ │ ├───month=12 │ │ │ │ ├───month=2 │ │ │ │ ├───month=3 │ │ │ │ ├───month=4 │ │ │ │ ├───month=5 │ │ │ │ ├───month=6 │ │ │ │ ├───month=7 │ │ │ │ ├───month=8 │ │ │ │ └───month=9 │ │ │ ├───year=2023 │ │ │ │ ├───month=1 │ │ │ │ ├───month=10 │ │ │ │ ├───month=11 │ │ │ │ ├───month=12 │ │ │ │ ├───month=2 │ │ │ │ ├───month=3 │ │ │ │ ├───month=4 │ │ │ │ ├───month=5 │ │ │ │ ├───month=6 │ │ │ │ ├───month=7 │ │ │ │ ├───month=8 │ │ │ │ └───month=9 │ │ │ ├───year=2024 │ │ │ │ ├───month=1 │ │ │ │ ├───month=10 │ │ │ │ ├───month=11 │ │ │ │ ├───month=12 │ │ │ │ ├───month=2 │ │ │ │ ├───month=3 │ │ │ │ ├───month=4 │ │ │ │ ├───month=5 │ │ │ │ ├───month=6 │ │ │ │ ├───month=7 │ │ │ │ ├───month=8 │ │ │ │ └───month=9 │ │ │ └───year=2025 │ │ │ └───month=1 │ │ └───M5 │ │ ├───year=2022 │ │ │ ├───month=1 │ │ │ ├───month=10 │ │ │ ├───month=11 │ │ │ ├───month=12 │ │ │ ├───month=2 │ │ │ ├───month=3 │ │ │ ├───month=4 │ │ │ ├───month=5 │ │ │ ├───month=6 │ │ │ ├───month=7 │ │ │ ├───month=8 │ │ │ └───month=9 │ │ ├───year=2023 │ │ │ ├───month=1 │ │ │ ├───month=10 │ │ │ ├───month=11 │ │ │ ├───month=12 │ │ │ ├───month=2 │ │ │ ├───month=3 │ │ │ ├───month=4 │ │ │ ├───month=5 │ │ │ ├───month=6 │ │ │ ├───month=7 │ │ │ ├───month=8 │ │ │ └───month=9 │ │ ├───year=2024 │ │ │ ├───month=1 │ │ │ ├───month=10 │ │ │ ├───month=11 │ │ │ ├───month=12 │ │ │ ├───month=2 │ │ │ ├───month=3 │ │ │ ├───month=4 │ │ │ ├───month=5 │ │ │ ├───month=6 │ │ │ ├───month=7 │ │ │ ├───month=8 │ │ │ └───month=9 │ │ └───year=2025 │ │ └───month=1 └───Ticks ├───EURUSD │ └───year=2025 │ ├───month=10 │ ├───month=11 │ ├───month=12 │ └───month=9 └───GBPUSD └───year=2025 ├───month=10 ├───month=11 ├───month=12 └───month=9
Перегрузка функций MetaTrader 5
И снова: в предыдущей статье мы смогли смоделировать некоторые торговые операции, хотя слишком сильно полагались на MetaTrader 5 для получения тиков, котировок и части критически важной информации. На этот раз мы хотим получить полностью или почти полностью изолированный пользовательский симулятор.
Сначала мы добавим экземпляр тестера. Это означает, что если пользователь запускает симулятор с аргументом IS_TESTER, установленным в True, то есть в режиме тестера стратегий, вместо извлечения критически важной информации, такой как котировки и тики, напрямую из MetaTrader 5, мы будем получать такую информацию из пользовательских путей, созданных в предыдущих разделах.
Когда IS_TESTER установлен в False, мы делаем обратное — извлекаем такие данные напрямую из MetaTrader 5.
class Simulator: def __init__(self, simulator_name: str, mt5_instance: mt5, deposit: float, leverage: str="1:100"): #... other variables self.IS_RUNNING = True # is the simulator running or stopped self.IS_TESTER = True # are we on the strategy tester mode or live trading self.symbol_info_cache: dict[str, namedtuple] = {} def Start(self, IS_TESTER: bool) -> bool: # simulator start self.IS_TESTER = IS_TESTER def Stop(self): # simulator stopped self.IS_RUNNING = False pass
symbol_info_tick
Теперь, когда у нас есть собственный способ хранения и чтения тиковых данных из локального пути, нам нужен способ возвращать такую информацию пользователю так же, как это делает клиент MetaTrader 5.
symbol_info_tick( symbol // financial instrument name )
Нам нужна аналогичная функция внутри класса Simulator. Функция должна решать, возвращать тики из MetaTrader 5 или тики внутри симулятора.
def symbol_info_tick(self, symbol: str) -> namedtuple: if self.IS_TESTER: return self.tick_cache[symbol] try: tick = self.mt5_instance.symbol_info_tick(symbol) except Exception as e: self.__GetLogger().warning(f"Failed. MT5 Error = {self.mt5_instance.last_error()}") return tick
Внутри класса симулятора у нас есть массив для отслеживания последних тиков.
В конструкторе класса:
self.tick_cache: dict[str, namedtuple] = {}
Однако этот симулятор должен получать эту тиковую информацию. Для такой задачи нам нужна функция.
def TickUpdate(self, symbol: str, tick: namedtuple): self.tick_cache[symbol] = tick
symbol_info
Эта функция получает с платформы MetaTrader 5 данные по указанному финансовому инструменту.
symbol_info(
symbol // financial instrument name
) Нам нужна аналогичная функция в нашем классе симулятора, но она не должна запрашивать эти данные из MetaTrader 5 более одного раза за время жизни симулятора.
После извлечения данных символа из MetaTrader 5 они должны сохраняться в массиве для последующего использования. Это снижает зависимость от MetaTrader 5 и улучшает общую производительность.
def symbol_info(self, symbol: str) -> namedtuple: """Gets data on the specified financial instrument.""" if symbol not in self.symbol_info_cache: info = self.mt5_instance.symbol_info(symbol) if info is None: return None self.symbol_info_cache[symbol] = info return self.symbol_info_cache[symbol]
Массив для временного хранения данных символов определяется аналогично массиву для хранения тиков, рассмотренному выше.
self.symbol_info_cache: dict[str, namedtuple] = {}
copy_rates_from
Эта функция получает бары из терминала MetaTrader 5, начиная с указанной даты, и возвращает заданное количество предыдущих баров.
copy_rates_from( symbol, // symbol name timeframe, // timeframe date_from, // initial bar open date count // number of bars )
В аналогичной функции нашего класса мы начинаем с того, что убеждаемся, что заданная начальная дата находится в формате UTC.
def copy_rates_from(self, symbol: str, timeframe: int, date_from: datetime, count: int) -> np.array: date_from = utils.ensure_utc(date_from)
Если пользователь выбрал режим тестера стратегий (IS_TESTER=True), мы получаем данные баров, сохранённые в Parquet-файлах.
if self.IS_TESTER: # instead of getting data from MetaTrader 5, get data stored in our custom directories path = os.path.join(config.BARS_HISTORY_DIR, symbol, utils.TIMEFRAMES_REV[timeframe]) lf = pl.scan_parquet(path) try: rates = ( lf .filter(pl.col("time") <= date_from) # get data starting at the given date .sort("time", descending=True) .limit(count) # limit the request to some bars .select([ pl.col("time").dt.epoch("s").cast(pl.Int64).alias("time"), pl.col("open"), pl.col("high"), pl.col("low"), pl.col("close"), pl.col("tick_volume"), pl.col("spread"), pl.col("real_volume"), ]) # return only what's required .collect(engine="streaming") # the streming engine, doesn't store data in memory ).to_dicts() rates = np.array(rates)[::-1] # reverse an array so it becomes oldest -> newest except Exception as e: config.tester_logger.warn(f"Failed to copy rates {e}") return np.array(dict()) else: rates = self.mt5_instance.copy_rates_from(symbol, timeframe, date_from, count) rates = np.array(self.__mt5_rates_to_dicts(rates)) if rates is None: config.simulator_logger.warn(f"Failed to copy rates. MetaTrader 5 error = {self.mt5_instance.last_error()}") return np.array(dict()) return rates
Если переменная класса IS_TESTER установлена в False, мы получаем данные баров напрямую из MetaTrader 5.
Поскольку MetaTrader 5 возвращает структурированный массив NumPy, преобразуем его в массив NumPy со словарями данных для каждого элемента массива. Это делает формат согласованным с форматом, полученным после преобразования объекта Polars DataFrame.
def __mt5_rates_to_dicts(self, rates) -> list[dict]: if rates is None or len(rates) == 0: return [] # structured numpy array from MT5 if rates.dtype.names is not None: return [ {name: r[name].item() if hasattr(r[name], "item") else r[name] for name in rates.dtype.names} for r in rates ]
Пример использования:
sim = Simulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500") start = datetime(2025, 1, 1) bars = 10 sim.Start(IS_TESTER=True) # start the simulator in the strategy tester mode rates = sim.copy_rates_from(symbol="EURUSD", timeframe=mt5.TIMEFRAME_H1, date_from=start, count=bars) print("is_tester=true\n", rates) sim.Start(IS_TESTER=False) # start the simulator in real-time trading rates = sim.copy_rates_from(symbol="EURUSD", timeframe=mt5.TIMEFRAME_H1, date_from=start, count=bars) print("is_tester=false\n",rates)
Вывод.
is_tester=true
[{'time': 1735653600, 'open': 1.04104, 'high': 1.04145, 'low': 1.03913, 'close': 1.03928, 'tick_volume': 2543, 'spread': 0, 'real_volume': 0}
{'time': 1735657200, 'open': 1.03929, 'high': 1.03973, 'low': 1.03836, 'close': 1.0393, 'tick_volume': 3171, 'spread': 0, 'real_volume': 0}
{'time': 1735660800, 'open': 1.03931, 'high': 1.03943, 'low': 1.03748, 'close': 1.03759, 'tick_volume': 4073, 'spread': 0, 'real_volume': 0}
{'time': 1735664400, 'open': 1.03759, 'high': 1.03893, 'low': 1.03527, 'close': 1.03548, 'tick_volume': 5531, 'spread': 0, 'real_volume': 0}
{'time': 1735668000, 'open': 1.03548, 'high': 1.03614, 'low': 1.0346899999999999, 'close': 1.03504, 'tick_volume': 3918, 'spread': 0, 'real_volume': 0}
{'time': 1735671600, 'open': 1.03504, 'high': 1.03551, 'low': 1.03442, 'close': 1.03493, 'tick_volume': 3279, 'spread': 0, 'real_volume': 0}
{'time': 1735675200, 'open': 1.0348600000000001, 'high': 1.03569, 'low': 1.03455, 'close': 1.0352999999999999, 'tick_volume': 2693, 'spread': 0, 'real_volume': 0}
{'time': 1735678800, 'open': 1.0352999999999999, 'high': 1.03647, 'low': 1.03516, 'close': 1.03548, 'tick_volume': 1840, 'spread': 0, 'real_volume': 0}
{'time': 1735682400, 'open': 1.03549, 'high': 1.03633, 'low': 1.03546, 'close': 1.03586, 'tick_volume': 1192, 'spread': 0, 'real_volume': 0}
{'time': 1735686000, 'open': 1.03586, 'high': 1.0361, 'low': 1.03527, 'close': 1.03527, 'tick_volume': 975, 'spread': 0, 'real_volume': 0}]
is_tester=false
[{'time': 1735653600, 'open': 1.04104, 'high': 1.04145, 'low': 1.03913, 'close': 1.03928, 'tick_volume': 2543, 'spread': 0, 'real_volume': 0}
{'time': 1735657200, 'open': 1.03929, 'high': 1.03973, 'low': 1.03836, 'close': 1.0393, 'tick_volume': 3171, 'spread': 0, 'real_volume': 0}
{'time': 1735660800, 'open': 1.03931, 'high': 1.03943, 'low': 1.03748, 'close': 1.03759, 'tick_volume': 4073, 'spread': 0, 'real_volume': 0}
{'time': 1735664400, 'open': 1.03759, 'high': 1.03893, 'low': 1.03527, 'close': 1.03548, 'tick_volume': 5531, 'spread': 0, 'real_volume': 0}
{'time': 1735668000, 'open': 1.03548, 'high': 1.03614, 'low': 1.0346899999999999, 'close': 1.03504, 'tick_volume': 3918, 'spread': 0, 'real_volume': 0}
{'time': 1735671600, 'open': 1.03504, 'high': 1.03551, 'low': 1.03442, 'close': 1.03493, 'tick_volume': 3279, 'spread': 0, 'real_volume': 0}
{'time': 1735675200, 'open': 1.0348600000000001, 'high': 1.03569, 'low': 1.03455, 'close': 1.0352999999999999, 'tick_volume': 2693, 'spread': 0, 'real_volume': 0}
{'time': 1735678800, 'open': 1.0352999999999999, 'high': 1.03647, 'low': 1.03516, 'close': 1.03548, 'tick_volume': 1840, 'spread': 0, 'real_volume': 0}
{'time': 1735682400, 'open': 1.03549, 'high': 1.03633, 'low': 1.03546, 'close': 1.03586, 'tick_volume': 1192, 'spread': 0, 'real_volume': 0}
{'time': 1735686000, 'open': 1.03586, 'high': 1.0361, 'low': 1.03527, 'close': 1.03527, 'tick_volume': 975, 'spread': 0, 'real_volume': 0}] copy_rates_from_pos
Согласно документации, эта функция получает бары из терминала MetaTrader 5, начиная с указанного индекса.
В индексе 0 находится текущий бар, а бар с наибольшим индексом является самым старым баром в терминале.
Это самая сложная из всех функций, копирующих информацию о барах из MetaTrader 5, просто потому что она зависит от времени.
Поскольку бар с индексом 0 всегда является текущим баром, это означает, что текущая функция должна знать время текущего тика. При запуске симулятора в так называемом тестере стратегий мы наследуем функцию copy_rates_from, которая принимает время начальной даты.
Мы передаём ей начальную дату:
текущее время + текущий таймфрейм в секундах * количество баров, запрошенное пользователем.
def copy_rates_from_pos(self, symbol: str, timeframe: int, start_pos: int, count: int) -> np.array: if self.tick is None or self.tick.time is None: self.__GetLogger().critical("Time information not found in the ticker, call the function 'TickUpdate' giving it the latest tick information") now = datetime.now(tz=timezone.utc) else: now = self.tick.time if self.IS_TESTER: rates = self.copy_rates_from(symbol=symbol, timeframe=timeframe, date_from=now+timedelta(seconds=utils.PeriodSeconds(timeframe)*start_pos), count=count) else: rates = self.mt5_instance.copy_rates_from_pos(symbol, timeframe, start_pos, count) rates = np.array(self.__mt5_rates_to_dicts(rates)) if rates is None: self.__GetLogger().warning(f"Failed to copy rates. MetaTrader 5 error = {self.mt5_instance.last_error()}") return np.array(dict()) return rates
Когда IS_TESTER=False, то есть система работает в реальном времени, симулятор получает бары напрямую из терминала MetaTrader 5.
Пример использования:
sim = Simulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500") start = datetime(2025, 1, 1) bars = 10 symbol = "EURUSD" timeframe = mt5.TIMEFRAME_H1 sim.Start(IS_TESTER=True) rates = sim.copy_rates_from_pos(symbol=symbol, timeframe=timeframe, start_pos=0, count=bars) print("is_tester=true\n", rates) sim.Start(IS_TESTER=False) # start the simulator in real-time trading rates = sim.copy_rates_from_pos(symbol=symbol, timeframe=timeframe, start_pos=0, count=bars) print("is_tester=false\n",rates)
Вывод.
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py 2025-12-25 12:42:33,366 | CRITICAL | tester | copy_rates_from_pos 221 --> Time information not found in the ticker, call the function 'TickUpdate' giving it the latest tick information is_tester=true [{'time': 1766584800, 'open': 1.17927, 'high': 1.17932, 'low': 1.1784, 'close': 1.17843, 'tick_volume': 1983, 'spread': 0, 'real_volume': 0} {'time': 1766588400, 'open': 1.17843, 'high': 1.17909, 'low': 1.17838, 'close': 1.17853, 'tick_volume': 2783, 'spread': 0, 'real_volume': 0} {'time': 1766592000, 'open': 1.17849, 'high': 1.17869, 'low': 1.17773, 'close': 1.17807, 'tick_volume': 2690, 'spread': 0, 'real_volume': 0} {'time': 1766595600, 'open': 1.17804, 'high': 1.17825, 'low': 1.17754, 'close': 1.17781, 'tick_volume': 2834, 'spread': 0, 'real_volume': 0} {'time': 1766599200, 'open': 1.17781, 'high': 1.1781, 'low': 1.17732, 'close': 1.17795, 'tick_volume': 2354, 'spread': 0, 'real_volume': 0} {'time': 1766602800, 'open': 1.17794, 'high': 1.17832, 'low': 1.17726, 'close': 1.17766, 'tick_volume': 1424, 'spread': 0, 'real_volume': 0} {'time': 1766606400, 'open': 1.17764, 'high': 1.17798, 'low': 1.17744, 'close': 1.17788, 'tick_volume': 1105, 'spread': 0, 'real_volume': 0} {'time': 1766610000, 'open': 1.17788, 'high': 1.1782, 'low': 1.17787, 'close': 1.17817, 'tick_volume': 654, 'spread': 0, 'real_volume': 0} {'time': 1766613600, 'open': 1.17817, 'high': 1.17819, 'low': 1.1779, 'close': 1.1779600000000001, 'tick_volume': 608, 'spread': 0, 'real_volume': 0} {'time': 1766617200, 'open': 1.1779600000000001, 'high': 1.17797, 'low': 1.17761, 'close': 1.17768, 'tick_volume': 1165, 'spread': 0, 'real_volume': 0}] 2025-12-25 12:42:33,394 | CRITICAL | simulator | copy_rates_from_pos 221 --> Time information not found in the ticker, call the function 'TickUpdate' giving it the latest tick information is_tester=false [{'time': 1766584800, 'open': 1.17927, 'high': 1.17932, 'low': 1.1784, 'close': 1.17843, 'tick_volume': 1983, 'spread': 0, 'real_volume': 0} {'time': 1766588400, 'open': 1.17843, 'high': 1.17909, 'low': 1.17838, 'close': 1.17853, 'tick_volume': 2783, 'spread': 0, 'real_volume': 0} {'time': 1766592000, 'open': 1.17849, 'high': 1.17869, 'low': 1.17773, 'close': 1.17807, 'tick_volume': 2690, 'spread': 0, 'real_volume': 0} {'time': 1766595600, 'open': 1.17804, 'high': 1.17825, 'low': 1.17754, 'close': 1.17781, 'tick_volume': 2834, 'spread': 0, 'real_volume': 0} {'time': 1766599200, 'open': 1.17781, 'high': 1.1781, 'low': 1.17732, 'close': 1.17795, 'tick_volume': 2354, 'spread': 0, 'real_volume': 0} {'time': 1766602800, 'open': 1.17794, 'high': 1.17832, 'low': 1.17726, 'close': 1.17766, 'tick_volume': 1424, 'spread': 0, 'real_volume': 0} {'time': 1766606400, 'open': 1.17764, 'high': 1.17798, 'low': 1.17744, 'close': 1.17788, 'tick_volume': 1105, 'spread': 0, 'real_volume': 0} {'time': 1766610000, 'open': 1.17788, 'high': 1.1782, 'low': 1.17787, 'close': 1.17817, 'tick_volume': 654, 'spread': 0, 'real_volume': 0} {'time': 1766613600, 'open': 1.17817, 'high': 1.17819, 'low': 1.1779, 'close': 1.1779600000000001, 'tick_volume': 608, 'spread': 0, 'real_volume': 0} {'time': 1766617200, 'open': 1.1779600000000001, 'high': 1.17797, 'low': 1.17761, 'close': 1.17768, 'tick_volume': 1165, 'spread': 0, 'real_volume': 0}]
copy_rates_range
Эта функция получает бары из терминала MetaTrader 5 в указанном диапазоне дат.
copy_rates_range( symbol, // symbol name timeframe, // timeframe date_from, // date the bars are requested from date_to // date, up to which the bars are requested )
В отличие от двух предыдущих, эта функция возвращает бары между двумя датами: date_from, то есть начальной датой, и date_to, то есть конечной датой.
def copy_rates_range(self, symbol: str, timeframe: int, date_from: datetime, date_to: datetime): date_from = utils.ensure_utc(date_from) date_to = utils.ensure_utc(date_to) if self.IS_TESTER: # instead of getting data from MetaTrader 5, get data stored in our custom directories path = os.path.join(config.BARS_HISTORY_DIR, symbol, utils.TIMEFRAMES_REV[timeframe]) lf = pl.scan_parquet(path) try: rates = ( lf .filter( (pl.col("time") >= pl.lit(date_from)) & (pl.col("time") <= pl.lit(date_to)) ) # get bars between date_from and date_to .sort("time", descending=True) .select([ pl.col("time").dt.epoch("s").cast(pl.Int64).alias("time"), pl.col("open"), pl.col("high"), pl.col("low"), pl.col("close"), pl.col("tick_volume"), pl.col("spread"), pl.col("real_volume"), ]) # return only what's required .collect(engine="streaming") # the streming engine, doesn't store data in memory ).to_dicts() rates = np.array(rates)[::-1] # reverse an array so it becomes oldest -> newest except Exception as e: self.__GetLogger().warning(f"Failed to copy rates {e}") return np.array(dict()) else: rates = self.mt5_instance.copy_rates_range(symbol, timeframe, date_from, date_to) rates = np.array(self.__mt5_rates_to_dicts(rates)) if rates is None: self.__GetLogger().warning(f"Failed to copy rates. MetaTrader 5 error = {self.mt5_instance.last_error()}") return np.array(dict()) return rates
copy_ticks_from
Согласно документации, эта функция получает тики из терминала MetaTrader 5, начиная с указанной даты.
copy_ticks_from( symbol, // symbol name date_from, // date the ticks are requested from count, // number of requested ticks flags // combination of flags defining the type of requested ticks )
В аналогичной функции внутри нашего класса симулятора мы читаем тики из нашей базы данных, когда пользователь выбрал режим тестера стратегий (IS_TESTER=True), а в противоположном случае читаем их напрямую из MetaTrader 5.
def copy_ticks_from(self, symbol: str, date_from: datetime, count: int, flags: int=mt5.COPY_TICKS_ALL) -> np.array: date_from = utils.ensure_utc(date_from) flag_mask = self.__tick_flag_mask(flags) if self.IS_TESTER: path = os.path.join(config.TICKS_HISTORY_DIR, symbol) lf = pl.scan_parquet(path) try: ticks = ( lf .filter(pl.col("time") >= pl.lit(date_from)) # get data starting at the given date .filter((pl.col("flags") & flag_mask) != 0) .sort( ["time", "time_msc"], descending=[False, False] ) .limit(count) # limit the request to a specified number of ticks .select([ pl.col("time").dt.epoch("s").cast(pl.Int64).alias("time"), pl.col("bid"), pl.col("ask"), pl.col("last"), pl.col("volume"), pl.col("time_msc"), pl.col("flags"), pl.col("volume_real"), ]) .collect(engine="streaming") # the streming engine, doesn't store data in memory ).to_dicts() ticks = np.array(ticks) except Exception as e: self.__GetLogger().warning(f"Failed to copy ticks {e}") return np.array(dict()) else: ticks = self.mt5_instance.copy_ticks_from(symbol, date_from, count, flags) ticks = np.array(self.__mt5_data_to_dicts(ticks)) if ticks is None: self.__GetLogger().warning(f"Failed to copy ticks. MetaTrader 5 error = {self.mt5_instance.last_error()}") return np.array(dict()) return ticks
Поскольку запрос тиков включает параметр flags, позволяющий пользователям выбирать, какие именно тики они хотят получить, нам нужен способ создать маску флагов, полезную для фильтрации тиков в зависимости от потребностей пользователя.
Согласно документации:
Флаг определяет тип запрашиваемых тиков.
Значения флагов описаны в перечислении COPY_TICKS .
| ID | Описание |
|---|---|
| COPY_TICKS_ALL | все тики |
| COPY_TICKS_INFO | тики, содержащие изменения цены Bid и/или Ask |
| COPY_TICKS_TRADE | тики, содержащие изменения Last и/или Volume |
TICK_FLAG определяет возможные флаги для тиков. Эти флаги используются для описания тиков, полученных функциями copy_ticks_from() и copy_ticks_range().
| ID | Описание |
|---|---|
| TICK_FLAG_BID | цена Bid изменилась |
| TICK_FLAG_ASK | цена Ask изменилась |
| TICK_FLAG_LAST | цена Last изменилась |
| TICK_FLAG_VOLUME | объём изменился |
| TICK_FLAG_BUY | последняя цена Buy изменилась |
| TICK_FLAG_SELL | последняя цена Sell изменилась |
def __tick_flag_mask(self, flags: int) -> int: if flags == mt5.COPY_TICKS_ALL: return ( mt5.TICK_FLAG_BID | mt5.TICK_FLAG_ASK | mt5.TICK_FLAG_LAST | mt5.TICK_FLAG_VOLUME | mt5.TICK_FLAG_BUY | mt5.TICK_FLAG_SELL ) mask = 0 if flags & mt5.COPY_TICKS_INFO: mask |= mt5.TICK_FLAG_BID | mt5.TICK_FLAG_ASK if flags & mt5.COPY_TICKS_TRADE: mask |= mt5.TICK_FLAG_LAST | mt5.TICK_FLAG_VOLUME return mask
Пример использования:
sim = Simulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500") start = datetime(2025, 1, 1) end = datetime(2025, 1, 5) bars = 10 symbol = "EURUSD" timeframe = mt5.TIMEFRAME_H1 sim.Start(IS_TESTER=True) # start simulation in the strategy tester ticks = sim.copy_ticks_from(symbol=symbol, date_from=start.replace(month=12, hour=0, minute=0), count=bars) print("is_tester=true\n", ticks) sim.Start(IS_TESTER=False) # start the simulator in real-time trading ticks = sim.copy_ticks_from(symbol=symbol, date_from=start.replace(month=12, hour=0, minute=0), count=bars) print("is_tester=false\n", ticks)
Вывод.
is_tester=true
[{'time': 1764547200, 'bid': 1.15936, 'ask': 1.1596899999999999, 'last': 0.0, 'volume': 0, 'time_msc': 1764547200174, 'flags': 134, 'volume_real': 0.0}
{'time': 1764547206, 'bid': 1.15934, 'ask': 1.15962, 'last': 0.0, 'volume': 0, 'time_msc': 1764547206476, 'flags': 134, 'volume_real': 0.0}
{'time': 1764547211, 'bid': 1.1593499999999999, 'ask': 1.15997, 'last': 0.0, 'volume': 0, 'time_msc': 1764547211273, 'flags': 134, 'volume_real': 0.0}
{'time': 1764547215, 'bid': 1.15936, 'ask': 1.15979, 'last': 0.0, 'volume': 0, 'time_msc': 1764547215872, 'flags': 134, 'volume_real': 0.0}
{'time': 1764547221, 'bid': 1.15936, 'ask': 1.15964, 'last': 0.0, 'volume': 0, 'time_msc': 1764547221475, 'flags': 4, 'volume_real': 0.0}
{'time': 1764547231, 'bid': 1.1593499999999999, 'ask': 1.15997, 'last': 0.0, 'volume': 0, 'time_msc': 1764547231674, 'flags': 134, 'volume_real': 0.0}
{'time': 1764547260, 'bid': 1.1593499999999999, 'ask': 1.15997, 'last': 0.0, 'volume': 0, 'time_msc': 1764547260073, 'flags': 130, 'volume_real': 0.0}
{'time': 1764547265, 'bid': 1.15892, 'ask': 1.15998, 'last': 0.0, 'volume': 0, 'time_msc': 1764547265485, 'flags': 134, 'volume_real': 0.0}
{'time': 1764547320, 'bid': 1.15892, 'ask': 1.15998, 'last': 0.0, 'volume': 0, 'time_msc': 1764547320074, 'flags': 130, 'volume_real': 0.0}
{'time': 1764547345, 'bid': 1.15894, 'ask': 1.15998, 'last': 0.0, 'volume': 0, 'time_msc': 1764547345872, 'flags': 134, 'volume_real': 0.0}]
is_tester=false
[{'time': 1764547200, 'bid': 1.15936, 'ask': 1.1596899999999999, 'last': 0.0, 'volume': 0, 'time_msc': 1764547200174, 'flags': 134, 'volume_real': 0.0}
{'time': 1764547206, 'bid': 1.15934, 'ask': 1.15962, 'last': 0.0, 'volume': 0, 'time_msc': 1764547206476, 'flags': 134, 'volume_real': 0.0}
{'time': 1764547211, 'bid': 1.1593499999999999, 'ask': 1.15997, 'last': 0.0, 'volume': 0, 'time_msc': 1764547211273, 'flags': 134, 'volume_real': 0.0}
{'time': 1764547215, 'bid': 1.15936, 'ask': 1.15979, 'last': 0.0, 'volume': 0, 'time_msc': 1764547215872, 'flags': 134, 'volume_real': 0.0}
{'time': 1764547221, 'bid': 1.15936, 'ask': 1.15964, 'last': 0.0, 'volume': 0, 'time_msc': 1764547221475, 'flags': 4, 'volume_real': 0.0}
{'time': 1764547231, 'bid': 1.1593499999999999, 'ask': 1.15997, 'last': 0.0, 'volume': 0, 'time_msc': 1764547231674, 'flags': 134, 'volume_real': 0.0}
{'time': 1764547260, 'bid': 1.1593499999999999, 'ask': 1.15997, 'last': 0.0, 'volume': 0, 'time_msc': 1764547260073, 'flags': 130, 'volume_real': 0.0}
{'time': 1764547265, 'bid': 1.15892, 'ask': 1.15998, 'last': 0.0, 'volume': 0, 'time_msc': 1764547265485, 'flags': 134, 'volume_real': 0.0}
{'time': 1764547320, 'bid': 1.15892, 'ask': 1.15998, 'last': 0.0, 'volume': 0, 'time_msc': 1764547320074, 'flags': 130, 'volume_real': 0.0}
{'time': 1764547345, 'bid': 1.15894, 'ask': 1.15998, 'last': 0.0, 'volume': 0, 'time_msc': 1764547345872, 'flags': 134, 'volume_real': 0.0}] copy_ticks_range
Согласно документации, эта функция получает тики из терминала MetaTrader 5 за указанный диапазон дат.
Сигнатура функции:
copy_ticks_range( symbol, // symbol name date_from, // date the ticks are requested from date_to, // date, up to which the ticks are requested flags // combination of flags defining the type of requested ticks )
Ниже приведена аналогичная реализация функции внутри класса Simulator.
def copy_ticks_range(self, symbol: str, date_from: datetime, date_to: datetime, flags: int=mt5.COPY_TICKS_ALL) -> np.array: date_from = utils.ensure_utc(date_from) date_to = utils.ensure_utc(date_to) flag_mask = self.__tick_flag_mask(flags) if self.IS_TESTER: path = os.path.join(config.TICKS_HISTORY_DIR, symbol) lf = pl.scan_parquet(path) try: ticks = ( lf .filter( (pl.col("time") >= pl.lit(date_from)) & (pl.col("time") <= pl.lit(date_to)) ) # get ticks between date_from and date_to .filter((pl.col("flags") & flag_mask) != 0) .sort( ["time", "time_msc"], descending=[False, False] ) .select([ pl.col("time").dt.epoch("s").cast(pl.Int64).alias("time"), pl.col("bid"), pl.col("ask"), pl.col("last"), pl.col("volume"), pl.col("time_msc"), pl.col("flags"), pl.col("volume_real"), ]) .collect(engine="streaming") # the streaming engine, doesn't store data in memory ).to_dicts() ticks = np.array(ticks) except Exception as e: self.__GetLogger().warning(f"Failed to copy ticks {e}") return np.array(dict()) else: ticks = self.mt5_instance.copy_ticks_range(symbol, date_from, date_to, flags) ticks = np.array(self.__mt5_data_to_dicts(ticks)) if ticks is None: self.__GetLogger().warning(f"Failed to copy ticks. MetaTrader 5 error = {self.mt5_instance.last_error()}") return np.array(dict()) return ticks
Пример использования:
sim = Simulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500") sim.Start(IS_TESTER=True) # start simulation in the strategy tester ticks = sim.copy_ticks_range(symbol=symbol, date_from=start.replace(month=12, hour=0, minute=0), date_to=end.replace(month=12, hour=0, minute=5)) print("is_tester=true\n", ticks) sim.Start(IS_TESTER=False) # start the simulator in real-time trading ticks = sim.copy_ticks_range(symbol=symbol, date_from=start.replace(month=12, hour=0, minute=0), date_to=end.replace(month=12, hour=0, minute=5)) print("is_tester=false\n", ticks)
Вывод.
is_tester=true
[{'time': 1764547200, 'bid': 1.15936, 'ask': 1.1596899999999999, 'last': 0.0, 'volume': 0, 'time_msc': 1764547200174, 'flags': 134, 'volume_real': 0.0}
{'time': 1764547206, 'bid': 1.15934, 'ask': 1.15962, 'last': 0.0, 'volume': 0, 'time_msc': 1764547206476, 'flags': 134, 'volume_real': 0.0}
{'time': 1764547211, 'bid': 1.1593499999999999, 'ask': 1.15997, 'last': 0.0, 'volume': 0, 'time_msc': 1764547211273, 'flags': 134, 'volume_real': 0.0}
...
{'time': 1764550799, 'bid': 1.15965, 'ask': 1.16006, 'last': 0.0, 'volume': 0, 'time_msc': 1764550799475, 'flags': 134, 'volume_real': 0.0}
{'time': 1764550799, 'bid': 1.15971, 'ask': 1.16011, 'last': 0.0, 'volume': 0, 'time_msc': 1764550799669, 'flags': 134, 'volume_real': 0.0}
{'time': 1764550799, 'bid': 1.15965, 'ask': 1.16006, 'last': 0.0, 'volume': 0, 'time_msc': 1764550799877, 'flags': 134, 'volume_real': 0.0}]
is_tester=false
[{'time': 1764547200, 'bid': 1.15936, 'ask': 1.1596899999999999, 'last': 0.0, 'volume': 0, 'time_msc': 1764547200174, 'flags': 134, 'volume_real': 0.0}
{'time': 1764547206, 'bid': 1.15934, 'ask': 1.15962, 'last': 0.0, 'volume': 0, 'time_msc': 1764547206476, 'flags': 134, 'volume_real': 0.0}
{'time': 1764547211, 'bid': 1.1593499999999999, 'ask': 1.15997, 'last': 0.0, 'volume': 0, 'time_msc': 1764547211273, 'flags': 134, 'volume_real': 0.0}
...
{'time': 1764893040, 'bid': 1.16424, 'ask': 1.16479, 'last': 0.0, 'volume': 0, 'time_msc': 1764893040071, 'flags': 130, 'volume_real': 0.0}
{'time': 1764893061, 'bid': 1.16424, 'ask': 1.16479, 'last': 0.0, 'volume': 0, 'time_msc': 1764893061887, 'flags': 4, 'volume_real': 0.0}
{'time': 1764893096, 'bid': 1.16424, 'ask': 1.16482, 'last': 0.0, 'volume': 0, 'time_msc': 1764893096077, 'flags': 4, 'volume_real': 0.0}] В предыдущей статье у нас были пользовательские функции для получения информации об открытых позициях, ордерах, сделках и так далее. На этот раз мы перегрузим все эти функции с использованием синтаксиса модуля Python–MetaTrader5.
orders_total
Согласно документации, эта функция получает количество активных ордеров из терминала MetaTrader 5
orders_total()
Она возвращает целочисленное значение.
Если симулятор работает в режиме тестера стратегий, функция возвращает количество ордеров, хранящихся в контейнере симулированных ордеров; в противном случае она возвращает ордера из клиента MetaTrader 5.
def orders_total(self) -> int: """Get the number of active orders. Returns (int): The number of active orders in either a simulator or MetaTrader 5 """ return len(self.orders_container) if self.IS_TESTER else self.mt5_instance.orders_total()
orders_get
Согласно документации,эта функция получает активные ордера с возможностью фильтрации по символу или тикету. Есть три варианта вызова.
orders_get()
Вызов с указанием символа, по которому нужно получить активные ордера.
orders_get(
symbol="SYMBOL" // symbol name
) Вызов с указанием группы символов, по которым нужно получить активные ордера.
orders_get( group="GROUP" // filter for selecting orders for symbols )
Вызов с указанием тикета ордера.
orders_get( ticket=TICKET // ticket )
Эта функция возвращает информацию в виде структуры именованного кортежа (namedtuple). В случае ошибки возвращает None. Информацию об ошибке можно получить с помощью last_error().
Чтобы наш симулятор был максимально похож на терминал MetaTrader 5, мы должны возвращать похожий тип данных (namedtuple).
from collections import namedtuple
Мы можем определить эквивалентную функцию в нашем симуляторе следующим образом:
def orders_get(self, symbol: Optional[str] = None, group: Optional[str] = None, ticket: Optional[int] = None) -> namedtuple: """G et active orders with the ability to filter by symbol or ticket. Есть три варианта вызова. Returns: list: Returns info in the form of a named tuple structure (namedtuple). В случае ошибки возвращает None. Информацию об ошибке можно получить с помощью last_error(). """
Нам нужно не только вернуть так называемый namedtuple, но и обеспечить похожее содержимое этого типа данных.
def __init__(self, simulator_name: str, mt5_instance: mt5, deposit: float, leverage: str="1:100"): # ----------------- TradeOrder -------------------------- self.TradeOrder = namedtuple( "TradeOrder", [ "ticket", "time_setup", "time_setup_msc", "time_done", "time_done_msc", "time_expiration", "type", "type_time", "type_filling", "state", "magic", "position_id", "position_by_id", "reason", "volume_initial", "volume_current", "price_open", "sl", "tp", "price_current", "price_stoplimit", "symbol", "comment", "external_id", ] )
Ниже приведена аналогичная функция в нашем классе Simulator.
def orders_get(self, symbol: Optional[str] = None, group: Optional[str] = None, ticket: Optional[int] = None) -> namedtuple: self.__orders_container__.extend([order1, order2]) if self.IS_TESTER: orders = self.__orders_container__ # no filters → return all orders if symbol is None and group is None and ticket is None: return tuple(orders) # symbol filter (highest priority) if symbol is not None: return tuple(o for o in orders if o.symbol == symbol) # group filter if group is not None: return tuple(o for o in orders if fnmatch.fnmatch(o.symbol, group)) # ticket filter if ticket is not None: return tuple(o for o in orders if o.ticket == ticket) return tuple() try: if symbol is not None: return self.mt5_instance.orders_get(symbol=symbol) if group is not None: return self.mt5_instance.orders_get(group=group) if ticket is not None: return self.mt5_instance.orders_get(ticket=ticket) return self.mt5_instance.orders_get() except Exception: return None
Если пользователь выбирает режим тестера стратегий (IS_TESTER=True), мы получаем ордера и информацию о них изнутри симулятора; в противном случае извлекаем их из терминала MetaTrader 5.
С двумя отложенными ордерами в моём терминале MetaTrader 5:

И две симулированные сделки:
order1 = self.TradeOrder( ticket=123456, time_setup=int(datetime.now().timestamp()), time_setup_msc=int(datetime.now().timestamp() * 1000), time_done=0, time_done_msc=0, time_expiration=0, type=mt5.ORDER_TYPE_BUY_LIMIT, type_time=0, type_filling=mt5.ORDER_FILLING_RETURN, state=mt5.ORDER_STATE_PLACED, magic=0, position_id=0, position_by_id=0, reason=0, volume_initial=0.01, volume_current=0.01, price_open=1.1750, sl=1.1700, tp=1.1800, price_current=1.1750, price_stoplimit=0.0, symbol="GBPUSD", comment="", external_id="", ) order2 = self.TradeOrder( ticket=123457, time_setup=int(datetime.now().timestamp()), time_setup_msc=int(datetime.now().timestamp() * 1000), time_done=0, time_done_msc=0, time_expiration=0, type=mt5.ORDER_TYPE_SELL_LIMIT, type_time=0, type_filling=mt5.ORDER_FILLING_RETURN, state=mt5.ORDER_STATE_PLACED, magic=0, position_id=0, position_by_id=0, reason=0, volume_initial=0.01, volume_current=0.01, price_open=1.1800, sl=1.1850, tp=1.1700, price_current=1.1800, price_stoplimit=0.0, symbol="EURUSD", comment="", external_id="", ) self.__orders_container__.extend([order1, order2])
Затем мы проверяем наличие ордеров как в MetaTrader 5, так и в симуляторе.
sim = Simulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500") sim.Start(IS_TESTER=True) # start simulation in the strategy tester print("Orders in the simulator:\n", sim.orders_get()) sim.Start(IS_TESTER=False) # start the simulator in real-time trading print("Orders in MetaTrader 5:\n", sim.orders_get())
Вывод:
Ордера в симуляторе: (TradeOrder(ticket=123456, time_setup=1766749779, time_setup_msc=1766749779726, time_done=0, time_done_msc=0, time_expiration=0, type=2, type_time=0, type_filling=2, state=1, magic=0, position_id=0, position_by_id=0, reason=0, volume_initial=0.01, volume_current=0.01, price_open=1.175, sl=1.17, tp=1.18, price_current=1.175, price_stoplimit=0.0, symbol='GBPUSD', comment='', external_id=''), TradeOrder(ticket=123457, time_setup=1766749779, time_setup_msc=1766749779726, time_done=0, time_done_msc=0, time_expiration=0, type=3, type_time=0, type_filling=2, state=1, magic=0, position_id=0, position_by_id=0, reason=0, volume_initial=0.01, volume_current=0.01, price_open=1.18, sl=1.185, tp=1.17, price_current=1.18, price_stoplimit=0.0, symbol='EURUSD', comment='', external_id='')) Orders in MetaTrader 5: (TradeOrder(ticket=1381968725, time_setup=1766748043, time_setup_msc=1766748043247, time_done=0, time_done_msc=0, time_expiration=0, type=2, type_time=0, type_filling=2, state=1, magic=0, position_id=0, position_by_id=0, reason=0, volume_initial=0.01, volume_current=0.01, price_open=1.17414, sl=0.0, tp=0.0, price_current=1.17769, price_stoplimit=0.0, symbol='EURUSD', comment='', external_id=''), TradeOrder(ticket=1381968767, time_setup=1766748049, time_setup_msc=1766748049051, time_done=0, time_done_msc=0, time_expiration=0, type=3, type_time=0, type_filling=2, state=1, magic=0, position_id=0, position_by_id=0, reason=0, volume_initial=0.01, volume_current=0.01, price_open=1.17949, sl=0.0, tp=0.0, price_current=1.17769, price_stoplimit=0.0, symbol='EURUSD', comment='', external_id=''))
positions_total
Согласно документации, эта функция возвращает количество открытых позиций в клиенте MetaTrader 5.
positions_total()
Ниже приведён аналогичный метод в симуляторе.
def positions_total(self) -> int: """Get the number of open positions in MetaTrader 5 client. Returns: int: number of positions """ if self.IS_TESTER: return len(self.__positions_container__) try: total = self.mt5_instance.positions_total() except Exception as e: self.__GetLogger().error(f"MetaTrader5 error = {e}") return -1 return total
positions_get
Этот метод выглядит и работает аналогично методу orders_get, который мы только что рассмотрели выше.
Функция получает открытые позиции с возможностью фильтрации по символу или тикету. У неё есть три варианта вызова.
Вызов без параметров возвращает открытые позиции по всем символам.
positions_get()
Вызов с указанием символа возвращает открытые позиции по указанному инструменту.
positions_get(
symbol="SYMBOL" // symbol name
) Вызов с указанием группы символов, по которым должны быть получены открытые позиции.
positions_get( group="GROUP" // filter for selecting positions by symbols )
Вызов с указанием тикета позиции.
positions_get( ticket=TICKET // ticket )
Подобно методу orders_get, этот метод возвращает данные в виде структуры именованного кортежа namedtuple. В случае ошибки возвращает None. Информацию об ошибке можно получить с помощью last_error().
С учётом этого нам нужна аналогичная структура для хранения информации о позициях в нашем симуляторе — похожая на ту, которую возвращает модуль MetaTrader 5-Python.
def positions_get(self, symbol: Optional[str] = None, group: Optional[str] = None, ticket: Optional[int] = None) -> namedtuple: if self.IS_TESTER: positions = self.__positions_container__ # no filters → return all positions if symbol is None and group is None and ticket is None: return tuple(positions) # symbol filter (highest priority) if symbol is not None: return tuple(o for o in positions if o.symbol == symbol) # group filter if group is not None: return tuple(o for o in positions if fnmatch.fnmatch(o.symbol, group)) # ticket filter if ticket is not None: return tuple(o for o in positions if o.ticket == ticket) return tuple() try: if symbol is not None: return self.mt5_instance.positions_get(symbol=symbol) if group is not None: return self.mt5_instance.positions_get(group=group) if ticket is not None: return self.mt5_instance.positions_get(ticket=ticket) return self.mt5_instance.positions_get() except Exception: return None
Пример использования:
sim = Simulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500") sim.Start(IS_TESTER=True) # start simulation in the strategy tester print("positions total in the Simulator: ",sim.positions_total()) print("positions in the Simulator:\n",sim.positions_get()) sim.Start(IS_TESTER=False) # start the simulator in real-time trading print("positions total in MetaTrader5: ",sim.positions_total()) print("positions in MetaTraer5:\n",sim.positions_get())
Вывод.
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py positions total in the Simulator: 0 positions in the Simulator: () positions total in MetaTrader5: 2 positions in MetaTraer5: (TradePosition(ticket=1381981938, time=1766748992, time_msc=1766748992425, time_update=1766748992, time_update_msc=1766748992425, type=0, magic=0, identifier=1381981938, reason=0, volume=0.01, price_open=1.17688, sl=0.0, tp=0.0, price_current=1.17755, swap=0.0, profit=0.67, symbol='EURUSD', comment='', external_id=''), TradePosition(ticket=1381981988, time=1766748994, time_msc=1766748994018, time_update=1766748994, time_update_msc=1766748994018, type=1, magic=0, identifier=1381981988, reason=0, volume=0.01, price_open=1.17688, sl=0.0, tp=0.0, price_current=1.17755, swap=0.0, profit=-0.67, symbol='EURUSD', comment='', external_id=''))
history_orders_total
Согласно документации, этот метод получает количество ордеров в торговой истории за определённый временной интервал.
history_orders_total(
date_from, // date the orders are requested from
date_to // date, up to which the orders are requested
) Параметры:
- date_from: дата, начиная с которой запрашиваются ордера. Задаётся объектом datetime или количеством секунд, прошедших с 1970.01.01.
- date_to: дата, до которой запрашиваются ордера. Задаётся объектом datetime или количеством секунд, прошедших с 1970.01.01.
Аналогичная функция в симуляторе может быть реализована следующим образом:
def history_orders_total(self, date_from: datetime, date_to: datetime) -> int: # date range is a requirement if date_from is None or date_to is None: self.__GetLogger().error("date_from and date_to must be specified") return None date_from = utils.ensure_utc(date_from) date_to = utils.ensure_utc(date_to) if self.IS_TESTER: date_from_ts = int(date_from.timestamp()) date_to_ts = int(date_to.timestamp()) return sum( 1 for o in self.__orders_history_container__ if date_from_ts <= o.time_setup <= date_to_ts ) try: total = self.mt5_instance.history_orders_total(date_from, date_to) except Exception as e: self.__GetLogger().error(f"MetaTrader5 error = {e}") return -1 return total
Пример использования:
sim.Start(IS_TESTER=True) # start simulation in the strategy tester date_to = datetime.now() date_from = date_to - timedelta(days=1) print(sim.history_orders_total(date_from=date_from,date_to=date_to)) sim.Start(IS_TESTER=False) # start the simulator in real-time trading print(sim.history_orders_total(date_from=date_from,date_to=date_to))
Вывод.
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py orders in the last 24 hours in the Simulator: 0 orders in the last 24 hours in MetaTrader5: 3
history_orders_get
Согласно документации, этот метод получает ордера из торговой истории с возможностью фильтрации по тикету или позиции.
Он возвращает все ордера, попадающие в указанный интервал.
У неё есть три варианта вызова.
history_orders_get( date_from, // date the orders are requested from date_to, // date, up to which the orders are requested group="GROUP" // filter for selecting orders by symbols )
Вызов с указанием тикета ордера. Возвращает ордер с указанным тикетом.
history_orders_get( ticket=TICKET // order ticket )
Вызов с указанием тикета позиции. Возвращает все ордера с тикетом позиции, указанным в свойстве ORDER_POSITION_ID .
history_orders_get( position=POSITION // position ticket )
Как и внутри функции history_orders_total, мы читаем всю информацию из массива с именем __orders_history_container__ с дополнительными фильтрами по тикету (ticket, тикет ордера), позиции (position, тикет сохранённой позиции) и группе (group, фильтр для выбора нужной группы символов).
def history_orders_get(self, date_from: datetime, date_to: datetime, group: Optional[str] = None, ticket: Optional[int] = None, position: Optional[int] = None ) -> namedtuple: if self.IS_TESTER: orders = self.__orders_history_container__ # ticket filter (highest priority) if ticket is not None: return tuple(o for o in orders if o.ticket == ticket) # position filter if position is not None: return tuple(o for o in orders if o.position_id == position) # date range is a requirement if date_from is None or date_to is None: self.__GetLogger().error("date_from and date_to must be specified") return None date_from_ts = int(utils.ensure_utc(date_from).timestamp()) date_to_ts = int(utils.ensure_utc(date_to).timestamp()) filtered = ( o for o in orders if date_from_ts <= o.time_setup <= date_to_ts ) # obtain orders that fall within this time range # optional group filter if group is not None: filtered = ( o for o in filtered if fnmatch.fnmatch(o.symbol, group) ) return tuple(filtered) try: # we are not on the strategy tester simulation if ticket is not None: return self.mt5_instance.history_orders_get(date_from, date_to, ticket=ticket) if position is not None: return self.mt5_instance.history_orders_get(date_from, date_to, position=position) if date_from is None or date_to is None: raise ValueError("date_from and date_to are required") date_from = utils.ensure_utc(date_from) date_to = utils.ensure_utc(date_to) if group is not None: return self.mt5_instance.history_orders_get( date_from, date_to, group=group ) return self.mt5_instance.history_orders_get(date_from, date_to) except Exception as e: self.__GetLogger().error(f"MetaTrader5 error = {e}") return None
history_deals_total
Согласно документации, эта функция получает количество сделок в торговой истории за указанный интервал.
history_deals_total(
date_from, // date the deals are requested from
date_to // date, up to which the deals are requested
) Параметры:
- date_from: Дата, начиная с которой запрашиваются сделки. Задаётся объектом datetime или количеством секунд, прошедших с 1970.01.01.
- date_to: Дата, до которой запрашиваются сделки. Задаётся объектом datetime или количеством секунд, прошедших с 1970.01.01.
def history_deals_total(self, date_from: datetime, date_to: datetime) -> int: """ Get the number of deals in history within the specified date range. Args: date_from (datetime): Date the orders are requested from. Задаётся объектом datetime или количеством секунд, прошедших с 1970.01.01. date_to (datetime, required): Date, up to which the orders are requested. Задаётся объектом datetime или количеством секунд, прошедших с 1970.01.01. Returns: An integer value. """ if date_from is None or date_to is None: self.__GetLogger().error("date_from and date_to must be specified") return -1 date_from = utils.ensure_utc(date_from) date_to = utils.ensure_utc(date_to) if self.IS_TESTER: date_from_ts = int(date_from.timestamp()) date_to_ts = int(date_to.timestamp()) return sum( 1 for d in self.__deals_history_container__ if date_from_ts <= d.time <= date_to_ts ) try: return self.mt5_instance.history_deals_total(date_from, date_to) except Exception as e: self.__GetLogger().error(f"MetaTrader5 error = {e}") return -1
Пример использования:
sim = Simulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500") date_to = datetime.now() date_from = date_to - timedelta(days=1) print("Total deals in the last 24 hours in MetaTrader5:", sim.history_deals_total(date_from=date_from,date_to=date_to)) sim.Start(IS_TESTER=False) # start the simulator in real-time trading print("Total deals in the last 24 hours in MetaTrader5:", sim.history_deals_total(date_from=date_from,date_to=date_to))
Вывод.
Total deals in the last 24 hours in MetaTrader5: 0 Total deals in the last 24 hours in MetaTrader5: 3
history_deals_get
Согласно документации, этот метод получает сделки из торговой истории за указанный временной интервал с возможностью фильтрации по тикету или позиции.
Функция имеет три варианта вызова.
history_deals_get( date_from, // date the deals are requested from date_to, // date, up to which the deals are requested group="GROUP" // filter for selecting deals for symbols )
Вызов с указанием тикета ордера. Возвращает все сделки, у которых указанный тикет ордера находится в свойстве DEAL_ORDER .
history_deals_get( ticket=TICKET // order ticket )
Вызов с указанием тикета позиции. Возвращает все сделки, у которых указанный тикет позиции находится в свойстве DEAL_POSITION_ID .
history_deals_get( position=POSITION // position ticket )
В классе Simulator мы создадим метод с таким же именем. Когда пользователь выбирает режим тестера стратегий (IS_TESTER=True), история сделок извлекается из массива внутри симулятора; в противном случае такая информация извлекается напрямую из клиента MetaTrader 5.
def history_deals_get(self, date_from: datetime, date_to: datetime, group: Optional[str] = None, ticket: Optional[int] = None, position: Optional[int] = None ) -> namedtuple: if self.IS_TESTER: deals = self.__deals_history_container__ # ticket filter (highest priority) if ticket is not None: return tuple(d for d in deals if d.ticket == ticket) # position filter if position is not None: return tuple(d for d in deals if d.position_id == position) # date range is a requirement if date_from is None or date_to is None: self.__GetLogger().error("date_from and date_to must be specified") return None date_from_ts = int(utils.ensure_utc(date_from).timestamp()) date_to_ts = int(utils.ensure_utc(date_to).timestamp()) filtered = ( d for d in deals if date_from_ts <= d.time <= date_to_ts ) # obtain orders that fall within this time range # optional group filter if group is not None: filtered = ( d for d in filtered if fnmatch.fnmatch(d.symbol, group) ) return tuple(filtered) try: # we are not on the strategy tester simulation if ticket is not None: return self.mt5_instance.history_deals_get(date_from, date_to, ticket=ticket) if position is not None: return self.mt5_instance.history_deals_get(date_from, date_to, position=position) if date_from is None or date_to is None: raise ValueError("date_from and date_to are required") date_from = utils.ensure_utc(date_from) date_to = utils.ensure_utc(date_to) if group is not None: return self.mt5_instance.history_deals_get( date_from, date_to, group=group ) return self.mt5_instance.history_deals_get(date_from, date_to) except Exception as e: self.__GetLogger().error(f"MetaTrader5 error = {e}") return None
Пример использования:
sim = Simulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500") sim.Start(IS_TESTER=True) # start simulation in the strategy tester date_to = datetime.now() date_from = date_to - timedelta(days=1) print("deals in the last 24 hours in the Simulator:\n", sim.history_deals_get(date_from=date_from,date_to=date_to)) sim.Start(IS_TESTER=False) # start the simulator in real-time trading print("Deals in the last 24 hours in MetaTrader5:\n", sim.history_deals_get(date_from=date_from,date_to=date_to))
Вывод.
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py deals in the last 24 hours in the Simulator: () Deals in the last 24 hours in MetaTrader5: (TradeDeal(ticket=1134768493, order=1381981938, time=1766748992, time_msc=1766748992425, type=0, entry=0, magic=0, position_id=1381981938, reason=0, volume=0.01, price=1.17688, commission=-0.04, swap=0.0, profit=0.0, fee=0.0, symbol='EURUSD', comment='', external_id=''), TradeDeal(ticket=1134768532, order=1381981988, time=1766748994, time_msc=1766748994018, type=1, entry=0, magic=0, position_id=1381981988, reason=0, volume=0.01, price=1.17688, commission=-0.04, swap=0.0, profit=0.0, fee=0.0, symbol='EURUSD', comment='', external_id=''), TradeDeal(ticket=1135016562, order=1381968767, time=1766763381, time_msc=1766763381530, type=1, entry=0, magic=0, position_id=1381968767, reason=0, volume=0.01, price=1.17953, commission=-0.04, swap=0.0, profit=0.0, fee=0.0, symbol='EURUSD', comment='', external_id=''))
account_info
Необходимо иметь способ получать информацию о счёте как из терминала MetaTrader 5, так и из симулятора. Чтобы реализовать это в нашем классе, нам нужен похожий на способ хранения и доступа к параметрам счёта.
Если вы запросите информацию о счёте из MetaTrader 5 с помощью метода account_info(), вы увидите кортеж, который выглядит так:
AccountInfo(login=52557820, trade_mode=0, leverage=500, limit_orders=200, margin_so_mode=0, trade_allowed=True, trade_expert=True, margin_mode=2, currency_digits=2, fifo_close=False, balance=941.54, credit=0.0, profit=2.37, equity=943.91, margin=2.36, margin_free=941.55, margin_level=39996.18644067797, margin_so_call=100.0, margin_so_so=0.0, margin_initial=0.0, margin_maintenance=0.0, assets=0.0, liabilities=0.0, commission_blocked=0.0, name='OMEGA MSIGWA', server='ICMarketsSC-Demo', currency='USD', company='Raw Trading Ltd')
В документации сказано, что функция возвращает информацию в виде структуры именованного кортежа (namedtuple). В случае ошибки возвращает None. Информацию об ошибке можно получить с помощью last_error().
Мы определяем аналогичную структуру внутри класса Simulator.
self.AccountInfo = namedtuple( "AccountInfo", [ "login", "trade_mode", "leverage", "limit_orders", "margin_so_mode", "trade_allowed", "trade_expert", "margin_mode", "currency_digits", "fifo_close", "balance", "credit", "profit", "equity", "margin", "margin_free", "margin_level", "margin_so_call", "margin_so_so", "margin_initial", "margin_maintenance", "assets", "liabilities", "commission_blocked", "name", "server", "currency", "company", ] )
Поскольку мы стремимся имитировать MetaTrader 5 с помощью этого класса симулятора, нам нужно перенять часть параметров счёта MetaTrader 5 в симулируемый счёт.
mt5_acc_info = mt5_instance.account_info() if mt5_acc_info is None: raise RuntimeError("Failed to obtain MT5 account info") self.__account_state_update( account_info=self.AccountInfo( # ---- identity / broker-controlled ---- login=11223344, trade_mode=mt5_acc_info.trade_mode, leverage=int(leverage.split(":")[1]), limit_orders=mt5_acc_info.limit_orders, margin_so_mode=mt5_acc_info.margin_so_mode, trade_allowed=mt5_acc_info.trade_allowed, trade_expert=mt5_acc_info.trade_expert, margin_mode=mt5_acc_info.margin_mode, currency_digits=mt5_acc_info.currency_digits, fifo_close=mt5_acc_info.fifo_close, # ---- simulator-controlled financials ---- balance=deposit, # simulator starting balance credit=mt5_acc_info.credit, profit=0.0, equity=deposit, margin=0.0, margin_free=deposit, margin_level=0.0, # ---- risk thresholds (copied from broker) ---- margin_so_call=mt5_acc_info.margin_so_call, margin_so_so=mt5_acc_info.margin_so_so, margin_initial=mt5_acc_info.margin_initial, margin_maintenance=mt5_acc_info.margin_maintenance, # ---- rarely used but keep parity ---- assets=mt5_acc_info.assets, liabilities=mt5_acc_info.liabilities, commission_blocked=mt5_acc_info.commission_blocked, # ---- descriptive ---- name="John Doe", server="MetaTrader5-Simulator", currency=mt5_acc_info.currency, company=mt5_acc_info.company, ) )
Мы заполняем все данные, кроме финансовых показателей, которые можем рассчитать сами: баланс счёта, equity, margin, free margin и margin level.
Внутри функции account_info мы проверяем, выбрал ли пользователь режим тестера стратегий (IS_TESTER=True): в таком случае возвращаем информацию о счёте симулятора; в противном случае возвращаем информацию о счёте из MetaTrader 5.
def account_info(self) -> namedtuple: """Gets info on the current trading account.""" if self.IS_TESTER: return self.AccountInfo mt5_ac_info = self.mt5_instance.account_info() if mt5_ac_info is None: self.__GetLogger().warning(f"Failed to obtain MT5 account info, MT5 Error = {self.mt5_instance.last_error()}") return return mt5_ac_info
Пример использования:
sim = Simulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:200") sim.Start(IS_TESTER=True) # start simulation in the strategy tester print("simulator's account info: ", sim.account_info()) sim.Start(IS_TESTER=False) # start the simulator in real-time trading print("MetaTrader5's account info: ", sim.account_info())
Вывод.
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py simulator's account info: AccountInfo(login=11223344, trade_mode=0, leverage=200, limit_orders=200, margin_so_mode=0, trade_allowed=True, trade_expert=True, margin_mode=2, currency_digits=2, fifo_close=False, balance=1078.3, credit=0.0, profit=0.0, equity=1078.3, margin=0.0, margin_free=1078.3, margin_level=0.0, margin_so_call=100.0, margin_so_so=0.0, margin_initial=0.0, margin_maintenance=0.0, assets=0.0, liabilities=0.0, commission_blocked=0.0, name='John Doe', server='MetaTrader5-Simulator', currency='USD', company='Raw Trading Ltd') MetaTrader5's account info: AccountInfo(login=52557820, trade_mode=0, leverage=500, limit_orders=200, margin_so_mode=0, trade_allowed=True, trade_expert=True, margin_mode=2, currency_digits=2, fifo_close=False, balance=941.54, credit=0.0, profit=2.37, equity=943.91, margin=2.36, margin_free=941.55, margin_level=39996.18644067797, margin_so_call=100.0, margin_so_so=0.0, margin_initial=0.0, margin_maintenance=0.0, assets=0.0, liabilities=0.0, commission_blocked=0.0, name='OMEGA MSIGWA', server='ICMarketsSC-Demo', currency='USD', company='Raw Trading Ltd')
order_calc_profit
Это одна из полезных функций в нашем симуляторе, поскольку она помогает оценить, сколько средств подвергается риску или какую прибыль планируется получить по конкретной позиции или ордеру.
Согласно документации:
Эта функция возвращает прибыль в валюте счёта для указанной торговой операции.
order_calc_profit( action, // order type (ORDER_TYPE_BUY or ORDER_TYPE_SELL) symbol, // symbol name volume, // volume price_open, // open price price_close // close price );
Чтобы создать аналогичную функцию в MQL5, нужно понять внутреннюю работу этой функции MetaTrader 5.
Подробное описание можно найти здесь: https://www.mql5.com/ru/book/automation/experts/experts_ordercalcprofit
Ниже приведена таблица с формулами для оценки прибыли ордера в MetaTrader 5.
| Идентификатор | Формула |
|---|---|
| SYMBOL_CALC_MODE_FOREX | (ClosePrice - OpenPrice) * ContractSize * Lots |
| SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE | (ClosePrice - OpenPrice) * ContractSize * Lots |
| SYMBOL_CALC_MODE_CFD | (ClosePrice - OpenPrice) * ContractSize * Lots |
| SYMBOL_CALC_MODE_CFDINDEX | (ClosePrice - OpenPrice) * ContractSize * Lots |
| SYMBOL_CALC_MODE_CFDLEVERAGE | (ClosePrice - OpenPrice) * ContractSize * Lots |
| SYMBOL_CALC_MODE_EXCH_STOCKS | (ClosePrice - OpenPrice) * ContractSize * Lots |
| SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX | (ClosePrice - OpenPrice) * ContractSize * Lots |
| SYMBOL_CALC_MODE_FUTURES | (ClosePrice - OpenPrice) * Lots * TickPrice / TickSize |
| SYMBOL_CALC_MODE_EXCH_FUTURES | (ClosePrice - OpenPrice) * Lots * TickPrice / TickSize |
| SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS | (ClosePrice - OpenPrice) * Lots * TickPrice / TickSize |
| SYMBOL_CALC_MODE_EXCH_BONDS | Lots * ContractSize * (ClosePrice * FaceValue + AccruedInterest) |
| SYMBOL_CALC_MODE_EXCH_BONDS_MOEX | Lots * ContractSize * (ClosePrice * FaceValue + AccruedInterest) |
| SYMBOL_CALC_MODE_SERV_COLLATERAL | Lots * ContractSize * MarketPrice * LiqudityRate |
Мы вводим те же формулы в одноимённую функцию внутри нашего симулятора.
def order_calc_profit(self, action: int, symbol: str, volume: float, price_open: float, price_close: float) -> float: """ Return profit in the account currency for a specified trading operation. Args: action (int): The type of position taken, either 0 (buy) or 1 (sell). symbol (str): Financial instrument name. volume (float): Trading operation volume. price_open (float): Open Price. price_close (float): Close Price. """ sym = self.symbol_info(symbol) if self.IS_TESTER: contract_size = sym.trade_contract_size # --- Determine direction --- if action == mt5.ORDER_TYPE_BUY: direction = 1 elif action == mt5.ORDER_TYPE_SELL: direction = -1 else: self.__GetLogger().critical("order_calc_profit failed: invalid order type") return 0.0 # --- Core profit calculation --- calc_mode = sym.trade_calc_mode price_delta = (price_close - price_open) * direction try: # ------------------ FOREX / CFD / STOCKS ----------------------- if calc_mode in ( mt5.SYMBOL_CALC_MODE_FOREX, mt5.SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE, mt5.SYMBOL_CALC_MODE_CFD, mt5.SYMBOL_CALC_MODE_CFDINDEX, mt5.SYMBOL_CALC_MODE_CFDLEVERAGE, mt5.SYMBOL_CALC_MODE_EXCH_STOCKS, mt5.SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX, ): profit = price_delta * contract_size * volume # ---------------- FUTURES -------------------- elif calc_mode in ( mt5.SYMBOL_CALC_MODE_FUTURES, mt5.SYMBOL_CALC_MODE_EXCH_FUTURES, # mt5.SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS, ): tick_value = sym.trade_tick_value tick_size = sym.trade_tick_size if tick_size <= 0: self.__GetLogger().critical("Invalid tick size") return 0.0 profit = price_delta * volume * (tick_value / tick_size) # ---------- BONDS ------------------- elif calc_mode in ( mt5.SYMBOL_CALC_MODE_EXCH_BONDS, mt5.SYMBOL_CALC_MODE_EXCH_BONDS_MOEX, ): face_value = sym.trade_face_value accrued_interest = sym.trade_accrued_interest profit = ( volume * contract_size * (price_close * face_value + accrued_interest) - volume * contract_size * (price_open * face_value) ) # ------ COLLATERAL ------- elif calc_mode == mt5.SYMBOL_CALC_MODE_SERV_COLLATERAL: liquidity_rate = sym.trade_liquidity_rate market_price = ( self.tick.ask if action == mt5.ORDER_TYPE_BUY else self.tick.bid ) profit = ( volume * contract_size * market_price * liquidity_rate ) else: self.__GetLogger().critical( f"Unsupported trade calc mode: {calc_mode}" ) return 0.0 return round(profit, 2) except Exception as e: self.__GetLogger().critical(f"Failed: {e}") return 0.0 # if we are not on the strategy tester try: profit = self.mt5_instance.order_calc_profit( action, symbol, volume, price_open, price_close ) except Exception as e: self.__GetLogger().critical(f"Failed to calculate profit of a position, MT5 error = {self.mt5_instance.last_error()}") return np.nan return profit
Пример использования:
sim = Simulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500") sim.Start(IS_TESTER=True) # start simulation in the strategy tester profit = sim.order_calc_profit(action=mt5.POSITION_TYPE_SELL, symbol=symbol, volume=0.01, price_open=entry, price_close=tp) print("Simulator profit caclulate: ", profit) sim.Start(IS_TESTER=False) # start the simulator in real-time trading profit = sim.order_calc_profit(action=mt5.POSITION_TYPE_SELL, symbol=symbol, volume=0.01, price_open=entry, price_close=tp) print("MT5 profit caclulate: ", round(profit, 2))
Вывод.
Simulator profit caclulate: 1.68 MT5 profit caclulate: 1.68
order_calc_margin
Это ещё одна полезная функция в API MetaTrader 5, несмотря на то, что её работа менее известна.
Согласно документации, эта функция рассчитывает маржу в валюте счёта, необходимую для выполнения указанной торговой операции.
Следующая таблица представляет формулы, используемые для реализации функции order_calc_margin.
| Идентификатор | Формула |
|---|---|
| SYMBOL_CALC_MODE_FOREX Forex | Lots * ContractSize * MarginRate / Leverage |
| SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE Forex without leverage | Lots * ContractSize * MarginRate |
| SYMBOL_CALC_MODE_CFD CFD | Lots * ContractSize * MarketPrice * MarginRate |
| SYMBOL_CALC_MODE_CFDLEVERAGE CFD with leverage | Lots * ContractSize * MarketPrice * MarginRate / Leverage |
| SYMBOL_CALC_MODE_CFDINDEX CFDs on indices | Lots * ContractSize * MarketPrice * TickPrice / TickSize * MarginRate |
| SYMBOL_CALC_MODE_EXCH_STOCKS Securities on the stock exchange | Lots * ContractSize * LastPrice * MarginRate |
| SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX Securities on MOEX | Lots * ContractSize * LastPrice * MarginRate |
| SYMBOL_CALC_MODE_FUTURES Futures | Lots * InitialMargin * MarginRate |
| SYMBOL_CALC_MODE_EXCH_FUTURES Futures on the stock exchange | Lots * InitialMargin * MarginRate or |
| SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS Futures on FORTS | Lots * InitialMargin * MarginRate or |
| SYMBOL_CALC_MODE_EXCH_BONDS Bonds on the stock exchange | Lots * ContractSize * FaceValue * OpenPrice / 100 |
| SYMBOL_CALC_MODE_EXCH_BONDS_MOEX Bonds on MOEX | Lots * ContractSize * FaceValue * OpenPrice / 100 |
| SYMBOL_CALC_MODE_SERV_COLLATERAL | Non-tradable asset (margin not applicable) |
Мы используем те же формулы для оценки маржи ордера в нашем классе Simulator.
def order_calc_margin(self, action: int, symbol: str, volume: float, price: float) -> float: """ Return margin in the account currency to perform a specified trading operation. """ if volume <= 0 or price <= 0: self.__GetLogger().error("order_calc_margin failed: invalid volume or price") return 0.0 if not self.IS_TESTER: try: return round(self.mt5_instance.order_calc_margin(action, symbol, volume, price), 2) except Exception: self.__GetLogger().warning(f"Failed: MT5 Error = {self.mt5_instance.last_error()}") return 0.0 # IS_TESTER = True sym = self.symbol_info(symbol) contract_size = sym.trade_contract_size leverage = max(self.AccountInfo.leverage, 1) margin_rate = ( sym.margin_initial if sym.margin_initial > 0 else sym.margin_maintenance ) if margin_rate <= 0: # if margin rate is zero set it to 1 margin_rate = 1.0 mode = sym.trade_calc_mode if mode == self.mt5_instance.SYMBOL_CALC_MODE_FOREX: margin = (volume * contract_size * price) / leverage elif mode == self.mt5_instance.SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE: margin = volume * contract_size * price elif mode in ( self.mt5_instance.SYMBOL_CALC_MODE_CFD, self.mt5_instance.SYMBOL_CALC_MODE_CFDINDEX, self.mt5_instance.SYMBOL_CALC_MODE_EXCH_STOCKS, self.mt5_instance.SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX, ): margin = volume * contract_size * price * margin_rate elif mode == self.mt5_instance.SYMBOL_CALC_MODE_CFDLEVERAGE: margin = (volume * contract_size * price * margin_rate) / leverage elif mode in ( self.mt5_instance.SYMBOL_CALC_MODE_FUTURES, self.mt5_instance.SYMBOL_CALC_MODE_EXCH_FUTURES, # self.mt5_instance.SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS, ): margin = volume * sym.margin_initial elif mode in ( self.mt5_instance.SYMBOL_CALC_MODE_EXCH_BONDS, self.mt5_instance.SYMBOL_CALC_MODE_EXCH_BONDS_MOEX, ): margin = ( volume * contract_size * sym.trade_face_value * price / 100 ) elif mode == self.mt5_instance.SYMBOL_CALC_MODE_SERV_COLLATERAL: margin = 0.0 else: self.__GetLogger().warning(f"Unknown calc mode {mode}, fallback margin formula used") margin = (volume * contract_size * price) / leverage return round(margin, 2)
Часть с margin_rate является самой сложной, поскольку нам нужно убедиться, что значения существуют, прежде чем выбирать подходящее значение ставки.
Заключительные мысли
В этой статье мы представили способ передачи тиковых данных в наш симулятор и реализовали почти все необходимые функции, предоставляемые Python API для MetaTrader 5. Это приближает нас к изолированной среде для симуляции работы MetaTrader 5 и, тем самым, к созданию пользовательского тестера стратегий для наших торговых ботов на Python.
В следующей статье мы реализуем торговые функции и смоделируем торговую активность на некоторых тиках из прошлого. Впереди ещё больше интересного, так что следите за продолжением!
До связи.
Поделитесь своими мыслями и помогите улучшить этот проект на GitHub: https://github.com/MegaJoctan/PyMetaTester
Вкладка вложений
| Название файла | Описание и использование |
|---|---|
| bars.py | Содержит функции для сбора баров из клиента MetaTrader 5 в пользовательский файл и путь. |
| ticks.py | Содержит функции для сбора тиков из клиента MetaTrader 5 в пользовательский файл и путь. |
| config.py | Python-файл конфигурации, где определены наиболее полезные переменные для повторного использования по всему проекту. |
| utils.py | Утилитный Python-файл, содержащий простые функции для помощи в различных задачах, то есть вспомогательные функции (helpers). |
| simulator.py | Содержит класс с именем Simulator. Основная логика нашего симулятора находится в одном месте. |
| test.py | Файл, используемый для тестирования всего кода и функций, рассмотренных в этой статье. |
| error_description.py | Содержит функции для преобразования всех кодов ошибок MetaTrader 5 в понятные человеку сообщения. |
| requirements.txt | Содержит все Python-зависимости и их версии, используемые в этом проекте. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20455
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Построение моделей волатильности в MQL5 (Часть I): Первичная реализация
Разработка инструментария для анализа Price Action (Часть 43): Вероятностный анализ свечных паттернов и пробоев
Автоматизация торговых стратегий в MQL5 (Часть 26): Создание системы усреднения на основе пин-баров для многопозиционной торговли
Разработка инструментария для анализа Price Action (Часть 42): Интерактивное тестирование на графике с кнопочной логикой и статистическими уровнями
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования