English Русский 中文 Español Deutsch Português
preview
MetaTrader 5を使用したPythonの高頻度裁定取引システム

MetaTrader 5を使用したPythonの高頻度裁定取引システム

MetaTrader 5トレーディング |
321 8
Yevgeniy Koshtenko
Yevgeniy Koshtenko

はじめに

外国為替市場、アルゴリズム戦略、Python、そしてMetaTrader 5。これらすべてが、私が裁定取引システムに取り組み始めたときに一つに結びつきました。アイデアはシンプルで、高頻度で価格の不均衡を検出するシステムを構築するというものでした。では、この取り組みは最終的に何をもたらしたのでしょうか。

この期間中、私はMetaTrader 5のAPIを最も多く活用しました。そして、合成クロスレートの計算に取り組みました。10や100といった数では満足せず、その数は1000を超えました。

リスク管理は、また別の課題でした。システムアーキテクチャやアルゴリズム、意思決定のプロセスなど、本記事ではそのすべてを詳細に解説します。バックテストや実際の取引結果もお見せします。そしてもちろん、今後の展望や新たなアイデアについても共有します。もしかすると、この記事をきっかけに、このテーマをさらに発展させたいと思う方がいらっしゃるかもしれません。私の取り組みが少しでも需要があり、アルゴリズム取引の発展に貢献できることを願っています。そして誰かがこれをベースに、より効果的な高頻度アービトラージ戦略を生み出してくれることを信じています。結局のところ、科学とは先人の経験を踏まえて前進していくことに他なりません。それでは、本題に入りましょう。


外国為替裁定取引入門

本当のところ、裁定取引とは何なのでしょうか。

通貨の両替に例えることができます。たとえば、ある場所でEURでUSDを購入し、すぐに別の場所でそのUSDをGBPに換え、最終的にGBPを再びEURに両替して利益を得る。これが最も基本的な裁定取引の形です。

しかし実際には、もっと複雑です。外国為替市場は非常に大規模かつ分散化されたマーケットであり、そこには多くの銀行、ブローカー、ファンドなどが存在しています。それぞれが独自の為替レートを設定しているため、往々にしてレートが一致しないのです。ここに裁定取引のチャンスが生まれます。とはいえ、これは「簡単に儲かる話」ではありません。価格の不一致は通常、数秒、時にはミリ秒単位でしか続きません。タイミングを逃さずに利益を得るのは、ほとんど不可能に近いのです。そのためには、高性能なコンピューターと高速なアルゴリズムが不可欠です。

裁定取引にはいくつかの種類があります。最もシンプルなものは、異なる場所でのレート差を利用して利益を得る手法です。より高度な手法としては、クロスレートを用いたものがあります。たとえば、GBPをUSDおよびEURに換算し、それをもとに算出したレートと、GBP/EURの直接レートを比較します。

リストはこれで終わりではありません。さらに、時間的裁定取引という手法もあります。これは、異なる時点での価格差に着目して利益を得る方法です。今買って、1分後に売る。ただそれだけに見えますが、最大の問題は、1分後の価格がどの方向に動くかがわからないという点です。これらが、裁定取引における主なリスクです。市場が想定より早く反転してしまったり、ブローカーが注文の実行を遅らせる場合もあります。このように、実際の運用には多くの困難とリスクが伴います。それでもなお、外国為替裁定取引は非常に人気の高い手法です。この分野には多額の資金が投じられており、裁定取引だけを専門とするトレーダーも数多く存在します。

それでは、簡単な理論紹介を終えたところで、私たちの戦略について具体的に見ていきましょう。


使用されるテクノロジーの概要:PythonとMetaTrader 5

さて、今回使用するのはPythonとMetaTrader 5です。 

まず、Pythonは汎用性が高く、読みやすくて理解しやすいプログラミング言語です。初心者から経験豊富な開発者まで幅広く支持されており、特にデータ分析の分野でその力を発揮します。

一方、MetaTrader 5は、あらゆる外国為替トレーダーにとって馴染み深い取引プラットフォームです。高い信頼性と直感的な操作性に加え、リアルタイムのレート配信、自動売買ロボット、テクニカル分析など、豊富な機能を一つに統合したオールインワンのアプリケーションです。良好な取引結果を得るためには、これらのツールを適切に組み合わせる必要があります。

具体的には、PythonがMetaTrader 5からデータを取得し、各種ライブラリで処理をおこない、その結果に基づいてMetaTrader 5に売買命令を返します。もちろん、ここには技術的な課題も存在しますが、この2つを連携させることで非常に強力かつ柔軟な取引システムを構築できます。

MetaTrader 5とPythonを連携させるための専用ライブラリが開発者から提供されています。インストールも簡単で、これを導入すれば、リアルタイムでのレート取得、注文の発注、ポジションの管理といった一連の操作をPythonから直接おこなうことが可能になります。まさに端末上での操作と同じことが、Pythonの力を借りて実現できるのです。

では、これによってどのようなことが可能になるのでしょうか。その可能性は非常に広がっています。たとえば、取引の完全自動化、過去のデータに基づいた高度な分析、独自の取引プラットフォームの構築が可能です。最後の例は上級者向けの領域ですが、確実に実現可能です。


環境設定:必要なライブラリのインストールとMetaTrader 5への接続

まずはPythonから作業を始めましょう。まだインストールしていない場合は、python.orgから最新版をダウンロードしてください。その際、[ADD TO PATCH]にチェックを入れることを忘れないでください。

