English Deutsch
preview
Python-MetaTrader 5ストラテジーテスター(第2回):シミュレーターにおけるバー、ティック、組み込み関数のオーバーロード処理

Python-MetaTrader 5ストラテジーテスター(第2回):シミュレーターにおけるバー、ティック、組み込み関数のオーバーロード処理

MetaTrader 5テスター |
16 0
Omega J Msigwa
Omega J Msigwa

内容


はじめに

前回の記事では、MetaTrader 5から取得したティックデータ、バーデータ、銘柄情報などに大きく依存するPythonのシミュレータークラス「TradeSimulator」について解説しました。

最初の記事では、MetaTrader 5クライアントおよびそのストラテジーテスター(シミュレーター)を模倣するために必要な基礎を構築しました。本記事では、ティックデータおよびバーデータを導入し、さらにPython-MetaTrader 5モジュールが提供する関数に類似した機能をシミュレーター内に実装していきます。これにより、MetaTrader 5が提供する機能の再現に一歩近づきます。


    MetaTrader 5の過去ティックの扱い

    ティックとは、金融商品の最も細かいリアルタイム価格更新単位であり、個々の価格変動、Bid/Ask価格の動き、取引量を表します。

    OHLCバー(始値、高値、安値、終値)とは異なり、ティックはミリ秒単位でデータを提供します。

    MQL5プログラミング言語のOnTick関数をご存じかもしれません。これは新しいティックが到着するたびに呼び出される、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か月分で280万件のティックデータを取得することができました。このデータのサイズはメガバイト単位で確認することもできます(これにより、この1回のティック取得でどれくらいのメモリ(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.1GBの容量になります。では、シミュレーター(ストラテジーテスター)において、ユーザーが12銘柄の多通貨ボットを20年間テストするとしたら、メモリ使用量や全体のパフォーマンスにどれほどの負荷がかかることになるでしょうか。

    このように大量のデータを扱う場合、メモリを過度に消費せず、かつ十分なパフォーマンスを維持できる最適なアプローチを見つける必要があります。

    その解決策のひとつとして、PolarsのDataFrameが非常に有効です。

    Polarsは使いやすく非常に高速であり、そのストリーミングAPIにより、メモリに収まりきらないような大規模データセット(たとえば100GB以上のデータ)であっても、非常に効率的に処理できます。

    また、データ全体の保存に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.174031.176030.0  ┆ … ┆ 134   ┆ 0.0         ┆ 20259     │
    │ 2025-09-22 00:00:47 UTC ┆ 1.174051.176050.0  ┆ … ┆ 134   ┆ 0.0         ┆ 20259     │
    │ 2025-09-22 00:05:00 UTC ┆ 1.173461.175460.0  ┆ … ┆ 134   ┆ 0.0         ┆ 20259     │
    │ 2025-09-22 00:05:05 UTC ┆ 1.173   ┆ 1.175   ┆ 0.0  ┆ … ┆ 134   ┆ 0.0         ┆ 20259     │
    │ 2025-09-22 00:05:10 UTC ┆ 1.173071.174870.0  ┆ … ┆ 134   ┆ 0.0         ┆ 20259     │
    │ …                       ┆ …       ┆ …       ┆ …    ┆ … ┆ …     ┆ …           ┆ …    ┆ …     │
    │ 2025-09-30 23:58:44 UTC ┆ 1.173351.173430.0  ┆ … ┆ 4     ┆ 0.0         ┆ 20259     │
    │ 2025-09-30 23:58:45 UTC ┆ 1.173351.173420.0  ┆ … ┆ 4     ┆ 0.0         ┆ 20259     │
    │ 2025-09-30 23:58:46 UTC ┆ 1.173351.173430.0  ┆ … ┆ 4     ┆ 0.0         ┆ 20259     │
    │ 2025-09-30 23:58:47 UTC ┆ 1.173351.173420.0  ┆ … ┆ 4     ┆ 0.0         ┆ 20259     │
    │ 2025-09-30 23:58:50 UTC ┆ 1.173341.1734  ┆ 0.0  ┆ … ┆ 134   ┆ 0.0         ┆ 20259     │
    └─────────────────────────┴─────────┴─────────┴──────┴───┴───────┴─────────────┴──────┴───────┘
    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.173371.175060.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202510    │
    │ 2025-10-01 00:00:02 UTC ┆ 1.173371.174020.0  ┆ … ┆ 4     ┆ 0.0         ┆ 202510    │
    │ 2025-10-01 00:00:02 UTC ┆ 1.173371.173890.0  ┆ … ┆ 4     ┆ 0.0         ┆ 202510    │
    │ …                       ┆ …       ┆ …       ┆ …    ┆ … ┆ …     ┆ …           ┆ …    ┆ …     │
    │ 2025-10-31 23:56:43 UTC ┆ 1.153681.153680.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202510    │
    │ 2025-10-31 23:56:52 UTC ┆ 1.153691.153690.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202510    │
    │ 2025-10-31 23:56:52 UTC ┆ 1.153711.153710.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202510    │
    │ 2025-10-31 23:56:53 UTC ┆ 1.1537  ┆ 1.1537  ┆ 0.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202510    │
    │ 2025-10-31 23:56:53 UTC ┆ 1.153711.153710.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202510    │
    └─────────────────────────┴─────────┴─────────┴──────┴───┴───────┴─────────────┴──────┴───────┘
    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.153650.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202511    │
    │ 2025-11-03 00:01:00 UTC ┆ 1.1528  ┆ 1.153650.0  ┆ … ┆ 130   ┆ 0.0         ┆ 202511    │
    │ 2025-11-03 00:01:00 UTC ┆ 1.1528  ┆ 1.153650.0  ┆ … ┆ 4     ┆ 0.0         ┆ 202511    │
    │ 2025-11-03 00:01:21 UTC ┆ 1.152951.153650.0  ┆ … ┆ 130   ┆ 0.0         ┆ 202511    │
    │ 2025-11-03 00:01:25 UTC ┆ 1.152821.153650.0  ┆ … ┆ 130   ┆ 0.0         ┆ 202511    │
    │ …                       ┆ …       ┆ …       ┆ …    ┆ … ┆ …     ┆ …           ┆ …    ┆ …     │
    │ 2025-11-28 23:55:12 UTC ┆ 1.159481.160180.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202511    │
    │ 2025-11-28 23:55:13 UTC ┆ 1.159551.160170.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202511    │
    │ 2025-11-28 23:55:36 UTC ┆ 1.159481.160180.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202511    │
    │ 2025-11-28 23:55:37 UTC ┆ 1.159531.160170.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202511    │
    │ 2025-11-28 23:55:54 UTC ┆ 1.159541.160240.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202511    │
    │ 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.159361.159690.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202512    │
    │ 2025-12-01 00:00:06 UTC ┆ 1.159341.159620.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202512    │
    │ 2025-12-01 00:00:11 UTC ┆ 1.159351.159970.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202512    │
    │ 2025-12-01 00:00:15 UTC ┆ 1.159361.159790.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202512    │
    │ 2025-12-01 00:00:21 UTC ┆ 1.159361.159640.0  ┆ … ┆ 4     ┆ 0.0         ┆ 202512    │
    │ …                       ┆ …       ┆ …       ┆ …    ┆ … ┆ …     ┆ …           ┆ …    ┆ …     │
    │ 2025-12-01 00:59:57 UTC ┆ 1.159641.160050.0  ┆ … ┆ 4     ┆ 0.0         ┆ 202512    │
    │ 2025-12-01 00:59:57 UTC ┆ 1.159721.160120.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202512    │
    │ 2025-12-01 00:59:57 UTC ┆ 1.159671.160050.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202512    │
    │ 2025-12-01 00:59:57 UTC ┆ 1.159711.160090.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202512    │
    │ 2025-12-01 00:59:57 UTC ┆ 1.159651.160050.0  ┆ … ┆ 134   ┆ 0.0         ┆ 202512    │
    └─────────────────────────┴─────────┴─────────┴──────┴───┴───────┴─────────────┴──────┴───────┘
    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に値を指定すると、その列をグループとして扱い、データを別々のサブフォルダに分割して保存してくれることです。

    2つの銘柄からティックデータを取得した場合

    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

    残念ながら、要求したすべてのティックデータ(2025年1月1日~12月1日)を取得することはできませんでした。どうやら、MetaTrader 5ターミナルに存在する以上のティックデータを取得することはできないようです。この場合、利用しているブローカーに数か月分のティックデータしか保存されておらず、その範囲のデータしか取得できませんでした。

    ファイル: C:\Users\Omega\AppData\Roaming\MetaQuotes\Terminal\010E047102812FC0C18890992854220E\bases\<broker name>\ticks\EURUSD



    MetaTrader 5の過去バーの扱い

    ティックとは異なり、バーは時間足に基づいているため、ティックよりも扱いやすいです。 ティックの取得方法と同様に、バーのデータも同じような手順で取得する必要があります。

    まず最初に、バーのデータをリクエストする前に、対象の銘柄が気配値表示で利用可能であることを確認し、選択しておく必要があります。

    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)

    たとえば、3つの銘柄から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
    )

    シミュレータークラス内にも同様の関数が必要です。関数は、ティックを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
       )

    前の2つとは異なり、この関数は2つの日付の間にあるバーを返します。すなわち、開始日である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

    ティックのリクエストには、ユーザーが取得したいティックの種類を指定できるフラグオプションが含まれているため、ユーザーの要件に応じてティックをフィルタリングできるように、フラグマスクを作成する仕組みが必要になります。

    ドキュメントによると、

    フラグは、要求されるティックの種類を定義します。

    フラグの値はCOPY_TICKS列挙型に定義されています。

    ID

    説明

    COPY_TICKS_ALL

    すべてのティック

    COPY_TICKS_INFO

    Ask価格やBid価格の変更を含むティック

    COPY_TICKS_TRADE

    Last価格や出来高の変化を含むティック


    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

    直近の買いレートが更新された

    TICK_FLAG_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

    ドキュメントによると、この関数は有効な注文を取得し、銘柄やチケットによるフィルタリング機能を備えています。呼び出しオプションは3つあります。

    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ターミナルにできるだけ近づけるためには、同様のデータ型を返す必要があります。 

    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. There are three call options.
    
        Returns:
            
            list: Returns info in the form of a named tuple structure (namedtuple). Return None in case of an error. The info on the error can be obtained using 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",
                ]
            )

    以下は、シミュレータークラス内の同様の関数です。

        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ターミナルに2つの指値注文があり、

    シミュレーション上でも2つの取引が存在している場合

            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())

    出力:

    Orders in the simulator:
     (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と同様の見た目と動作を持っています。

    ドキュメントによると:

    この関数は保有ポジションを取得し、銘柄やチケットによるフィルタリング機能を備えています。呼び出し方法は3種類あります。

    パラメータなしで呼び出した場合、すべての銘柄の保有ポジションが返されます。

    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年1月1日からの経過秒数で指定します。 
    • date_to:注文の取得終了日。datetimeオブジェクト、または1970年1月1日からの経過秒数で指定します。

    シミュレーターにおける同様の関数は、以下のように実装できます。

        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

    ドキュメントによると、このメソッドは取引履歴から注文を取得し、チケットやポジションによるフィルタリング機能を備えています。 

    指定された期間内に含まれるすべての注文を返します。

    呼び出し方法は3種類あります。

    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という配列から取得されます。さらに、注文チケット、ポジション(保存されているポジションのチケット)、およびグループ(必要な銘柄を整理するためのフィルター)による追加のフィルタリングがおこなわれます。

        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年1月1日からの経過秒数で指定します。 
    • date_to:取引の取得終了日。datetimeオブジェクト、または1970年1月1日からの経過秒数で指定します。

        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. Set by the 'datetime' object or as several seconds elapsed since 1970.01.01. 
                
                date_to (datetime, required): Date, up to which the orders are requested. Set by the 'datetime' object or as several seconds elapsed since 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

    ドキュメントによると、このメソッドは指定された期間内の取引履歴から約定(deal)を取得し、チケットやポジションによるフィルタリング機能を備えています。

    この関数には3つのバリエーションがあります。

    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
    )

    シミュレータークラスに、同じ名前のメソッドを作成します。ユーザーがストラテジーテスターモードを選択している場合(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()で取得できます。

    シミュレータークラス内にも同様の構造を定義します。

            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,
                )
            )

    計算可能な財務情報(口座残高、エクイティ、証拠金、余剰証拠金、証拠金維持率など)を除き、すべての詳細をあらかじめ設定しておきます。

    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
       );

    MetaTrader 5 の関数と同様のものをMQL5で作成するためには、この関数の内部動作を理解する必要があります。

    詳細については、https://www.mql5.com/ja/book/automation/experts/experts_ordercalcprofitをご覧ください。

    以下の表は、MetaTrader 5 における注文の利益を推定するための数式をまとめたものです。

    識別子

    SYMBOL_CALC_MODE_FOREX

    (終値 - 始値)* 契約サイズ * ロット数

    SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE

    (終値 - 始値)* 契約サイズ * ロット数

    SYMBOL_CALC_MODE_CFD

    (終値 - 始値)* 契約サイズ * ロット数

    SYMBOL_CALC_MODE_CFDINDEX

    (終値 - 始値)* 契約サイズ * ロット数

    SYMBOL_CALC_MODE_CFDLEVERAGE

    (終値 - 始値)* 契約サイズ * ロット数

    SYMBOL_CALC_MODE_EXCH_STOCKS

    (終値 - 始値)* 契約サイズ * ロット数

    SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX

    (終値 - 始値)* 契約サイズ * ロット数

    SYMBOL_CALC_MODE_FUTURES

    (終値 - 始値) * ロット数 * ティック価格 / ティックサイズ

    SYMBOL_CALC_MODE_EXCH_FUTURES

    (終値 - 始値) * ロット数 * ティック価格 / ティックサイズ

    SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS

    (終値 - 始値) * ロット数 * ティック価格 / ティックサイズ

    SYMBOL_CALC_MODE_EXCH_BONDS

    ロット数 * 契約サイズ * (終値 * 額面価格 + 発生利息)

    SYMBOL_CALC_MODE_EXCH_BONDS_MOEX

    ロット数 * 契約サイズ * (終値 * 額面価格 + 発生利息)

    SYMBOL_CALC_MODE_SERV_COLLATERAL

    ロット数 * 契約サイズ * 市場価格 * 流動率


    シミュレーター内の同様の名前の関数に、同じ数式を導入します。

        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

    これは、MetaTrader 5 API におけるもうひとつの有用な関数ですが、その動作はあまり広く知られていません。

    ドキュメントによると、この関数は、指定された取引操作を実行するために必要となる、口座通貨建ての証拠金を計算します。

    以下の表は、order_calc_margin 関数がどのように構築されているかを示すために使用される数式をまとめたものです。

    識別子

    SYMBOL_CALC_MODE_FOREX

    外国為替

    ロット数 * 契約サイズ * 証拠金率 / レバレッジ

    SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE

    レバレッジなしのFX

    ロット数 * 契約サイズ * 証拠金率

    SYMBOL_CALC_MODE_CFD

    CFD

    ロット数 * 契約サイズ * 市場価格 * 証拠金率

    SYMBOL_CALC_MODE_CFDLEVERAGE

    レバレッジを効かせたCFD

    ロット数 * 契約サイズ * 市場価格 * 証拠金率 / レバレッジ

    SYMBOL_CALC_MODE_CFDINDEX

    指数に関するCFD

    ロット数 * 契約サイズ * 市場価格 * ティック価格 / ティックサイズ * 証拠金率

    SYMBOL_CALC_MODE_EXCH_STOCKS

    証券取引所に上場されている証券

    ロット数 * 契約サイズ * 最終価格 * 証拠金率

    SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX

    MOEX上場証券

    ロット数 * 契約サイズ * 最終価格 * 証拠金率

    SYMBOL_CALC_MODE_FUTURES

    先物

    ロット数 * 初期証拠金 * 証拠金率

    SYMBOL_CALC_MODE_EXCH_FUTURES

    証券取引所の先物取引

    ロット数 * 初期証拠金 * 証拠金率 または
    ロット * 維持証拠金 * 証拠金率

    SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS

    FORTSの先物取引

    ロット数 * 初期証拠金 * 証拠金率 または
    ロット * 維持証拠金 * 証拠金率

    SYMBOL_CALC_MODE_EXCH_BONDS

    証券取引所に上場されている債券

    ロット数 × 契約サイズ × 額面金額 × オープン価格 / 100

    SYMBOL_CALC_MODE_EXCH_BONDS_MOEX

    MOEX上場債券

    ロット数 × 契約サイズ × 額面金額 × オープン価格 / 100

    SYMBOL_CALC_MODE_SERV_COLLATERAL

    非取引資産(証拠金不要)


    シミュレータークラスでは、注文の証拠金を推定する際に、同じ数式を使用します。

        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の部分は最も厄介で、どのレート値を使用するべきか判断する前に、必要な値がすべて存在していることを確認しなければなりません。


    最終的な考察

    本記事では、シミュレーターにおけるティックデータの受け渡し方法を紹介し、MetaTrader 5 の Python API が提供するほぼすべての必要な関数を実装しました。これにより、MetaTrader 5 の動作を再現するための独立した環境に一歩近づくことができました。この仕組みによって、Python製自動売買ボットのためのカスタムストラテジーテスターを構築することが可能になります。

    次回の記事では、取引関数を実装し、過去のいくつかのティックデータに対して取引の動作をシミュレーションする予定です。さらに興味深い内容が続きますので、ぜひご期待ください。

    では、また。

    GitHub: https://github.com/MegaJoctan/PyMetaTesterで、このプロジェクトに関するご意見を共有し、改善にご協力ください。


    添付ファイル

    ファイル名 説明と使用法
    bars.py MetaTrader 5クライアントからバーデータを取得し、指定したファイルパスに保存する関数を含むファイルです。 
    ticks.py MetaTrader 5クライアントからティックデータを取得し、指定したファイルパスに保存する関数を含むファイルです。
    config.py プロジェクト全体で再利用性を高めるための変数を定義した設定ファイルです。
    utils.py さまざまなタスクを支援するシンプルな関数(ヘルパー関数)を含むユーティリティファイルです。
    simulator.py Simulatorというクラスを定義し、シミュレーターの主要なロジックをまとめたファイルです。
    test.py 本記事で説明したコードや関数をテストするためのファイルです。
    error_description.py MetaTrader 5のエラーコードを人間が読めるメッセージに変換する関数を含むファイルです。
    requirements.txt  このプロジェクトで使用されるPythonの依存関係とそのバージョンを記述したファイルです。 


    MetaQuotes Ltdにより英語から翻訳されました。
    元の記事: https://www.mql5.com/en/articles/20455

    添付されたファイル |
    Attachments.zip (18.65 KB)
    ラリー・ウィリアムズの『市場の秘密』(第4回):MQL5における短期的スイングハイとスイングローの自動化 ラリー・ウィリアムズの『市場の秘密』(第4回):MQL5における短期的スイングハイとスイングローの自動化
    MQL5を使って、ラリー・ウィリアムズの短期スイングパターンの自動化を習得していきます。このガイドでは、非ランダムな市場構造を活用する、完全に設定可能なエキスパートアドバイザー(EA)を開発します。堅牢なリスク管理と柔軟なエグジットロジックの統合方法も解説し、システマティックな戦略開発とバックテストのための確かな基盤を提供します。
    MQL5でボラティリティモデルを構築する(第I回):初期実装 MQL5でボラティリティモデルを構築する(第I回):初期実装
    本記事では、Pythonのarchパッケージに類似した機能を持つ、ボラティリティモデリング用のMQL5ライブラリを提示します。このライブラリは現在、一般的な条件付き平均モデル(HAR、AR、一定平均、ゼロ平均)および条件付き分散モデル(一定分散、ARCH、GARCH)をサポートしています。
    エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
    この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
    MQL5でのAI搭載取引システムの構築(第8回):アニメーション、タイミング指標、応答管理ツールによるUIの改善 MQL5でのAI搭載取引システムの構築(第8回):アニメーション、タイミング指標、応答管理ツールによるUIの改善
    本記事では、MQL5におけるAI駆動取引システムを、ユーザーインターフェースの改善によって強化します。具体的には、リクエストの準備フェーズおよび思考フェーズにおけるローディングアニメーションの追加や、レスポンスに表示される処理時間(タイミングメトリクス)による応答の向上などを実装します。さらに、AIへの再クエリを行うための再生成ボタンや、最新の応答をファイルとして保存できるエクスポート機能などのレスポンス管理ツールを追加し、操作性を向上させます。