XM 500ドル入金ボーナスの詳細はこちら ▶︎

無料スワップ表示ツール「swap_checker」の使い方・ソースコード

  • URLをコピーしました!

スワップアービトラージはFXの中でも期待値の高いトレード手法の1つです。プラススワップがマイナススワップを上回る組み合わせでポジションを両建てすることで、為替リスクを無視してスワップ収益を得ることができます。

ただ海外FXブローカーの通貨ペアごとのスワップポイントを調べるのは手間がかかります。

そんな時はPythonスクリプトの「swap_checker」を使いましょう。スワップポイントを通貨ペアごとに一覧表示でき、スワップ収益の高い順に並べ替えることもできます。

この記事ではスワップ一覧表示ツールの使い方・無料公開ソースコードを紹介していきます。

目次

スワップ表示ツールの無料公開ソースコード

スワップ表示ツールのソースコードはこちら。(ソースコード引用元

import MetaTrader5 as mt5
import time
from datetime import datetime
import sys
import tkinter as tk
from tkinter import ttk, messagebox
import threading
import traceback

# 実行中のMT5プロセスを検出するためにpsutilライブラリを使用します
try:
    import psutil
except ImportError:
    psutil = None

# tksheetライブラリをインポート
# 事前に `pip install tksheet` を実行してください
try:
    import tksheet
except ImportError:
    messagebox.showerror(
        "ライブラリ不足",
        "tksheetライブラリが見つかりません。\nコマンドプロンプトで `pip install tksheet` を実行してインストールしてください。"
    )
    sys.exit(1)


# --- MT5ターミナル選択部分は変更なし ---
def _discover_terminals() -> list[str]:
    paths = []
    if psutil:
        for p in psutil.process_iter(attrs=["name", "exe"]):
            if "terminal64.exe" in (p.info.get("name") or "").lower():
                exe_path = p.info.get("exe")
                if exe_path and exe_path not in paths:
                    paths.append(exe_path)
    return paths

def choose_terminal() -> str | None:
    if not psutil:
         messagebox.showerror(
            "ライブラリ不足",
            "psutilライブラリが見つかりません。\nコマンドプロンプトで `pip install psutil` を実行してインストールしてください。"
        )
         return None
    root = tk.Tk()
    root.withdraw()
    win = tk.Toplevel(root)
    win.title("接続するMT5ターミナルを選択")
    win.grab_set()
    message_label = None
    def on_close():
        win.destroy()
        root.destroy()
    win.protocol("WM_DELETE_WINDOW", on_close)
    def populate_tree():
        nonlocal message_label
        if message_label and message_label.winfo_exists():
            message_label.destroy()
            message_label = None
        for i in tree.get_children():
            tree.delete(i)
        found_terminals = _discover_terminals()
        if not found_terminals:
            tree.grid_remove()
            message_label = ttk.Label(win, text="実行中のMT5ターミナルが見つかりません。\nMT5を起動後、「再読込」を押してください。")
            message_label.grid(row=0, column=0, columnspan=3, padx=10, pady=10)
            use_button.config(state="disabled")
            return
        tree.grid()
        is_data_found = False
        for exe in found_terminals:
            try:
                if mt5.initialize(path=exe):
                    acc = mt5.account_info()
                    if acc:
                        tree.insert("", tk.END, values=(exe, acc.login, acc.server, f"{acc.balance:.2f} {acc.currency}", acc.name))
                        is_data_found = True
                    mt5.shutdown()
            except Exception as e:
                print(f"ターミナルへの接続エラー {exe}: {e}")
        if not is_data_found:
            tree.grid_remove()
            message_label = ttk.Label(win, text="MT5アカウント情報が取得できませんでした。\nログイン状態を確認してください。")
            message_label.grid(row=0, column=0, columnspan=3, padx=10, pady=10)
            use_button.config(state="disabled")
        else:
            if tree.get_children():
                tree.selection_set(tree.get_children()[0])
            use_button.config(state="normal")
    cols = ("exe", "login", "server", "balance", "name")
    tree = ttk.Treeview(win, columns=cols, show="headings", height=8)
    col_widths = {"exe": 340, "login": 80, "server": 170, "balance": 120, "name": 150}
    for c in cols:
        tree.heading(c, text=c.capitalize())
        tree.column(c, width=col_widths[c], anchor="w", stretch=tk.FALSE)
    tree.grid(row=0, column=0, columnspan=3, padx=6, pady=6)
    selected_path: dict[str, str | None] = {"path": None}
    def _use_selection() -> None:
        if tree.selection():
            selected_path["path"] = tree.item(tree.selection()[0], "values")[0]
        win.destroy()
    button_frame = ttk.Frame(win)
    button_frame.grid(row=1, column=0, columnspan=3, pady=(0, 6), padx=6, sticky="e")
    ttk.Button(button_frame, text="再読込", command=populate_tree).pack(side="left", padx=(0, 5))
    use_button = ttk.Button(button_frame, text="このターミナルを使用", command=_use_selection)
    use_button.pack(side="left")
    populate_tree()
    win.wait_window()
    root.destroy()
    return selected_path["path"]


class SwapViewerApp:
    def __init__(self, terminal_path: str):
        self.path = terminal_path
        self.root = tk.Tk()
        self.root.title("MT5 スワップチェッカー")
        self.root.geometry("1450x600")

        self.timer_id = None
        self.current_data = []
        self.last_sort_col = "swap_both_1d_jpy"
        self.last_sort_reverse = True

        if not self._connect_mt5():
            self.root.destroy()
            return
        
        self._setup_ui()
        self.update_data()
        self.root.protocol("WM_DELETE_WINDOW", self._on_closing)

    def _connect_mt5(self) -> bool:
        if not mt5.initialize(path=self.path):
            messagebox.showerror("MT5接続エラー", f"MT5の初期化に失敗しました: {mt5.last_error()}")
            return False
        self.account_info = mt5.account_info()
        if not self.account_info:
            messagebox.showerror("MT5接続エラー", f"アカウント情報の取得に失敗しました: {mt5.last_error()}")
            mt5.shutdown()
            return False
        if self.account_info.currency != "JPY":
            messagebox.showwarning("口座通貨の確認", f"このツールの円換算機能は、口座通貨がJPYの場合に最適化されています。\n現在の口座通貨: {self.account_info.currency}")
        return True

    def _setup_ui(self):
        main_frame = ttk.Frame(self.root, padding=10)
        main_frame.pack(fill=tk.BOTH, expand=True)
        top_frame = ttk.Frame(main_frame)
        top_frame.pack(fill=tk.X, pady=(0, 5))
        conn_text = f"接続先: {self.account_info.server} ({self.account_info.login})"
        ttk.Label(top_frame, text=conn_text, font=("Meiryo", 10)).pack(side=tk.LEFT)
        self.last_updated_var = tk.StringVar(value="最終更新: ----/--/-- --:--:--")
        ttk.Label(top_frame, textvariable=self.last_updated_var).pack(side=tk.RIGHT)
        
        sheet_frame = ttk.Frame(main_frame) 
        sheet_frame.pack(fill=tk.BOTH, expand=True)
        
        self.columns = {
            "symbol":           ("通貨ペア", 110, "w"),
            "swap_long_pts":    ("買いスワップ(Pts/%)", 150, "e"), "swap_short_pts": ("売りスワップ(Pts/%)", 150, "e"),
            "swap_long_jpy":    ("1Lot損益(買/円)", 140, "e"), "swap_short_jpy": ("1Lot損益(売/円)", 140, "e"),
            "swap_both_1d_jpy": ("1日両建て損益(円)", 160, "e"), "rollover_day": ("3倍デー適用曜日", 130, "center"),
            "swap_long_3d":     ("3倍デー(買/円)", 140, "e"), "swap_short_3d":     ("3倍デー(売/円)", 140, "e"),
            "swap_both_3d_jpy": ("3倍デー両建て損益(円)", 180, "e"),
        }
        
        self.sheet = tksheet.Sheet(sheet_frame,
                                   headers=[h for h, w, a in self.columns.values()],
                                   show_toolbar=False,
                                   show_top_left=False)
        self.sheet.pack(fill=tk.BOTH, expand=True)
        self.sheet.enable_bindings("single_select", "drag_select", "ctrl_select", "arrowkeys", "right_click_popup_menu", "rc_select")
        
        for i, (col_id, (header, width, anchor)) in enumerate(self.columns.items()):
            align = "w" if anchor == "w" else "e" if anchor == "e" else "center"
            self.sheet.column_width(column=i, width=width)
            self.sheet.align_columns(columns=i, align=align)

        
        # ヘッダークリックのイベント登録を削除
        
        # --- 下部コントロールパネル ---
        bottom_frame = ttk.Frame(main_frame)
        bottom_frame.pack(fill=tk.X, pady=(5, 0))
        
        # 更新ボタン
        self.update_button = ttk.Button(bottom_frame, text="手動更新", command=self.update_data)
        self.update_button.pack(side=tk.LEFT, padx=(0, 10))

        
        # 手動ソート機能のUIを追加
        sort_frame = ttk.Frame(bottom_frame)
        sort_frame.pack(side=tk.LEFT)
        
        ttk.Label(sort_frame, text="並べ替え:").pack(side=tk.LEFT, padx=(0, 5))

        # 表示名と内部IDの対応辞書を作成
        self.col_display_to_id = {v[0]: k for k, v in self.columns.items()}
        
        # ソート列選択のプルダウンメニュー
        self.sort_combo_var = tk.StringVar()
        self.sort_combo = ttk.Combobox(sort_frame, textvariable=self.sort_combo_var,
                                       values=[v[0] for v in self.columns.values()],
                                       state="readonly", width=25)
        self.sort_combo.set(self.columns[self.last_sort_col][0]) # 初期値を設定
        self.sort_combo.pack(side=tk.LEFT, padx=(0, 5))
        self.sort_combo.bind("<<ComboboxSelected>>", self._on_sort_combo_select)
        
        # 降順/昇順 切り替えボタン
        self.sort_order_button = ttk.Button(sort_frame, text="降順", command=self._on_sort_order_toggle, width=6)
        self.sort_order_button.pack(side=tk.LEFT)
        
        # ステータス表示
        self.status_var = tk.StringVar(value="準備完了")
        ttk.Label(bottom_frame, textvariable=self.status_var).pack(side=tk.LEFT, padx=20)
    
    
    # プルダウンメニューが選択された時の処理
    def _on_sort_combo_select(self, event=None):
        selected_display_name = self.sort_combo_var.get()
        new_sort_col = self.col_display_to_id[selected_display_name]
        self._sort_column(new_sort_col, keep_direction=True) # 並び順は維持したまま列を切り替え

    
    # 降順/昇順ボタンが押された時の処理
    def _on_sort_order_toggle(self, event=None):
        # 昇順/降順を反転させる
        self.last_sort_reverse = not self.last_sort_reverse
        # 現在選択されている列で再ソートを実行
        self._sort_column(self.last_sort_col, keep_direction=True)
        
    def _fetch_data_thread(self):
        
        try:
            all_symbols = mt5.symbols_get()
            if not all_symbols:
                self.root.after(0, self._update_gui_with_error, "エラー: 通貨ペアリストの取得に失敗。")
                return
            
            visible_symbols = [s for s in all_symbols if s.visible]
            if not visible_symbols:
                self.root.after(0, self._update_gui_with_error, "エラー: MT5の「気配値表示」に通貨ペアを追加してください。")
                return

            all_data = []
            weekdays = ["日", "月", "火", "水", "木", "金", "土"]

            for info in visible_symbols:
                tick = mt5.symbol_info_tick(info.name)
                if not tick or tick.bid == 0 or tick.ask == 0:
                    continue

                swap_long_jpy = 0.0
                swap_short_jpy = 0.0

                if info.swap_mode == mt5.SYMBOL_SWAP_MODE_POINTS:
                    tick_value = info.trade_tick_value
                    swap_long_jpy = info.swap_long * tick_value
                    swap_short_jpy = info.swap_short * tick_value
                elif info.swap_mode in [3, 5]:
                    swap_long_profit_ccy = 1.0 * info.trade_contract_size * tick.ask * (info.swap_long / 100) / 360
                    swap_short_profit_ccy = 1.0 * info.trade_contract_size * tick.bid * (info.swap_short / 100) / 360
                    if info.currency_profit != self.account_info.currency:
                        conversion_pair = info.currency_profit + self.account_info.currency
                        conversion_tick = mt5.symbol_info_tick(conversion_pair)
                        if conversion_tick and conversion_tick.bid > 0:
                            swap_long_jpy = swap_long_profit_ccy * conversion_tick.bid
                            swap_short_jpy = swap_short_profit_ccy * conversion_tick.bid
                        else:
                            continue
                    else:
                        swap_long_jpy = swap_long_profit_ccy
                        swap_short_jpy = swap_short_profit_ccy
                else:
                    continue
                
                all_data.append({
                    "symbol": info.name,
                    "swap_long_pts": info.swap_long, "swap_short_pts": info.swap_short,
                    "swap_long_jpy": swap_long_jpy, "swap_short_jpy": swap_short_jpy,
                    "swap_both_1d_jpy": swap_long_jpy + swap_short_jpy,
                    "rollover_day": weekdays[info.swap_rollover3days],
                    "swap_long_3d": swap_long_jpy * 3, "swap_short_3d": swap_short_jpy * 3,
                    "swap_both_3d_jpy": (swap_long_jpy + swap_short_jpy) * 3,
                    "swap_mode": info.swap_mode
                })
            
            self.root.after(0, self._update_gui, all_data)
        except Exception:
            traceback.print_exc()
            self.root.after(0, self._update_gui_with_error, "エラーが発生。詳細はコンソールを確認。")

    def _format_value(self, value, format_spec, is_jpy=False):
        unit = " 円" if is_jpy else ""
        if value == 0: return f"{value:{format_spec}}{unit}"
        return f"{value:+{format_spec}}{unit}"
    
    def _format_swap_points(self, value, mode):
        if mode in [3, 5]:
            return f"{value:+.3f} %"
        return f"{value:+.3f}"

    def _update_gui(self, data_list):
        self.current_data = data_list
        self._sort_column(self.last_sort_col, keep_direction=True)
        self.last_updated_var.set(f"最終更新: {datetime.now().strftime('%Y/%m/%d %H:%M:%S')}")
        self.status_var.set(f"データ取得完了。{len(data_list)}件の通貨ペアを表示中。")
        self.update_button.config(state="normal")

    def _redraw_view(self):
        
        if not self.current_data:
            self.sheet.set_sheet_data(data=[[]])
            return

        formatted_data = []
        for data in self.current_data:
            row = [
                data["symbol"],
                self._format_swap_points(data["swap_long_pts"], data["swap_mode"]),
                self._format_swap_points(data["swap_short_pts"], data["swap_mode"]),
                self._format_value(data["swap_long_jpy"], ",.2f", True),
                self._format_value(data["swap_short_jpy"], ",.2f", True),
                self._format_value(data["swap_both_1d_jpy"], ",.2f", True),
                data["rollover_day"],
                self._format_value(data["swap_long_3d"], ",.2f", True),
                self._format_value(data["swap_short_3d"], ",.2f", True),
                self._format_value(data["swap_both_3d_jpy"], ",.2f", True)
            ]
            formatted_data.append(row)

        self.sheet.set_sheet_data(data=formatted_data, redraw=False)

        color_target_cols = [
            "swap_long_pts", "swap_short_pts", "swap_long_jpy", "swap_short_jpy",
            "swap_both_1d_jpy", "swap_long_3d", "swap_short_3d", "swap_both_3d_jpy"
        ]
        
        col_id_to_index = {col_id: i for i, col_id in enumerate(self.columns.keys())}

        for r, data_row in enumerate(self.current_data):
            for col_id in color_target_cols:
                c = col_id_to_index[col_id]
                value = data_row[col_id]
                color = ""
                if value > 0:
                    color = "blue"
                elif value < 0:
                    color = "red"
                
                if color:
                    self.sheet.highlight_cells(row=r, column=c, fg=color)

        self.sheet.redraw()


    def _sort_column(self, col, keep_direction=False):
        if not self.current_data:
            return
        
        if not keep_direction:
            # この関数が直接呼ばれることはなくなったが念のため残す
            if self.last_sort_col == col:
                self.last_sort_reverse = not self.last_sort_reverse
            else:
                self.last_sort_reverse = True
        
        self.last_sort_col = col
        is_numeric = col not in ["symbol", "rollover_day"]
        
        key_func = (lambda d: d.get(col, 0)) if is_numeric else (lambda d: str(d.get(col, '')))
        self.current_data.sort(key=key_func, reverse=self.last_sort_reverse)
        
        # ソートボタンのテキストを現在の状態に合わせて更新
        self.sort_order_button.config(text="降順" if self.last_sort_reverse else "昇順")
        
        self._redraw_view()

    def _update_gui_with_error(self, message):
        self.sheet.set_sheet_data(data=[[]])
        self.status_var.set(message)
        self.update_button.config(state="normal")

    def update_data(self):
        self.status_var.set("データを取得中...")
        self.update_button.config(state="disabled")
        thread = threading.Thread(target=self._fetch_data_thread, daemon=True)
        thread.start()
        if self.timer_id: self.root.after_cancel(self.timer_id)
        self.timer_id = self.root.after(3600000, self.update_data)

    def _on_closing(self):
        if messagebox.askokcancel("終了確認", "アプリケーションを終了しますか?"):
            if self.timer_id: self.root.after_cancel(self.timer_id)
            mt5.shutdown()
            self.root.destroy()
            print("MT5との接続をシャットダウンしました。")

    def run(self):
        self.root.mainloop()

def main():
    terminal_path = choose_terminal()
    if not terminal_path:
        print("MT5ターミナルが選択されませんでした。プログラムを終了します。")
        sys.exit(0)
    app = SwapViewerApp(terminal_path)
    app.run()

if __name__ == "__main__":
    main()

スクリプトの稼働に成功すると、以下のようにスワップ損益を一覧表示できます。スワップの高い順にソートすることでスワップアービトラージにも転用できます。

Screenshot

Pythonスクリプトの使い方

このコードは、MetaTrader 5 (MT5) と連携して、気配値表示に表示されている全通貨ペアのスワップポイント情報を取得し、一覧表示するためのGUIアプリケーションです。特に、スワップポイントを日本円に換算した損益を表示したり、両建てした場合の損益を計算したりする機能が特徴です。

以下に、このコードの準備から使い方までを詳しく解説します。

1. このコードの機能

  • 実行中のMT5ターミナルを選択: 複数のMT5を同時に起動している場合でも、接続したいターミナルをアカウント情報(口座番号、サーバー、残高など)を見ながら選択できます。
  • スワップ情報の自動取得: 接続したMT5の気配値表示に登録されている全通貨ペアのスワップポイント(買い・売り)を取得します。
  • スワップ損益の円換算: 取得したスワップポイントを、1ロット(trade_contract_size)あたり1日で発生する日本円の損益に自動で換算して表示します。口座通貨がJPYでない場合でも、円への換算を試みます。
  • 両建て時の損益表示: 同じ通貨ペアを買いと売りで両建てした場合の1日あたりの合計損益を計算します。
  • スワップ3倍デー対応: スワップが3倍になる曜日と、その際の損益も計算して表示します。
  • ソート(並べ替え)機能: 表示されたデータを、特定の列(例: 「1日両建て損益(円)」)を基準に昇順・降順で並べ替えることができます。
  • 自動更新機能: 1時間ごとにデータを自動で再取得します(手動更新も可能です)。

2. 準備(実行前に必要なこと)

このコードを実行するには、お使いのPCに以下の環境が整っている必要があります。

  1. Pythonのインストール: PCにPythonがインストールされていない場合は、公式サイトからインストールしてください。
  2. MetaTrader 5 (64bit版)のインストール: MT5ターミナルがPCにインストールされている必要があります。
  3. 必要なPythonライブラリのインストール: このコードは複数の外部ライブラリを使用します。コマンドプロンプトやターミナルを開き、以下のコマンドを1つずつ実行してインストールしてください。 pip install MetaTrader5
    pip install psutil
    pip install tksheet
    コード内にもエラーチェックがありますが、事前にインストールしておくのが確実です。

3. 使い方(ステップ・バイ・ステップ)

  1. MT5を起動し、口座にログインする
    • まず、情報を取得したいMT5ターミナルを起動してください。
    • スワップ情報を確認したい取引口座にログインしておきます。
    • MT5の「気配値表示」に、情報を取得したい通貨ペアをすべて表示させておいてください。(このツールは気配値表示に表示されている通貨ペアのみを対象とします)
  2. Pythonスクリプトを実行する
    • このコードを swap_viewer.py のような名前で保存します。
    • コマンドプロンプトやターミナルで、保存したファイルがあるディレクトリに移動し、以下のコマンドを実行します。
    python swap_viewer.py
  3. 接続するMT5ターミナルを選択する
    • スクリプトを実行すると、最初に「接続するMT5ターミナルを選択」というウィンドウが表示されます。
    • 現在PCで実行中のMT5ターミナルが一覧で表示されます。口座番号やサーバー名を確認し、接続したいものを選択します。
    • 選択したら、「このターミナルを使用」ボタンをクリックします。
    • もしMT5を起動したばかりでリストに表示されない場合は、「再読込」ボタンを押してください。
  4. メイン画面でスワップ情報を確認する
    • ターミナルを選択すると、メインウィンドウが起動し、自動的にスワップ情報の取得が始まります。
    • しばらく待つと、以下のような情報が表形式で表示されます。
    各列の意味:
    • 通貨ペア: 通貨ペア名です。
    • 買いスワップ(Pts/%): 買いポジションの1日あたりのスワップ。単位はブローカーにより異なり、ポイントまたは年率(%)で表示されます。
    • 売りスワップ(Pts/%): 売りポジションの1日あたりのスワップ。
    • 1Lot損益(買/円): 買いポジションを1ロット保有した場合の1日あたりのスワップ損益(日本円)。
    • 1Lot損益(売/円): 売りポジションを1ロット保有した場合の1日あたりのスワップ損益(日本円)。
    • 1日両建て損益(円): 買いと売りを1ロットずつ両建てした場合の1日あたりの合計損益(日本円)。マイナスになることが多いです。
    • 3倍デー適用曜日: スワップが3日分付与される曜日です。
    • 3倍デー(買/円): 3倍デーの日の、買いポジション1ロットあたりのスワップ損益(日本円)。
    • 3倍デー(売/円): 3倍デーの日の、売りポジション1ロットあたりのスワップ損益(日本円)。
    • 3倍デー両建て損益(円): 3倍デーの日の、両建て1ロットあたりの合計損益(日本円)。
  5. データの更新と並べ替え
    • 手動更新: 左下の「手動更新」ボタンを押すと、いつでも最新のスワップ情報を取得できます。
    • 並べ替え:
      • 並べ替え:」の隣にあるプルダウンメニューから、基準にしたい列(例:「1日両建て損益(円)」)を選択します。
      • その隣の「降順」/「昇順」ボタンを押すことで、データの並び順を切り替えることができます。
      • これにより、「両建てした時に最も損失が少ない通貨ペア」や「買いスワップが最も高い通貨ペア」などを簡単に見つけることができます。
  6. アプリケーションの終了
    • ウィンドウ右上の「×」ボタンを押すと、「アプリケーションを終了しますか?」という確認メッセージが表示されます。
    • 「OK」を押すと、MT5との接続を安全に切断し、アプリケーションを終了します。

4. コードのポイント解説

  • psutilライブラリ: PC上で実行中のプロセスを検出するために使用しています。これにより、terminal64.exeという名前のプロセスを見つけ出し、MT5の実行パスを特定しています。
  • tksheetライブラリ: 高機能なテーブル(スプレッドシート)をGUI上に表示するために使用しています。標準のtkinterのウィジェットよりもはるかに見やすく、操作性に優れています。
  • threadingライブラリ: MT5からのデータ取得は時間がかかる可能性があるため、別スレッドで実行しています。これにより、データ取得中にGUIが固まってしまう(フリーズする)のを防いでいます。
  • スワップ計算ロジック: _fetch_data_threadメソッド内で、ブローカーごとのスワップ計算方式(info.swap_mode)を判定し、ポイント建ての場合と年率(%)建ての場合で計算方法を分けて、最終的にすべて日本円の損益に換算しています。

CSV出力できるようにするには?

ご提示のコードにCSV出力機能を追加するには、以下の手順で修正します

  1. 必要なライブラリをインポートする: ファイル保存ダイアログを表示するためのtkinter.filedialogと、CSVファイルを扱うためのcsvライブラリを追加します。
  2. UIに「CSV出力」ボタンを追加する: 手動更新ボタンの隣に、CSV出力を実行するためのボタンを配置します。
  3. CSV出力処理のメソッドを実装する: ボタンが押されたときに、現在表示されているデータをCSVファイルとして保存する処理を記述します。

以下が、上記の手順で修正を加えた完全なコードです。変更・追加した箇所には# <<< 変更/追加というコメントを付けています。

import MetaTrader5 as mt5
import time
from datetime import datetime
import sys
import tkinter as tk
from tkinter import ttk, messagebox, filedialog # <<< 変更/追加: filedialogを追加
import threading
import traceback
import csv # <<< 追加: CSVライブラリをインポート

# 実行中のMT5プロセスを検出するためにpsutilライブラリを使用します
try:
    import psutil
except ImportError:
    psutil = None

# tksheetライブラリをインポート
# 事前に `pip install tksheet` を実行してください
try:
    import tksheet
except ImportError:
    messagebox.showerror(
        "ライブラリ不足",
        "tksheetライブラリが見つかりません。\nコマンドプロンプトで `pip install tksheet` を実行してインストールしてください。"
    )
    sys.exit(1)


# --- MT5ターミナル選択部分は変更なし ---
def _discover_terminals() -> list[str]:
    paths = []
    if psutil:
        for p in psutil.process_iter(attrs=["name", "exe"]):
            if "terminal64.exe" in (p.info.get("name") or "").lower():
                exe_path = p.info.get("exe")
                if exe_path and exe_path not in paths:
                    paths.append(exe_path)
    return paths

def choose_terminal() -> str | None:
    if not psutil:
         messagebox.showerror(
            "ライブラリ不足",
            "psutilライブラリが見つかりません。\nコマンドプロンプトで `pip install psutil` を実行してインストールしてください。"
        )
         return None
    root = tk.Tk()
    root.withdraw()
    win = tk.Toplevel(root)
    win.title("接続するMT5ターミナルを選択")
    win.grab_set()
    message_label = None
    def on_close():
        win.destroy()
        root.destroy()
    win.protocol("WM_DELETE_WINDOW", on_close)
    def populate_tree():
        nonlocal message_label
        if message_label and message_label.winfo_exists():
            message_label.destroy()
            message_label = None
        for i in tree.get_children():
            tree.delete(i)
        found_terminals = _discover_terminals()
        if not found_terminals:
            tree.grid_remove()
            message_label = ttk.Label(win, text="実行中のMT5ターミナルが見つかりません。\nMT5を起動後、「再読込」を押してください。")
            message_label.grid(row=0, column=0, columnspan=3, padx=10, pady=10)
            use_button.config(state="disabled")
            return
        tree.grid()
        is_data_found = False
        for exe in found_terminals:
            try:
                if mt5.initialize(path=exe):
                    acc = mt5.account_info()
                    if acc:
                        tree.insert("", tk.END, values=(exe, acc.login, acc.server, f"{acc.balance:.2f} {acc.currency}", acc.name))
                        is_data_found = True
                    mt5.shutdown()
            except Exception as e:
                print(f"ターミナルへの接続エラー {exe}: {e}")
        if not is_data_found:
            tree.grid_remove()
            message_label = ttk.Label(win, text="MT5アカウント情報が取得できませんでした。\nログイン状態を確認してください。")
            message_label.grid(row=0, column=0, columnspan=3, padx=10, pady=10)
            use_button.config(state="disabled")
        else:
            if tree.get_children():
                tree.selection_set(tree.get_children()[0])
            use_button.config(state="normal")
    cols = ("exe", "login", "server", "balance", "name")
    tree = ttk.Treeview(win, columns=cols, show="headings", height=8)
    col_widths = {"exe": 340, "login": 80, "server": 170, "balance": 120, "name": 150}
    for c in cols:
        tree.heading(c, text=c.capitalize())
        tree.column(c, width=col_widths[c], anchor="w", stretch=tk.FALSE)
    tree.grid(row=0, column=0, columnspan=3, padx=6, pady=6)
    selected_path: dict[str, str | None] = {"path": None}
    def _use_selection() -> None:
        if tree.selection():
            selected_path["path"] = tree.item(tree.selection()[0], "values")[0]
        win.destroy()
    button_frame = ttk.Frame(win)
    button_frame.grid(row=1, column=0, columnspan=3, pady=(0, 6), padx=6, sticky="e")
    ttk.Button(button_frame, text="再読込", command=populate_tree).pack(side="left", padx=(0, 5))
    use_button = ttk.Button(button_frame, text="このターミナルを使用", command=_use_selection)
    use_button.pack(side="left")
    populate_tree()
    win.wait_window()
    root.destroy()
    return selected_path["path"]


class SwapViewerApp:
    def __init__(self, terminal_path: str):
        self.path = terminal_path
        self.root = tk.Tk()
        self.root.title("MT5 スワップチェッカー")
        self.root.geometry("1450x600")

        self.timer_id = None
        self.current_data = []
        self.last_sort_col = "swap_both_1d_jpy"
        self.last_sort_reverse = True

        if not self._connect_mt5():
            self.root.destroy()
            return

        self._setup_ui()
        self.update_data()
        self.root.protocol("WM_DELETE_WINDOW", self._on_closing)

    def _connect_mt5(self) -> bool:
        if not mt5.initialize(path=self.path):
            messagebox.showerror("MT5接続エラー", f"MT5の初期化に失敗しました: {mt5.last_error()}")
            return False
        self.account_info = mt5.account_info()
        if not self.account_info:
            messagebox.showerror("MT5接続エラー", f"アカウント情報の取得に失敗しました: {mt5.last_error()}")
            mt5.shutdown()
            return False
        if self.account_info.currency != "JPY":
            messagebox.showwarning("口座通貨の確認", f"このツールの円換算機能は、口座通貨がJPYの場合に最適化されています。\n現在の口座通貨: {self.account_info.currency}")
        return True

    def _setup_ui(self):
        main_frame = ttk.Frame(self.root, padding=10)
        main_frame.pack(fill=tk.BOTH, expand=True)
        top_frame = ttk.Frame(main_frame)
        top_frame.pack(fill=tk.X, pady=(0, 5))
        conn_text = f"接続先: {self.account_info.server} ({self.account_info.login})"
        ttk.Label(top_frame, text=conn_text, font=("Meiryo", 10)).pack(side=tk.LEFT)
        self.last_updated_var = tk.StringVar(value="最終更新: ----/--/-- --:--:--")
        ttk.Label(top_frame, textvariable=self.last_updated_var).pack(side=tk.RIGHT)

        sheet_frame = ttk.Frame(main_frame) 
        sheet_frame.pack(fill=tk.BOTH, expand=True)

        self.columns = {
            "symbol":           ("通貨ペア", 110, "w"),
            "swap_long_pts":    ("買いスワップ(Pts/%)", 150, "e"), "swap_short_pts": ("売りスワップ(Pts/%)", 150, "e"),
            "swap_long_jpy":    ("1Lot損益(買/円)", 140, "e"), "swap_short_jpy": ("1Lot損益(売/円)", 140, "e"),
            "swap_both_1d_jpy": ("1日両建て損益(円)", 160, "e"), "rollover_day": ("3倍デー適用曜日", 130, "center"),
            "swap_long_3d":     ("3倍デー(買/円)", 140, "e"), "swap_short_3d":     ("3倍デー(売/円)", 140, "e"),
            "swap_both_3d_jpy": ("3倍デー両建て損益(円)", 180, "e"),
        }

        self.sheet = tksheet.Sheet(sheet_frame,
                                   headers=[h for h, w, a in self.columns.values()],
                                   show_toolbar=False,
                                   show_top_left=False)
        self.sheet.pack(fill=tk.BOTH, expand=True)
        self.sheet.enable_bindings("single_select", "drag_select", "ctrl_select", "arrowkeys", "right_click_popup_menu", "rc_select")

        for i, (col_id, (header, width, anchor)) in enumerate(self.columns.items()):
            align = "w" if anchor == "w" else "e" if anchor == "e" else "center"
            self.sheet.column_width(column=i, width=width)
            self.sheet.align_columns(columns=i, align=align)


        # --- 下部コントロールパネル ---
        bottom_frame = ttk.Frame(main_frame)
        bottom_frame.pack(fill=tk.X, pady=(5, 0))

        # 更新ボタン
        self.update_button = ttk.Button(bottom_frame, text="手動更新", command=self.update_data)
        self.update_button.pack(side=tk.LEFT, padx=(0, 10))

        # <<< 追加: CSV出力ボタン >>>
        self.export_button = ttk.Button(bottom_frame, text="CSV出力", command=self._export_to_csv)
        self.export_button.pack(side=tk.LEFT, padx=(0, 20))

        # 手動ソート機能のUIを追加
        sort_frame = ttk.Frame(bottom_frame)
        sort_frame.pack(side=tk.LEFT)

        ttk.Label(sort_frame, text="並べ替え:").pack(side=tk.LEFT, padx=(0, 5))

        self.col_display_to_id = {v[0]: k for k, v in self.columns.items()}

        self.sort_combo_var = tk.StringVar()
        self.sort_combo = ttk.Combobox(sort_frame, textvariable=self.sort_combo_var,
                                       values=[v[0] for v in self.columns.values()],
                                       state="readonly", width=25)
        self.sort_combo.set(self.columns[self.last_sort_col][0])
        self.sort_combo.pack(side=tk.LEFT, padx=(0, 5))
        self.sort_combo.bind("<<ComboboxSelected>>", self._on_sort_combo_select)

        self.sort_order_button = ttk.Button(sort_frame, text="降順", command=self._on_sort_order_toggle, width=6)
        self.sort_order_button.pack(side=tk.LEFT)

        self.status_var = tk.StringVar(value="準備完了")
        ttk.Label(bottom_frame, textvariable=self.status_var).pack(side=tk.LEFT, padx=20)

    # <<< 追加: CSV出力機能のメソッド >>>
    def _export_to_csv(self):
        # データがなければ処理を中断
        if not self.current_data:
            messagebox.showwarning("CSV出力", "出力するデータがありません。")
            return

        # ファイル保存ダイアログを表示
        try:
            filepath = filedialog.asksaveasfilename(
                defaultextension=".csv",
                filetypes=[("CSVファイル", "*.csv"), ("すべてのファイル", "*.*")],
                title="CSVファイルとして保存",
                # デフォルトのファイル名に現在日時を入れる
                initialfile=f"swap_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
            )
        except tk.TclError:
            print("ファイルダイアログを開けませんでした。")
            return

        # ユーザーがダイアログをキャンセルした場合は何もしない
        if not filepath:
            return

        try:
            # CSVに出力する列のID(キー)のリストを定義
            field_ids = list(self.columns.keys())

            # CSVのヘッダー行(日本語)を作成
            headers = [self.columns[col_id][0] for col_id in field_ids]

            # ファイルを書き込みモードで開く
            # encoding='utf-8-sig' はExcelで日本語が文字化けするのを防ぐ
            # newline='' は余分な改行が入るのを防ぐ
            with open(filepath, 'w', newline='', encoding='utf-8-sig') as csvfile:
                writer = csv.writer(csvfile)

                # 1行目: ヘッダーを書き込む
                writer.writerow(headers)

                # 2行目以降: データを1行ずつ書き込む
                for row_data in self.current_data:
                    # field_idsの順序で辞書から値を取り出し、リストを作成
                    row_to_write = [row_data.get(key, '') for key in field_ids]
                    writer.writerow(row_to_write)

            messagebox.showinfo("CSV出力完了", f"データをCSVファイルに出力しました。\nパス: {filepath}")

        except Exception as e:
            messagebox.showerror("書き込みエラー", f"CSVファイルの書き込み中にエラーが発生しました:\n{e}")
            traceback.print_exc() # コンソールに詳細なエラー情報を出力

    def _on_sort_combo_select(self, event=None):
        selected_display_name = self.sort_combo_var.get()
        new_sort_col = self.col_display_to_id[selected_display_name]
        self._sort_column(new_sort_col, keep_direction=True)

    def _on_sort_order_toggle(self, event=None):
        self.last_sort_reverse = not self.last_sort_reverse
        self._sort_column(self.last_sort_col, keep_direction=True)

    def _fetch_data_thread(self):
        try:
            all_symbols = mt5.symbols_get()
            if not all_symbols:
                self.root.after(0, self._update_gui_with_error, "エラー: 通貨ペアリストの取得に失敗。")
                return

            visible_symbols = [s for s in all_symbols if s.visible]
            if not visible_symbols:
                self.root.after(0, self._update_gui_with_error, "エラー: MT5の「気配値表示」に通貨ペアを追加してください。")
                return

            all_data = []
            weekdays = ["日", "月", "火", "水", "木", "金", "土"]

            for info in visible_symbols:
                tick = mt5.symbol_info_tick(info.name)
                if not tick or tick.bid == 0 or tick.ask == 0:
                    continue

                swap_long_jpy = 0.0
                swap_short_jpy = 0.0

                if info.swap_mode == mt5.SYMBOL_SWAP_MODE_POINTS:
                    tick_value = info.trade_tick_value
                    swap_long_jpy = info.swap_long * tick_value
                    swap_short_jpy = info.swap_short * tick_value
                elif info.swap_mode in [3, 5]: # %指定、もしくは%指定(オープン価格ベース)
                    swap_long_profit_ccy = 1.0 * info.trade_contract_size * tick.ask * (info.swap_long / 100) / 360
                    swap_short_profit_ccy = 1.0 * info.trade_contract_size * tick.bid * (info.swap_short / 100) / 360
                    if info.currency_profit != self.account_info.currency:
                        conversion_pair = info.currency_profit + self.account_info.currency
                        conversion_tick = mt5.symbol_info_tick(conversion_pair)
                        if conversion_tick and conversion_tick.bid > 0:
                            swap_long_jpy = swap_long_profit_ccy * conversion_tick.bid
                            swap_short_jpy = swap_short_profit_ccy * conversion_tick.bid
                        else:
                            continue
                    else:
                        swap_long_jpy = swap_long_profit_ccy
                        swap_short_jpy = swap_short_profit_ccy
                else:
                    continue

                all_data.append({
                    "symbol": info.name,
                    "swap_long_pts": info.swap_long, "swap_short_pts": info.swap_short,
                    "swap_long_jpy": swap_long_jpy, "swap_short_jpy": swap_short_jpy,
                    "swap_both_1d_jpy": swap_long_jpy + swap_short_jpy,
                    "rollover_day": weekdays[info.swap_rollover3days],
                    "swap_long_3d": swap_long_jpy * 3, "swap_short_3d": swap_short_jpy * 3,
                    "swap_both_3d_jpy": (swap_long_jpy + swap_short_jpy) * 3,
                    "swap_mode": info.swap_mode
                })

            self.root.after(0, self._update_gui, all_data)
        except Exception:
            traceback.print_exc()
            self.root.after(0, self._update_gui_with_error, "エラーが発生。詳細はコンソールを確認。")

    def _format_value(self, value, format_spec, is_jpy=False):
        unit = " 円" if is_jpy else ""
        if value == 0: return f"{value:{format_spec}}{unit}"
        return f"{value:+{format_spec}}{unit}"

    def _format_swap_points(self, value, mode):
        if mode in [3, 5]:
            return f"{value:+.3f} %"
        return f"{value:+.3f}"

    def _update_gui(self, data_list):
        self.current_data = data_list
        self._sort_column(self.last_sort_col, keep_direction=True)
        self.last_updated_var.set(f"最終更新: {datetime.now().strftime('%Y/%m/%d %H:%M:%S')}")
        self.status_var.set(f"データ取得完了。{len(data_list)}件の通貨ペアを表示中。")
        self.update_button.config(state="normal")

    def _redraw_view(self):
        if not self.current_data:
            self.sheet.set_sheet_data(data=[[]])
            return

        formatted_data = []
        for data in self.current_data:
            row = [
                data["symbol"],
                self._format_swap_points(data["swap_long_pts"], data["swap_mode"]),
                self._format_swap_points(data["swap_short_pts"], data["swap_mode"]),
                self._format_value(data["swap_long_jpy"], ",.2f", True),
                self._format_value(data["swap_short_jpy"], ",.2f", True),
                self._format_value(data["swap_both_1d_jpy"], ",.2f", True),
                data["rollover_day"],
                self._format_value(data["swap_long_3d"], ",.2f", True),
                self._format_value(data["swap_short_3d"], ",.2f", True),
                self._format_value(data["swap_both_3d_jpy"], ",.2f", True)
            ]
            formatted_data.append(row)

        self.sheet.set_sheet_data(data=formatted_data, redraw=False)

        color_target_cols = [
            "swap_long_pts", "swap_short_pts", "swap_long_jpy", "swap_short_jpy",
            "swap_both_1d_jpy", "swap_long_3d", "swap_short_3d", "swap_both_3d_jpy"
        ]

        col_id_to_index = {col_id: i for i, col_id in enumerate(self.columns.keys())}

        for r, data_row in enumerate(self.current_data):
            for col_id in color_target_cols:
                c = col_id_to_index[col_id]
                value = data_row[col_id]
                color = ""
                if value > 0:
                    color = "blue"
                elif value < 0:
                    color = "red"

                if color:
                    self.sheet.highlight_cells(row=r, column=c, fg=color)

        self.sheet.redraw()

    def _sort_column(self, col, keep_direction=False):
        if not self.current_data:
            return

        if not keep_direction:
            if self.last_sort_col == col:
                self.last_sort_reverse = not self.last_sort_reverse
            else:
                self.last_sort_reverse = True

        self.last_sort_col = col
        is_numeric = col not in ["symbol", "rollover_day"]

        key_func = (lambda d: d.get(col, 0)) if is_numeric else (lambda d: str(d.get(col, '')))
        self.current_data.sort(key=key_func, reverse=self.last_sort_reverse)

        self.sort_order_button.config(text="降順" if self.last_sort_reverse else "昇順")

        self._redraw_view()

    def _update_gui_with_error(self, message):
        self.sheet.set_sheet_data(data=[[]])
        self.status_var.set(message)
        self.update_button.config(state="normal")

    def update_data(self):
        self.status_var.set("データを取得中...")
        self.update_button.config(state="disabled")
        thread = threading.Thread(target=self._fetch_data_thread, daemon=True)
        thread.start()
        if self.timer_id: self.root.after_cancel(self.timer_id)
        self.timer_id = self.root.after(3600000, self.update_data)

    def _on_closing(self):
        if messagebox.askokcancel("終了確認", "アプリケーションを終了しますか?"):
            if self.timer_id: self.root.after_cancel(self.timer_id)
            mt5.shutdown()
            self.root.destroy()
            print("MT5との接続をシャットダウンしました。")

    def run(self):
        self.root.mainloop()

def main():
    terminal_path = choose_terminal()
    if not terminal_path:
        print("MT5ターミナルが選択されませんでした。プログラムを終了します。")
        sys.exit(0)
    app = SwapViewerApp(terminal_path)
    app.run()

if __name__ == "__main__":
    main()

スワップアービトラージ向けに転用するには?

このコードをスワップアービトラージ(異業者両建て)に活用するには、現在の「単一ブローカーの情報を表示する」というコンセプトから、「複数ブローカーの情報を比較し、最も有利な組み合わせを見つける」というコンセプトに拡張する必要があります。

以下に、そのための具体的な改造方針とステップを解説します。

1. 現状のコードの役割と限界

まず、このコードがスワップアービトラージの文脈で何ができ、何ができないかを理解しましょう。

  • できること:
    • 単一ブローカー内の全通貨ペアのスワップポイントと円換算損益を一覧表示できる。
    • 「1日両建て損益」の列を見れば、そのブローカー内で両建てした場合にスワップがプラスになる(サヤ取りできる)通貨ペアを簡単に見つけられる。
  • できないこと(アービトラージに必要な要素):
    • 複数ブローカーの比較: スワップアービトラージの基本は、A社でプラスの買いスワップ、B社でプラス(またはマイナス幅が小さい)の売りスワップを持つ同じ通貨ペアを両建てすることです。このコードは1つのMT5ターミナルにしか接続できません。
    • スプレッドコストの考慮: アービトラージの利益は「合計スワップ – 合計スプレッドコスト」です。スプレッドを考慮しないと、スワップがプラスでもトータルでマイナスになる可能性があります。現在のコードはスプレッドを表示・計算していません。
    • 最適な組み合わせの抽出: 複数のブローカー情報を得た後、どの組み合わせが最も利益が出るかを自動で計算し、ランキング表示する機能が必要です。

2. スワップアービトラージツールへの改造案

以下のステップでコードを改造していくことで、本格的なスワップアービトラージツールに進化させることができます。

ステップ1: 複数ブローカーからのデータ取得機能の追加

これが最も重要な変更点です。複数のMT5ターミナルから同時に情報を取得するロジックを組み込みます。

方針:
choose_terminal()で1つ選ぶのではなく、実行中の全ターミナル、あるいは選択した複数のターミナルから順番にデータを取得し、ブローカーごとに情報を保持するようにします。

具体的な実装:

  1. main関数と接続部分の変更:
    choose_terminal()を改造し、複数のターミナルパスを返せるようにするか、あるいは_discover_terminals()で見つかった全てのターミナルパスのリストをそのまま使います。
  2. データ取得ループの作成:
    メインロジックで、取得したターミナルパスのリストをループ処理します。 # 概念的なコード def fetch_data_from_all_terminals(terminal_paths: list[str]) -> dict: all_brokers_data = {} for path in terminal_paths: print(f"Connecting to {path}...") if mt5.initialize(path=path): account_info = mt5.account_info() broker_name = f"{account_info.server} ({account_info.login})" # _fetch_data_thread の中身をここに移植・改造する symbol_data_list = fetch_swap_data_for_broker() # 各ブローカーのデータを取得する関数 all_brokers_data[broker_name] = { item['symbol']: item for item in symbol_data_list } mt5.shutdown() else: print(f"Failed to connect to {path}") return all_brokers_data</code></pre></li>

ステップ2: スプレッドと取引コストの計算

スワップ情報と同時にスプレッド情報も取得し、円換算のコストを計算します。

方針:
_fetch_data_thread(またはステップ1で作成する新しい関数)内で、symbol_info_tickからaskbidを取得し、スプレッドを計算します。

具体的な実装:

# データ取得関数内に追加
info = mt5.symbol_info(symbol_name)
tick = mt5.symbol_info_tick(symbol_name)

if not tick or not info:
    continue

spread = tick.ask - tick.bid

# スプレッドを円換算する
# trade_tick_valueは1ポイントあたりの価値(口座通貨建て)
# trade_tick_sizeは1ポイントのサイズ(例: 0.001)
spread_cost_jpy = (spread / info.trade_tick_size) * info.trade_tick_value if info.trade_tick_size > 0 else 0

# 取得データ辞書に追加
data_dict = {
    "symbol": info.name,
    "swap_long_jpy": swap_long_jpy,
    "swap_short_jpy": swap_short_jpy,
    "spread_jpy": spread_cost_jpy, # この行を追加
    # ... 他のデータ
}

ステップ3: データの統合とアービトラージ機会の計算

全ブローカーのデータを集めた後、通貨ペアごとに最も有利な「買いブローカー」と「売りブローカー」の組み合わせを探します。

方針:
通貨ペアをキーにして、全ブローカーのデータを横断的に比較します。

具体的な実装:

# 概念的なコード
def find_arbitrage_opportunities(all_brokers_data: dict):
    opportunities = []

    # 全ての通貨ペアをリストアップ
    all_symbols = set()
    for broker_data in all_brokers_data.values():
        all_symbols.update(broker_data.keys())

    for symbol in all_symbols:
        best_long = {"broker": None, "swap": -float('inf'), "spread_cost": 0}
        best_short = {"broker": None, "swap": -float('inf'), "spread_cost": 0}

        # 最も有利な買いスワップと売りスワップを探す
        for broker_name, broker_data in all_brokers_data.items():
            if symbol in broker_data:
                data = broker_data[symbol]
                # 買いスワップの比較
                if data['swap_long_jpy'] > best_long['swap']:
                    best_long = {"broker": broker_name, "swap": data['swap_long_jpy'], "spread_cost": data['spread_jpy']}
                # 売りスワップの比較
                if data['swap_short_jpy'] > best_short['swap']: # 売りスワップはマイナスなので、大きい方(0に近い方)が有利
                    best_short = {"broker": broker_name, "swap": data['swap_short_jpy'], "spread_cost": data['spread_jpy']}

        # 有利な組み合わせが見つかった場合
        if best_long['broker'] and best_short['broker']:
            total_swap = best_long['swap'] + best_short['swap']
            total_spread_cost = best_long['spread_cost'] + best_short['spread_cost']
            net_profit_1d = total_swap - total_spread_cost

            opportunities.append({
                "symbol": symbol,
                "long_broker": best_long['broker'],
                "long_swap_jpy": best_long['swap'],
                "short_broker": best_short['broker'],
                "short_swap_jpy": best_short['swap'],
                "total_swap_1d_jpy": total_swap,
                "total_spread_cost_jpy": total_spread_cost,
                "net_profit_1d_jpy": net_profit_1d, # 1日あたりの実質利益
                "net_profit_3d_jpy": (total_swap * 3) - total_spread_cost, # 3倍デーの実質利益
            })

    # 実質利益でソートする
    opportunities.sort(key=lambda x: x['net_profit_1d_jpy'], reverse=True)
    return opportunities

ステップ4: GUIの改造

現在のGUIは単一ブローカーの情報表示用です。アービトラージの結果を表示するために、新しい表示形式(列)が必要です。

方針:
tksheetに表示する列をアービトラージ機会のデータ構造に合わせます。

新しい列の候補:

  • 通貨ペア
  • 買いブローカー
  • 買いスワップ(円)
  • 売りブローカー
  • 売りスワップ(円)
  • 合計スワップ(円)
  • 合計スプレッドコスト(円)
  • 1日あたり実質利益(円)
  • 3倍デー実質利益(円)

_setup_uiメソッドと_redraw_viewメソッドを大幅に書き換えて、これらの新しい列とデータを表示するようにします。

まとめと注意点

この改造により、あなたのツールは強力なスワップアービトラージ分析ツールになります。

  1. データ取得: 複数のMT5ターミナルを同時に起動し、それぞれのターミナルからスワップとスプレッドの情報を取得する。
  2. コスト計算: スプレッドを円換算し、取引コストを算出する。
  3. 比較・計算: 通貨ペアごとに、全ブローカーの中で最も有利な「買い」と「売り」の組み合わせを見つけ、合計スワップ、合計コスト、実質利益を計算する。
  4. 結果表示: 計算結果を実質利益の高い順にソートしてGUIに表示する。

【重要】

  • 実行環境: このツールを実行するには、比較したいブローカーのMT5ターミナルを全てPC上で同時に起動しておく必要があります。
  • リスク: スワップポイントは日々変動します。また、スプレッドも常に変動します。計算結果はあくまでその時点でのスナップショットです。実際の取引にはスリッページなどのリスクも伴います。
  • テスト: まずはデモ口座で十分にテストし、ツールの計算が正しいこと、意図通りに動作することを確認してください。

この改造は簡単ではありませんが、非常にやりがいのあるプロジェクトです。ぜひ挑戦してみてください。

両建てポジションを管理するなら、コピートレードツールがおすすめ

元のコードはスワップを表示するツールで、取引する機能はありません。LLMで取引機能を実装することもできますが、複雑な機能を持つコードはエラーが発生しやすいです。スワップ表示と取引は別々にしましょう。

スワップアービトラージをするなら、コピートレードツールを使いましょう。反対売買の機能に対応していれば、マスター口座とフォロワー口座でそれぞれ両建てポジションを保有できるようになります。

おすすめはFX BlueのPersonal Trade Copierで、会員登録すれば無料ダウンロードできます。

Personal Trade Copierの使い方については、こちらの記事で詳しくまとめられています。

コピートレードをするなら、Copygram、MetaCopier、Traders Connectなどのコピートレードプラットフォームも候補に入ります。

ただ反対売買機能があるか確認する必要がありますし、サービスによっては月額料金が発生することもあります。まずは無料のPersonal Trade Copierから使ったほうがいいでしょう。

スワップフリー口座でマイナススワップのコストを節約

スワップアービトラージを行う場合、マイナススワップ側の口座をスワップフリー口座にすることでコストを節約することができます。

代表的なのはXM KIWAMI口座ですね。B-bookブローカーの中で希少な無期限スワップフリーを提供しており、スワップアービトラージで長期的に利益を出し続けることが可能です。

またKIWAMI口座は取引口座の最大レバレッジが1000倍と非常に高く、10ロットから100ロット以上のポジションを保有しても、必要証拠金が低く抑えられロスカットされにくくなります。

スワップフリーを提供しているブローカーとしては、他にもExness、FXGT、HFMなどが挙げられます。ただFXGTとHFMはスワップフリーが数日ほどしか適用されませんし、Exnessもスワップアービトラージが発覚するとスワップフリーを剥奪されるリスクがあります。

これはXMも同じ話で、大きなロットのマイナススワップポジションを長期間保有したままにしていると、スワップフリーを剥奪されるケースがあります。

スワップフリー口座+スワップアービトラージで荒稼ぎできたのは2020年あたりまでの話です。その頃は海外FXバブルが発生しており、ブローカーも客寄せのためにスワップフリーを大盤振る舞いしていました。

ただ2025年現在は海外FXバブルも下火になっており、スワップフリー口座を使ったスワップアービトラージが発覚すると、スワップフリーを剥奪されるリスクが高まっています。

スワップフリー口座があればスワップアービトラージは簡単になりますが、ブローカーによるスワップフリー剥奪リスクは警戒しておいたほうがいいです。

FX初心者にスワップアービトラージをおすすめしない理由

スワップアービトラージは為替リスクを無視して稼げるトレード手法ですが、以下の理由からFX初心者にはおすすめしていません。

  • 多額の資金が必要(300-1,000万円以上)
  • カウンターパーティリスクが高い
  • ブローカーごとのスワップを調べるため、口座をたくさん開設する必要がある
  • 業者側に一方的にレバレッジ制限され、ロスカットされるリスクがある
  • 2つ以上の口座で同時に取引するのは、管理コストが高い
  • スワップフリー口座の場合、スワップフリーの剥奪リスクがある

まずスワップアービトラージはポジションを両建てして1日1.0pipsを稼いでいくスタイルなので、月10万円以上の利益を出すなら10ロット以上のポジション両建てする必要があります。

高ロットポジションを長期間保有すると、必要証拠金に加えて、含み損も大きくなりやすいため、ロスカットされないよう資金をたくさん入れておく必要があります。目安は300万円から1,000万円以上ですが、海外FX初心者は資金10万円前後で始めるケースが多いため、資金面でのハードルは高めです。

またスワップポイントはコロコロ変わるので、毎日ブローカーのスワップポイントをチェックする必要があります。特にレバレッジの高いB-bookブローカーの場合、アービトラージ取引だと判明したタイミングで、スワップを改悪してくることもあるので注意しましょう。

スワップアービトラージでは「プラススワップ>マイナススワップ」の組み合わせを見つける必要があります。ただ海外ブローカーもスワップアービトラージで簡単に利益を出させないよう、プラススワップよりもマイナススワップの方が高めに設定されているケースが多いです。

「プラススワップ>マイナススワップ」の組み合わせを見つけるため、たくさんの海外FXブローカーの口座を開設する必要もあります。事務的なコストも膨大になるでしょう。

既にスワップアービトラージのノウハウがあるFX上級者であればそのまま続けても構わないでしょうが、FX初心者が今から参入するのは割に合わないです。

FX初心者が効率よく稼ぐなら、Stop Grid Traderによる指標ピラミッディングがおすすめ。利益率・資本効率に優れており、資金100万円以下でも実践できます。

役に立った記事はSNSシェア!
  • URLをコピーしました!
  • URLをコピーしました!

海外FXの稼ぎ方

海外FXの効率的な稼ぎ方はこちら。

  1. XM スタンダード口座を開設し、3種ボーナスを獲得する
  2. 海外FXの稼ぎ方を体得する
  3. 取引コストの低いTradeview cTrader口座/ILC口座に乗り換える

海外FX初心者の運用口座には、XM スタンダード口座がおすすめ。口座の最大レバレッジが1000倍で、ゴールドの銘柄レバレッジも1000倍なため、ゴールドのハイリスクトレードが可能となります。

またXMは3種類のボーナスを提供しており、10万円の入金額を17万円ほどに増やせます。ボーナスは損失カバー機能があるので、ハイリスクなトレードに使いましょう。

海外FXの稼ぎ方は、億トレーダー(Xアカウント)のコピートレード、無料bot「Stop Grid Trader」による自動売買などがおすすめ。

裁量トレードがメインなら、運用口座はTradeview cTrader口座がおすすめ。高性能FXプラットフォーム「cTrader」に対応しており、リミット注文・ストップ注文を設置しやすくなります。

botによる自動売買をするなら、運用口座はTradeview ILC口座がおすすめ。スプレッドが非常に狭く、取引手数料も1ロット往復5ドルと最安値クラスなので、取引回数の多いトレードロジックで利益を出しやすくなります。

この記事を書いた人

海外FXの情報を備忘録としてまとめています。
運用は自己責任でお願いします。
Twitterで「海外FXの有益情報bot」も運用してます。

目次