次に必要なのはライブラリのインストールです。使用するライブラリは数種類ありますが、最も重要なのがMetaTrader5用の公式ライブラリです。インストール作業は非常に簡単です。

コマンドラインを開き、次のように入力します。

pip install MetaTrader5 pandas numpy

Enterキーを押したら、休憩して、コーヒーでもお茶でも、お好きなものを飲みに行きましょう。

インストールが完了したら、次はMetaTrader 5との接続設定に進みます。

まず、MetaTrader 5本体をブローカーなどからダウンロードしてインストールしてください。重要なのは、端末のパスを確認しておくことです。一般的には「C:\ProgramFiles\MetaTrader5\terminal64.exe」のようなパスになります。

次にPythonを開いて次のように入力します。

import MetaTrader5 as mt5

if not mt5.initialize(path="C:/Program Files/MetaTrader 5/terminal64.exe"):
    print("Alas! Failed to connect :(")
    mt5.shutdown()
else:
    print("Hooray! Connection successful!")

すべてが開始されたら、次の部分に進みます。


コード構造:主な関数とその目的

まずは「import」から始めましょう。ここでは、MetaTrader5、pandas、datetime、pytzなどのライブラリをインポートしています。 その後、各種関数が登場します。

  • 最初の関数はremove_duplicate_indicesです。です。この関数は、データ内に重複するインデックスがないことを確認します。
  • 次はget_mt5_dataで、MetaTrader5の機能にアクセスし、過去24時MetaTrader 5の機能にアクセスし、過去24時間分の必要なデータを抽出します。
  • get_currency_dataは非常に興味深い関数です。これは、AUDUSD、EURUSD、GBPJPYなど多数の通貨ペアに対してget_mt5_dataを呼び出します。
  • その次がcalculate_synthetic_pricesです。この機能はまさに本システムの目玉です。通貨ペアを処理しながら、数百の合成価格を生成します。
  • analyze_arbitrageは、実際の価格と合成価格を比較し、裁定取引のチャンスを探します。見つかったすべての結果はCSVファイルに保存されます。
  • open_test_limit_orderは、さらに強力なコードユニットです。裁定取引の機会が見つかったときにテスト注文を発行しますが、同時に開ける取引は最大で10件までに制限されています。

そして最後に、「main」関数です。この関数は、すべての処理を正しい順序で呼び出して管理します。

処理全体は無限ループで構成されており、5分ごとに繰り返し実行されます。ただし、これは市場が開いている時間に限ります。これが本システムの構造です。シンプルでありながら、非常に効率的です。 


MetaTrader5からデータを取得する:get_mt5_data関数

最初のタスクは端末からデータを受信することです。

if not mt5.initialize(path=terminal_path):
    print(f"Failed to connect to MetaTrader 5 terminal at {terminal_path}")
    return None
timezone = pytz.timezone("Etc/UTC")
utc_from = datetime.now(timezone) - timedelta(days=1)

ここでUTCを使用している点に注意してください。なぜなら、為替市場ではタイムゾーンの混乱は致命的だからです。

次に最も重要なのは、ティックデータの取得です。

ticks = mt5.copy_ticks_from(symbol, utc_from, count, mt5.COPY_TICKS_ALL)

データは受信されたでしょうか。素晴らしいです。次はそれを処理する必要があります。ここではpandasを使います。

ticks_frame = pd.DataFrame(ticks)
ticks_frame['time'] = pd.to_datetime(ticks_frame['time'], unit='s')

これで完成です。データを含む自分専用のDataFrameができました。すでに分析に使える状態です。

しかし、何か問題が起きたらどうなるでしょうか。ご心配なく。関数内でちゃんと対処しています。

if ticks is None:
    print(f"Failed to fetch data for {symbol}")
    return None

このように、問題が発生した場合はメッセージを出力し、Noneを返します。


複数の通貨ペアの処理:get_currency_data関数

システム(get_currency_data関数)をさらに詳しく見ていきます。それではコードを見ていきましょう。

def get_currency_data():
    # Define currency pairs and the amount of data
    symbols = ["AUDUSD", "AUDJPY", "CADJPY", "AUDCHF", "AUDNZD", "USDCAD", "USDCHF", "USDJPY", "NZDUSD", "GBPUSD", "EURUSD", "CADCHF", "CHFJPY", "NZDCAD", "NZDCHF", "NZDJPY", "GBPCAD", "GBPCHF", "GBPJPY", "GBPNZD", "EURCAD", "EURCHF", "EURGBP", "EURJPY", "EURNZD"]
    count = 1000  # number of data points for each currency pair
    data = {}
    for symbol in symbols:
        df = get_mt5_data(symbol, count, terminal_path)
        if df is not None:
            data[symbol] = df[['time', 'bid', 'ask']].set_index('time')
    return data

最初に通貨ペアを定義します。リストには、AUDUSD、EURUSD、GBPJPYなど、馴染みのある銘柄が含まれています。

では次のステップに進みます。空のdata辞書の作成です。ここには後ほど必要な情報が格納されます。

そして関数が実行を開始します。通貨ペアのリストを順番に処理していきます。各ペアに対してget_mt5_dataを呼び出し、もしデータが取得できた場合(Noneでない場合)は、重要な情報(time、bid、ask)のみを抽出します。

そして最後に、この関数はデータで埋められた辞書を返します。 

こうしてget_currency_data関数が完成します。小さくても強力で、シンプルながらも非常に効果的な関数です。


2000年の総合価格の計算:戦略と実行

システムの基本に踏み込みましょう。calculate_synthetic_prices関数です。これは合成データを取得することを可能にします。

それではコードを見ていきましょう。

def calculate_synthetic_prices(data):
    synthetic_prices = {}

    # Remove duplicate indices from all DataFrames in the data dictionary
    for key in data:
        data[key] = remove_duplicate_indices(data[key])

    # Calculate synthetic prices for all pairs using multiple methods
    pairs = [('AUDUSD', 'USDCHF'), ('AUDUSD', 'NZDUSD'), ('AUDUSD', 'USDJPY'),
             ('USDCHF', 'USDCAD'), ('USDCHF', 'NZDCHF'), ('USDCHF', 'CHFJPY'),
             ('USDJPY', 'USDCAD'), ('USDJPY', 'NZDJPY'), ('USDJPY', 'GBPJPY'),
             ('NZDUSD', 'NZDCAD'), ('NZDUSD', 'NZDCHF'), ('NZDUSD', 'NZDJPY'),
             ('GBPUSD', 'GBPCAD'), ('GBPUSD', 'GBPCHF'), ('GBPUSD', 'GBPJPY'),
             ('EURUSD', 'EURCAD'), ('EURUSD', 'EURCHF'), ('EURUSD', 'EURJPY'),
             ('CADCHF', 'CADJPY'), ('CADCHF', 'GBPCAD'), ('CADCHF', 'EURCAD'),
             ('CHFJPY', 'GBPCHF'), ('CHFJPY', 'EURCHF'), ('CHFJPY', 'NZDCHF'),
             ('NZDCAD', 'NZDJPY'), ('NZDCAD', 'GBPNZD'), ('NZDCAD', 'EURNZD'),
             ('NZDCHF', 'NZDJPY'), ('NZDCHF', 'GBPNZD'), ('NZDCHF', 'EURNZD'),
             ('NZDJPY', 'GBPNZD'), ('NZDJPY', 'EURNZD')]

    method_count = 1
    for pair1, pair2 in pairs:
        print(f"Calculating synthetic price for {pair1} and {pair2} using method {method_count}")
        synthetic_prices[f'{pair1}_{method_count}'] = data[pair1]['bid'] / data[pair2]['ask']
        method_count += 1
        print(f"Calculating synthetic price for {pair1} and {pair2} using method {method_count}")
        synthetic_prices[f'{pair1}_{method_count}'] = data[pair1]['bid'] / data[pair2]['bid']
        method_count += 1

    return pd.DataFrame(synthetic_prices)


裁定取引の機会の分析:analyze_arbitrage関数

まず、空の辞書synthetic_pricesを作成し、そこにデータを格納していきます。次に、すべてのデータを確認して重複したインデックスを削除し、将来的なエラーを防ぎます。

次のステップは「pairs」リストの用意です。これは合成に使う通貨ペアのリストです。その後、すべてのペアをループ処理します。各ペアに対して、次の2つの方法で合成価格を計算します。

  1. 1つ目のペアのbid価格を2つ目のペアのask価格で割る
  2. 1つ目のペアのbid価格を2つ目のペアのbid価格で割る

計算するたびにmethod_countを増やし、 結果として2000もの合成ペアを作り出します。

calculate_synthetic_prices関数は単に価格を計算するだけでなく、新しい取引チャンスを生み出します。この機能は裁定取引の機会という形で大きな成果をもたらします。


結果の視覚化:データをCSVに保存する

analyze_arbitrage関数を見てみましょう。この関数は単にデータを分析するだけでなく、膨大な数値の流れから必要な情報を探し出します。内容を見ていきましょう。

def analyze_arbitrage(data, synthetic_prices, method_count):
    # Calculate spreads for each pair
    spreads = {}
    for pair in data.keys():
        for i in range(1, method_count + 1):
            synthetic_pair = f'{pair}_{i}'
            if synthetic_pair in synthetic_prices.columns:
                print(f"Analyzing arbitrage opportunity for {synthetic_pair}")
                spreads[synthetic_pair] = data[pair]['bid'] - synthetic_prices[synthetic_pair]
    # Identify arbitrage opportunities
    arbitrage_opportunities = pd.DataFrame(spreads) > 0.00008
    print("Arbitrage opportunities:")
    print(arbitrage_opportunities)
    # Save the full table of arbitrage opportunities to a CSV file
    arbitrage_opportunities.to_csv('arbitrage_opportunities.csv')
    return arbitrage_opportunities

まず、この関数は空の「spreads」辞書を作成します。そこにデータを格納していきます。

次のステップに進みましょう。関数はすべての通貨ペアとその合成ペアを順に処理します。れぞれのペアについて、スプレッド(実際のbid価格と合成価格の差)を計算します。

spreads[synthetic_pair] = data[pair]['bid'] - synthetic_prices[synthetic_pair]

この行は非常に重要な役割を果たしています。実価格と合成価格の差を求め、その差が正であれば、裁定取引のチャンスがあるということです。

さらに信頼性の高い結果を得るために、0.00008という値を基準にします。

arbitrage_opportunities = pd.DataFrame(spreads) > 0.00008

この行では、8ポイント未満の差をすべて除外します。こうすることで、より利益の見込める機会だけを抽出できます。

次のステップは次のとおりです。

arbitrage_opportunities.to_csv('arbitrage_opportunities.csv')

これにより、すべてのデータがCSVファイルとして保存されます。これで私たちはそのデータを分析したり、グラフを作成したりと、より生産的な作業ができるようになります。すべてはこのanalyze_arbitrage関数のおかげです。この関数は単なる分析ツールではなく、チャンスを探し出し、見つけ出し、そして保存してくれるのです。


テスト注文の開始:open_test_limit_order関数

次に、open_test_limit_order関数について見ていきましょう。この関数は、私たちの代わりにテスト注文を出してくれる役割を果たします。

見てみましょう。

def open_test_limit_order(symbol, order_type, price, volume, take_profit, stop_loss, terminal_path):
    if not mt5.initialize(path=terminal_path):
        print(f"Failed to connect to MetaTrader 5 terminal at {terminal_path}")
        return None
    symbol_info = mt5.symbol_info(symbol)
    positions_total = mt5.positions_total()
    if symbol_info is None:
        print(f"Instrument not found: {symbol}")
        return None
    if positions_total >= MAX_OPEN_TRADES:
        print("MAX POSITIONS TOTAL!")
        return None
    # Check if symbol_info is None before accessing its attributes
    if symbol_info is not None:
        request = {
            "action": mt5.TRADE_ACTION_DEAL,
            "symbol": symbol,
            "volume": volume,
            "type": order_type,
            "price": price,
            "deviation": 30,
            "magic": 123456,
            "comment": "Stochastic Stupi Sustem",
            "type_time": mt5.ORDER_TIME_GTC,
            "type_filling": mt5.ORDER_FILLING_IOC,
            "tp": price + take_profit * symbol_info.point if order_type == mt5.ORDER_TYPE_BUY else price - take_profit * symbol_info.point,
            "sl": price - stop_loss * symbol_info.point if order_type == mt5.ORDER_TYPE_BUY else price + stop_loss * symbol_info.point,
        }
        result = mt5.order_send(request)
        if result is not None and result.retcode == mt5.TRADE_RETCODE_DONE:
            print(f"Test limit order placed for {symbol}")
            return result.order
        else:
            print(f"Error: Test limit order not placed for {symbol}, retcode={result.retcode if result is not None else 'None'}")
            return None
    else:
        print(f"Error: Symbol info not found for {symbol}")
        return None

この関数が最初におこなうことは、MetaTrader 5端末への接続を試行することです。そして、取引したい銘柄が実際に存在するかどうかを確認します。

次のコード:

if positions_total >= MAX_OPEN_TRADES:
    print("MAX POSITIONS TOTAL!")
    return None

このチェックは、同時に開くポジション数の上限を超えないようにするためのものです。

続いて、注文を発行するためのリクエストを生成します。ここには、注文タイプ、数量、価格、偏差、マジックナンバー、コメントなどの、さまざまなパラメータが含まれます。すべてが正常に処理されれば、関数はその旨を出力してくれます。何か問題が発生した場合には、エラーメッセージを表示します。

これがopen_test_limit_order関数の基本的な動作です。この関数は、私たちと市場をつなぐ役割を担っています。ある意味では、ブローカーのような存在です。


一時的な取引制限:特定の時間帯の取引

それでは取引時間についてお話しましょう。 

if current_time >= datetime.strptime("23:30", "%H:%M").time() or current_time <= datetime.strptime("05:00", "%H:%M").time():
    print("Current time is between 23:30 and 05:00. Skipping execution.")
    time.sleep(300)  # Wait for 5 minutes before checking again
    continue

どうなっているのでしょうか。システムは現在の時刻をチェックします。時刻が午後11時30分から午前5時の間であれば、取引時間外と判断して5分間スタンバイモードに入ります。その後再び起動して時間を確認し、まだ取引時間外であれば再度スタンバイに入ります。

これは何故必要なのでしょうか。理由は大きく分けて3つあります。まずは、流動性です。夜間は、通常は取引量が少なくなります。2番目はスプレッドです。夜間は、スプレッドが広がりやすくなります。3番目はニュースです。重要なニュースは、たいてい日中の営業時間中に発表されます。


実行時ループとエラー処理

「main」関数を見てみましょう。これはまるで船長のような存在ですが、舵の代わりにキーボードを使います。何をするのでしょうか。やることはシンプルです。

  1. データの収集
  2. 合成価格の計算 
  3. 裁定取引の機会を探す 
  4. 注文を出す

さらに、簡単なエラー処理も含まれています。 

def main():
    data = get_currency_data()
    synthetic_prices = calculate_synthetic_prices(data)
    method_count = 2000  # Define the method_count variable here
    arbitrage_opportunities = analyze_arbitrage(data, synthetic_prices, method_count)

    # Trade based on arbitrage opportunities
    for symbol in arbitrage_opportunities.columns:
        if arbitrage_opportunities[symbol].any():
            direction = "BUY" if arbitrage_opportunities[symbol].iloc[0] else "SELL"
            symbol = symbol.split('_')[0]  # Remove the index from the symbol
            symbol_info = mt5.symbol_info_tick(symbol)
            if symbol_info is not None:
                price = symbol_info.bid if direction == "BUY" else symbol_info.ask
                take_profit = 450
                stop_loss = 200
                order = open_test_limit_order(symbol, mt5.ORDER_TYPE_BUY if direction == "BUY" else mt5.ORDER_TYPE_SELL, price, 0.50, take_profit, stop_loss, terminal_path)
            else:
                print(f"Error: Symbol info tick not found for {symbol}")


システムのスケーラビリティ:新しい通貨ペアとメソッドの追加

新しい通貨ペアを追加する場合、単にこのリストに追加してください。

symbols = ["EURUSD", "GBPUSD", "USDJPY", ... , "YOURPAIR"]

システムは新しいペアを認識するようになりました。新しい計算メソッドについてはどうでしょうか。

def calculate_synthetic_prices(data):
    # ... existing code ...
    
    # Add a new method
    synthetic_prices[f'{pair1}_{method_count}'] = data[pair1]['ask'] / data[pair2]['bid']
    method_count += 1


裁定取引システムのテストとバックテスト

バックテストについて話しましょう。これは、あらゆる取引システムにとって非常に重要なポイントです。私たちの裁定取引システムも例外ではありません。

私たちは何をしたのでしょうか。過去のデータに基づいて戦略を実行しました。なぜでしょうか。その効率性を確認するためです。コードはまず、get_historical_dataから始まります。この関数は、MetaTrader 5から過去のデータを取得します。このデータがなければ、生産的な作業はできません。

続いてcalculate_synthetic_pricesが登場します。ここでは、合成為替レートを計算します。これは裁定取引戦略の中核部分です。analyze_arbitrageは機会を検出する機能です。実際の価格と合成価格を比較し、その差を見つけて潜在的な利益を狙います。simulate_tradeはほぼ取引そのものですが、テストモードで実行されます。これは非常に重要なプロセスです。本番でお金を失うより、シミュレーションで失敗する方が良いのです。

最後に、backtest_arbitrage_systemがすべてを統合し、履歴データを使って戦略を実行します。日々、ひとつひとつの取引を検証していきます。

import MetaTrader5 as mt5
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
import pytz

# Path to MetaTrader 5 terminal
terminal_path = "C:/Program Files/ForexBroker - MetaTrader 5/Arima/terminal64.exe"

def remove_duplicate_indices(df):
    """Removes duplicate indices, keeping only the first row with a unique index."""
    return df[~df.index.duplicated(keep='first')]

def get_historical_data(start_date, end_date, terminal_path):
    if not mt5.initialize(path=terminal_path):
        print(f"Failed to connect to MetaTrader 5 terminal at {terminal_path}")
        return None

    symbols = ["AUDUSD", "AUDJPY", "CADJPY", "AUDCHF", "AUDNZD", "USDCAD", "USDCHF", "USDJPY", "NZDUSD", "GBPUSD", "EURUSD", "CADCHF", "CHFJPY", "NZDCAD", "NZDCHF", "NZDJPY", "GBPCAD", "GBPCHF", "GBPJPY", "GBPNZD", "EURCAD", "EURCHF", "EURGBP", "EURJPY", "EURNZD"]
    
    historical_data = {}
    for symbol in symbols:
        timeframe = mt5.TIMEFRAME_M1
        rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date)
        if rates is not None and len(rates) > 0:
            df = pd.DataFrame(rates)
            df['time'] = pd.to_datetime(df['time'], unit='s')
            df.set_index('time', inplace=True)
            df = df[['open', 'high', 'low', 'close']]
            df['bid'] = df['close']  # Simplification: use 'close' as 'bid'
            df['ask'] = df['close'] + 0.000001  # Simplification: add spread
            historical_data[symbol] = df

    mt5.shutdown()
    return historical_data

def calculate_synthetic_prices(data):
    synthetic_prices = {}
    pairs = [('AUDUSD', 'USDCHF'), ('AUDUSD', 'NZDUSD'), ('AUDUSD', 'USDJPY'),
             ('USDCHF', 'USDCAD'), ('USDCHF', 'NZDCHF'), ('USDCHF', 'CHFJPY'),
             ('USDJPY', 'USDCAD'), ('USDJPY', 'NZDJPY'), ('USDJPY', 'GBPJPY'),
             ('NZDUSD', 'NZDCAD'), ('NZDUSD', 'NZDCHF'), ('NZDUSD', 'NZDJPY'),
             ('GBPUSD', 'GBPCAD'), ('GBPUSD', 'GBPCHF'), ('GBPUSD', 'GBPJPY'),
             ('EURUSD', 'EURCAD'), ('EURUSD', 'EURCHF'), ('EURUSD', 'EURJPY'),
             ('CADCHF', 'CADJPY'), ('CADCHF', 'GBPCAD'), ('CADCHF', 'EURCAD'),
             ('CHFJPY', 'GBPCHF'), ('CHFJPY', 'EURCHF'), ('CHFJPY', 'NZDCHF'),
             ('NZDCAD', 'NZDJPY'), ('NZDCAD', 'GBPNZD'), ('NZDCAD', 'EURNZD'),
             ('NZDCHF', 'NZDJPY'), ('NZDCHF', 'GBPNZD'), ('NZDCHF', 'EURNZD'),
             ('NZDJPY', 'GBPNZD'), ('NZDJPY', 'EURNZD')]

    for pair1, pair2 in pairs:
        if pair1 in data and pair2 in data:
            synthetic_prices[f'{pair1}_{pair2}_1'] = data[pair1]['bid'] / data[pair2]['ask']
            synthetic_prices[f'{pair1}_{pair2}_2'] = data[pair1]['bid'] / data[pair2]['bid']

    return pd.DataFrame(synthetic_prices)

def analyze_arbitrage(data, synthetic_prices):
    spreads = {}
    for pair in data.keys():
        for synth_pair in synthetic_prices.columns:
            if pair in synth_pair:
                spreads[synth_pair] = data[pair]['bid'] - synthetic_prices[synth_pair]

    arbitrage_opportunities = pd.DataFrame(spreads) > 0.00008
    return arbitrage_opportunities

def simulate_trade(data, direction, entry_price, take_profit, stop_loss):
    for i, row in data.iterrows():
        current_price = row['bid'] if direction == "BUY" else row['ask']
        
        if direction == "BUY":
            if current_price >= entry_price + take_profit:
                return {'profit': take_profit * 800, 'duration': i}
            elif current_price <= entry_price - stop_loss:
                return {'profit': -stop_loss * 400, 'duration': i}
        else:  # SELL
            if current_price <= entry_price - take_profit:
                return {'profit': take_profit * 800, 'duration': i}
            elif current_price >= entry_price + stop_loss:
                return {'profit': -stop_loss * 400, 'duration': i}
    
    # If the loop completes without hitting TP or SL, close at the last price
    last_price = data['bid'].iloc[-1] if direction == "BUY" else data['ask'].iloc[-1]
    profit = (last_price - entry_price) * 100000 if direction == "BUY" else (entry_price - last_price) * 100000
    return {'profit': profit, 'duration': len(data)}

def backtest_arbitrage_system(historical_data, start_date, end_date):
    equity_curve = [10000]  # Starting with $10,000
    trades = []
    dates = pd.date_range(start=start_date, end=end_date, freq='D')

    for current_date in dates:
        print(f"Backtesting for date: {current_date.date()}")
        
        # Get data for the current day
        data = {symbol: df[df.index.date == current_date.date()] for symbol, df in historical_data.items()}
        
        # Skip if no data for the current day
        if all(df.empty for df in data.values()):
            continue

        synthetic_prices = calculate_synthetic_prices(data)
        arbitrage_opportunities = analyze_arbitrage(data, synthetic_prices)

        # Simulate trades based on arbitrage opportunities
        for symbol in arbitrage_opportunities.columns:
            if arbitrage_opportunities[symbol].any():
                direction = "BUY" if arbitrage_opportunities[symbol].iloc[0] else "SELL"
                base_symbol = symbol.split('_')[0]
                if base_symbol in data and not data[base_symbol].empty:
                    price = data[base_symbol]['bid'].iloc[-1] if direction == "BUY" else data[base_symbol]['ask'].iloc[-1]
                    take_profit = 800 * 0.00001  # Convert to price
                    stop_loss = 400 * 0.00001  # Convert to price
                    
                    # Simulate trade
                    trade_result = simulate_trade(data[base_symbol], direction, price, take_profit, stop_loss)
                    trades.append(trade_result)
                    
                    # Update equity curve
                    equity_curve.append(equity_curve[-1] + trade_result['profit'])

    return equity_curve, trades

def main():
    start_date = datetime(2024, 1, 1, tzinfo=pytz.UTC)
    end_date = datetime(2024, 8, 31, tzinfo=pytz.UTC)  # Backtest for January-August 2024
    
    print("Fetching historical data...")
    historical_data = get_historical_data(start_date, end_date, terminal_path)
    
    if historical_data is None:
        print("Failed to fetch historical data. Exiting.")
        return

    print("Starting backtest...")
    equity_curve, trades = backtest_arbitrage_system(historical_data, start_date, end_date)

    total_profit = sum(trade['profit'] for trade in trades)
    win_rate = sum(1 for trade in trades if trade['profit'] > 0) / len(trades) if trades else 0

    print(f"Backtest completed. Results:")
    print(f"Total Profit: ${total_profit:.2f}")
    print(f"Win Rate: {win_rate:.2%}")
    print(f"Final Equity: ${equity_curve[-1]:.2f}")

    # Plot equity curve
    plt.figure(figsize=(15, 10))
    plt.plot(equity_curve)
    plt.title('Equity Curve: Backtest Results')
    plt.xlabel('Trade Number')
    plt.ylabel('Account Balance ($)')
    plt.savefig('equity_curve.png')
    plt.close()

    print("Equity curve saved as 'equity_curve.png'.")

if __name__ == "__main__":
    main()

なぜこれが重要なのでしょうか。それは、バックテストがシステムの効率性を示してくれるからです。利益が出るのか、それとも証拠金を減らしてしまうのか。ドローダウンはどれくらいか。勝率は何パーセントか。これらすべては、バックテストから分かります。

もちろん、過去の結果が将来を保証するわけではありません。市場は常に変化しています。ですが、バックテストをしなければ、何の結果も得られません。結果を知ることで、おおよそ何が期待できるかが分かります。もうひとつ重要な点があります。それは、バックテストによってシステムの最適化ができるということです。パラメータを変更しては結果を確認し、それを何度も繰り返します。そうして一歩一歩、私たちのシステムを改善していくのです。

こちらが、私たちのシステムのバックテスト結果です。

以下はMetaTrader 5でのシステムのテストです。

以下はシステムのMQL5 EAのコードです。

//+------------------------------------------------------------------+
//|                                                 TrissBotDemo.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
// Input parameters
input int MAX_OPEN_TRADES = 10;
input double VOLUME = 0.50;
input int TAKE_PROFIT = 450;
input int STOP_LOSS = 200;
input double MIN_SPREAD = 0.00008;

// Global variables
string symbols[] = {"AUDUSD", "AUDJPY", "CADJPY", "AUDCHF", "AUDNZD", "USDCAD", "USDCHF", "USDJPY", "NZDUSD", "GBPUSD", "EURUSD", "CADCHF", "CHFJPY", "NZDCAD", "NZDCHF", "NZDJPY", "GBPCAD", "GBPCHF", "GBPJPY", "GBPNZD", "EURCAD", "EURCHF", "EURGBP", "EURJPY", "EURNZD"};
int symbolsTotal;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    symbolsTotal = ArraySize(symbols);
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    // Cleanup code here
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    if(!IsTradeAllowed()) return;
    
    datetime currentTime = TimeGMT();
    if(currentTime >= StringToTime("23:30:00") || currentTime <= StringToTime("05:00:00"))
    {
        Print("Current time is between 23:30 and 05:00. Skipping execution.");
        return;
    }
    
    AnalyzeAndTrade();
}

//+------------------------------------------------------------------+
//| Analyze arbitrage opportunities and trade                        |
//+------------------------------------------------------------------+
void AnalyzeAndTrade()
{
    double synthetic_prices[];
    ArrayResize(synthetic_prices, symbolsTotal);
    
    for(int i = 0; i < symbolsTotal; i++)
    {
        synthetic_prices[i] = CalculateSyntheticPrice(symbols[i]);
        double currentPrice = SymbolInfoDouble(symbols[i], SYMBOL_BID);
        
        if(MathAbs(currentPrice - synthetic_prices[i]) > MIN_SPREAD)
        {
            if(currentPrice > synthetic_prices[i])
            {
                OpenOrder(symbols[i], ORDER_TYPE_SELL);
            }
            else
            {
                OpenOrder(symbols[i], ORDER_TYPE_BUY);
            }
        }
        
    }
}

//+------------------------------------------------------------------+
//| Calculate synthetic price for a symbol                           |
//+------------------------------------------------------------------+
double CalculateSyntheticPrice(string symbol)
{
    // This is a simplified version. You need to implement the logic
    // to calculate synthetic prices based on your specific method
    return SymbolInfoDouble(symbol, SYMBOL_ASK);
}

//+------------------------------------------------------------------+
//| Open a new order                                                 |
//+------------------------------------------------------------------+
void OpenOrder(string symbol, ENUM_ORDER_TYPE orderType)
{
    if(PositionsTotal() >= MAX_OPEN_TRADES)
    {
        Print("MAX POSITIONS TOTAL!");
        return;
    }
    
    double price = (orderType == ORDER_TYPE_BUY) ? SymbolInfoDouble(symbol, SYMBOL_ASK) : SymbolInfoDouble(symbol, SYMBOL_BID);
    double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
    
    double tp = (orderType == ORDER_TYPE_BUY) ? price + TAKE_PROFIT * point : price - TAKE_PROFIT * point;
    double sl = (orderType == ORDER_TYPE_BUY) ? price - STOP_LOSS * point : price + STOP_LOSS * point;
    
    MqlTradeRequest request = {};
    MqlTradeResult result = {};
    
    request.action = TRADE_ACTION_DEAL;
    request.symbol = symbol;
    request.volume = VOLUME;
    request.type = orderType;
    request.price = price;
    request.deviation = 30;
    request.magic = 123456;
    request.comment = "ArbitrageAdvisor";
    request.type_time = ORDER_TIME_GTC;
    request.type_filling = ORDER_FILLING_IOC;
    request.tp = tp;
    request.sl = sl;
    
    if(!OrderSend(request, result))
    {
        Print("OrderSend error ", GetLastError());
        return;
    }
    
    if(result.retcode == TRADE_RETCODE_DONE)
    {
        Print("Order placed successfully");
    }
    else
    {
        Print("Order failed with retcode ", result.retcode);
    }
}

//+------------------------------------------------------------------+
//| Check if trading is allowed                                      |
//+------------------------------------------------------------------+
bool IsTradeAllowed()
{
    if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
    {
        Print("Trade is not allowed in the terminal");
        return false;
    }
    
    if(!MQLInfoInteger(MQL_TRADE_ALLOWED))
    {
        Print("Trade is not allowed in the Expert Advisor");
        return false;
    }
    
    return true;
}


ブローカー向けシステムの改善可能性と合法性、あるいは指値注文で流動性プロバイダーに打撃を与えない方法

私たちのシステムには、他にも潜在的な課題があります。ブローカーや流動性プロバイダーは、このようなシステムをあまり歓迎しないことが多いのです。なぜかというと、私たちは本質的に市場から必要な流動性を奪っているからです。これには「Toxic Order Flow(有害な注文フロー)」という専門用語すら存在します。 

これは現実的な問題です。私たちは成行注文によって、文字通り市場から流動性を吸い上げています。流動性は、大口投資家にも小口トレーダーにも必要不可欠なものです。当然、そこには悪影響が出る可能性もあります。

このような状況ではどうすればいいでしょうか。ひとつの妥協案が、指値注文の活用です。 

しかし、指値注文だけで問題がすべて解決するわけではありません。Toxic Order Flowと見なされる本当の理由は、「市場からの流動性の奪取」そのものというより、その注文フローを処理する負荷が極めて高いという点にあります。この問題は、私自身まだ解決できていません。たとえば、膨大な裁定取引の注文を処理するのに100ドルかかるのに対して、そこから得られる手数料が50ドルしかないのであれば、それはブローカーにとって赤字です。したがって、高回転率・高ロットサイズ・高速執行といった要素がカギになる可能性があります。この条件を満たせば、ブローカー側がリベートを支払う余地も出てくるでしょう。

ではコードに移りましょう。どうすれば改善できるでしょうか。まず第一に、指値注文を処理する機能を追加することが挙げられます。ただし、これには多くの検討事項があります。未約定注文の「待機ロジック」や「キャンセル条件」などをしっかり設計する必要があります。

また、機械学習の導入も興味深いアプローチです。たとえば、「どの裁定取引の機会が最も成功しやすいか」を予測するように、システムを訓練できるかもしれません。 


結論

まとめてみましょう。私たちは、裁定取引のチャンスを見つけるシステムを構築しました。 ただし、忘れてはならないのは、このシステムがすべての経済的問題を解決してくれるわけではないということです。 

バックテストについても整理しました。これは時間ベースのデータを扱い、システムが過去にどのように動作していたかを確認することができます。しかし、過去の結果が将来の成果を保証するわけではありません。市場というのは、常に変化し続ける複雑なメカニズムなのです。

では、何がいちばん重要なのでしょうか。コードではありません。アルゴリズムでもありません。最も重要なのは、あなた自身です。学び、実験し、間違え、そしてまた挑戦し続けようとする意志。これこそが本当に価値あるものです。

だから、そこで立ち止まらないでください。このシステムは、あなたのアルゴリズム取引の旅の出発点に過ぎません。新しいアイデアや新たな戦略への足がかりとして活用してください。人生と同じように、取引においても最も大切なのはバランスです。リスクと慎重さ、欲望と理性、複雑さとシンプルさの間でバランスを取ることです。

この刺激的な旅におけるあなたの成功を祈っています。そして、あなたのアルゴリズムが常に市場の一歩先を行きますように。

MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/15964

最後のコメント | ディスカッションに移動 (8)
Andrey Khatimlianskii
Andrey Khatimlianskii | 28 10月 2024 において 16:09
pivomoe #:

最初のペアの入札は?最初のペアは

AUDUSD もペアです。AUDからUSDへ。

Roman Shiredchenko
Roman Shiredchenko | 28 10月 2024 において 20:12
pivomoe #:

どういうことなのか説明してほしい:

ペアです:

最初のペアの入札は何ですか?最初のペアは

これがシンセティックスの作り方だ。差ではなく、分割。そして単純ではなく、.......
leonerd
leonerd | 21 11月 2024 において 10:59
ticks = mt5.copy_ticks_from(symbol, utc_from, count, mt5.COPY_TICKS_ALL)

すべてインストールしました。これがティックで出てくる:

array([b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、

b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、

b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、

b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''、b''】、]

dtype='|V0')


そして、ここではすでに時間通りに終了している:

ticks_frame['time'] = pd.to_datetime(ticks_frame['time'], unit='s')
leonerd
leonerd | 22 11月 2024 において 09:11

例のコードhttps://www.mql5.com/ja/docs/python_metatrader5/mt5copyticksfrom_py も動きません。

>>>  timezone = pytz.timezone("Etc/UTC")
>>>  utc_from = datetime(2020, 1, 10, tzinfo=timezone)
>>>  ticks = mt5.copy_ticks_from("EURUSD", utc_from, 100000, mt5.COPY_TICKS_ALL)
>>>
>>> print("受信ダニ:",len(ticks))
Получено тиков: 100000
>>> print("結果としてのダニをそのまま受け止めよう")
Выведем полученные тики как есть
>>>  count = 0
>>> for tick in ticks:
...     count+=1
...     print(tick)
...     if count >= 100:
...         break
...
b''
b''
b''
b''

とにかく、pythonってどんな感じ?どのように準備すればいいのか?不明です...。

Yaochi Lin
Yaochi Lin | 28 8月 2025 において 22:25
これは本当に機能するのでしょうか?通常のブローカーではサポートされていません。
MetaTrader 5を使用してPythonでカスタム通貨ペアパターンを見つける MetaTrader 5を使用してPythonでカスタム通貨ペアパターンを見つける
外国為替市場には繰り返しパターンや規則性が存在するのでしょうか。私は、PythonとMetaTrader 5を使って独自のパターン分析システムを構築することに決めました。これは、外国為替市場を攻略するための、数学とプログラミングの一種の融合です。
人工散布アルゴリズム(ASHA) 人工散布アルゴリズム(ASHA)
この記事では、一般的な最適化問題を解決するために開発された新しいメタヒューリスティック手法、人工散布アルゴリズム(ASHA: Artificial Showering Algorithm)を紹介します。ASHAは、水の流れと蓄積のプロセスをシミュレーションすることで、各リソース単位(水)が最適解を探索する「理想フィールド」という概念を構築します。本稿では、ASHAがフローと蓄積の原理をどのように適応させ、探索空間内でリソースを効率的に割り当てるかを解説し、その実装およびテスト結果を紹介します。
経済予測:Pythonの可能性を探る 経済予測:Pythonの可能性を探る
世界銀行の経済データは、将来の動向を予測するためにどのように活用できるのでしょうか。そして、AIモデルと経済学を組み合わせることで、どのようなことが可能になるのでしょうか。
DoEasy - サービス関数(第3回):アウトサイドバーパターン DoEasy - サービス関数(第3回):アウトサイドバーパターン
本記事では、DoEasyライブラリにおけるアウトサイドバーのプライスアクションパターンを開発し、価格パターン管理へのアクセス手法を最適化します。あわせて、ライブラリのテスト中に判明したエラーや不具合の修正もおこないます。