XM ボーナスキャンペーンをチェックする ▶︎

無料・高収益グリッドbot「Stop-Grid Trader(Chisiki Grid)」の公開ソースコード・使い方

  • URLをコピーしました!

Chisikiさんが超優秀なグリッドEAのソースコードを無料公開しました。収益性・リスクリワードが非常に優秀で、しかも口座縛りなしで無料入手できるため、EA初心者の練習台にぴったりです。

ただトレードロジックがやや複雑なので、備忘録として軽くまとめておきます。

この記事では無料EA「Stop-Grid Trader(Chisiki Grid)」の使い方を解説していきます。

目次

Stop-Grid Traderとは?

Stop-Grid TraderはChisikiさんが無料配布しているグリッドトレードのサンプルコードです。

Chisikiさんは1日で数千ドル-数万ドルの利益を出している腕利きのトレーダーです。レイテンシー取引を使わずに大きな利益を出していることから、海外FX上級者の間で参考にしている人も多いです。

Stop-Grid Traderのトレードロジック・パラメーター概要

Stop-Grid Traderのトレードロジックは「ストラドル+ピラミッディング」です。起動するとBuy Stopを10段、Sell Stopを10段それぞれ設置します。トレンド相場で大きな利益を狙えます。

ピラミッディングの利確タイミングは、最後のグリッド段数+1段のところです。グリッド段数が10段なら、11段目に相当するところでピラミッディングのポジションを一括決済します。

またこのbotではロットを2分割して、それぞれ異なるトレードロジックで運用します。

  • ピラミッディングトレード
  • グリッドごとの順張りトレード+ドテントレード

トレンドが弱くてピラミッディングで利益が得られなかったとしても、他のトレードロジックで利益を確保できます。

ゴールドは1日でおよそ4000point上下します。レンジの半分2000pointで利確するには、グリッド間隔は200point、ポジション段数は片側10段にしておくといいでしょう。

このbotのグリッド間隔は「現在のRAWスプレッド*係数(DEF_MULTIPLIER)」で決まります。例えばゴールドのスプレッドが20pointなら、係数は10倍に設定すると、グリッド間隔は200pointになります。

デフォルトの2倍だとグリッド間隔が50-80pointの高頻度設定になり、手数料負けしやすくなります。初めて使う場合は、係数は5-10倍に設定するといいでしょう。

1日あたりの収益の目安は?

以下のツイートでは、botを6時間運用して77ドル稼ぐことに成功しています。(設定ロットは0.02ロット)

Chisikiさんのように1日で数千ドル稼ぎたいなら、運用ロットを大きくするか、グリッド間隔を狭くして高頻度運用するかしましょう。リスクリワードは非常に優秀なので、使いこなせれば爆発的な利益を得られるはずです。

Stop-Grid Traderの公開ソースコード(Pythonスクリプト)

Stop-Grid Traderのソースコードはこちら。右上のボードボタンでソースコードをコピペできます。

"""
Stop-Grid Trader  – version 3.1
------------------------------------------------------------
Mini-GUI bot for MetaTrader 5 that trades a symmetric
stop-grid around the current mid-price.

Key workflow (unchanged trading logic, polished GUI):
  1.  Select a running MT5 terminal.
  2.  Enter parameters, then press **Start**:
        • Symbol name (incl. suffix)
        • Price-digits to round to
        • Base lot (even, ≥ 0.02)
        • Orders per side
        • Grid multiplier  (= spread × factor)
        • Loop count (#restarts after a full close)
  3.  The bot places alternating BUY_STOP / SELL_STOP orders
      at ±multiplier·spread intervals, outermost stops include
      a TP one grid-step farther out.
  4.  Every 0.02-lot fill is partially closed at +1 grid step
      (0.01 lot).  The remainder:
          – SL is moved to break-even (both grid & BE-REV).
          – A 0.02-lot reverse STOP is placed at the BE price.
          – If the reverse position’s 0.01 lot remainder finds
            price already beyond the initial mid-price, it is
            closed instantly; otherwise TP = initial mid-price.
  5.  Once the current mid-price touches the outer TP level,
      all pending orders are removed, every position is closed,
      and (optionally) the grid restarts up to *loop count*.
"""

import tkinter as tk
from tkinter import ttk, messagebox
import threading, time, sys
import MetaTrader5 as mt5
try:
    import psutil
except ImportError:
    psutil = None

# ── default GUI values ──────────────────────────────────────────
DEF_SYMBOL        = "XAUUSD"
DEF_DIGITS        = 2
DEF_LOT           = 0.02
DEF_ORDERS_SIDE   = 10
DEF_MULTIPLIER    = 2.0
DEF_LOOP          = 0              # 0 ⇒ run once
# ── constants (rarely changed) ─────────────────────────────────
DEVIATION         = 100
MAGIC_NUMBER      = 0
GRID_TAG          = "basic grid"
CHECK_INTERVAL    = 1.0
# ───────────────────────────────────────────────────────────────


# ═════════════════════════ GUI HELPERS ═════════════════════════
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 = p.info.get("exe") or ""
                if exe and exe not in paths:
                    paths.append(exe)
    return paths


def choose_terminal() -> str | None:
    """Modal: pick an MT5 terminal."""
    root = tk.Tk(); root.withdraw()
    win = tk.Toplevel(root); win.title("Select MT5 terminal"); win.grab_set()

    cols = ("exe", "login", "server", "balance", "currency", "name")
    tree = ttk.Treeview(win, columns=cols, show="headings", height=8)
    for c, w in zip(cols, (340, 80, 170, 100, 70, 150)):
        tree.heading(c, text=c); tree.column(c, width=w, anchor="w")

    for exe in _discover_terminals():
        mt5.initialize(path=exe)
        acc, _ = mt5.account_info(), mt5.terminal_info()
        if acc:
            tree.insert(
                "", tk.END,
                values=(
                    exe, acc.login, acc.server,
                    f"{acc.balance:.2f}", acc.currency, acc.name
                )
            )
        mt5.shutdown()

    tree.grid(row=0, column=0, columnspan=2, padx=6, pady=6)

    sel: dict[str, str | None] = {"path": None}

    def _use() -> None:
        if tree.selection():
            sel["path"] = tree.item(tree.selection()[0], "values")[0]
            win.destroy()

    ttk.Button(win, text="Use", command=_use)\
        .grid(row=1, column=1, pady=(0, 6), padx=6, sticky="e")

    if tree.get_children():
        tree.selection_set(tree.get_children()[0])

    win.wait_window(); root.destroy()
    return sel["path"]


class ParamDialog(tk.Toplevel):
    """Gather user parameters; returns None if canceled."""

    def __init__(self, parent: tk.Tk):
        super().__init__(parent)
        self.title("Grid parameters"); self.grab_set()
        self.res: tuple | None = None

        rows = (
            ("Symbol",          DEF_SYMBOL),
            ("Price digits",    str(DEF_DIGITS)),
            ("Base lot",        f"{DEF_LOT:.2f}"),
            ("Orders / side",   str(DEF_ORDERS_SIDE)),
            ("Grid multiplier", str(DEF_MULTIPLIER)),
            ("Loop count",      str(DEF_LOOP)),
        )
        self.vars: list[tk.StringVar] = []
        for r, (label, default) in enumerate(rows):
            ttk.Label(self, text=label).grid(row=r, column=0, sticky="w", padx=6, pady=4)
            var = tk.StringVar(value=default); self.vars.append(var)
            ttk.Entry(self, textvariable=var, width=15)\
                .grid(row=r, column=1, sticky="w", padx=6, pady=4)

        ttk.Button(self, text="Start", command=self._ok)\
            .grid(row=len(rows), column=1, pady=8, sticky="e")

    def _ok(self) -> None:
        try:
            sym   = self.vars[0].get().strip()
            digs  = int(self.vars[1].get())
            lot   = float(self.vars[2].get())
            nside = int(self.vars[3].get())
            mult  = float(self.vars[4].get())
            loops = int(self.vars[5].get())

            if digs < 0:               raise ValueError("digits ≥ 0")
            if lot < 0.02 or int(round(lot * 100)) % 2:
                raise ValueError("lot must be even ×0.01 and ≥0.02")
            if nside < 1:              raise ValueError("orders / side ≥ 1")
            if mult <= 0:              raise ValueError("multiplier > 0")
            if loops < 0:              raise ValueError("loop count ≥ 0")
        except Exception as err:
            messagebox.showerror("Invalid input", str(err), parent=self)
            return

        self.res = (sym, digs, lot, nside, mult, loops)
        self.destroy()


# ═════════════════════════ TRADER CLASS ════════════════════════
class StopGridTrader:

    def __init__(
        self,
        terminal_path: str,
        symbol: str,
        digits: int,
        base_lot: float,
        orders_side: int,
        multiplier: float,
        loop_count: int
    ):
        self.path   = terminal_path
        self.symbol = symbol
        self.digits = digits
        self.lot    = base_lot
        self.side   = orders_side
        self.mult   = multiplier
        self.loopN  = loop_count
        self.done   = 0  # loops completed

        # runtime
        self.mid: float | None = None
        self.step_pts: int | None = None
        self.tp_high = self.tp_low = None
        self.running = False

        # status window
        self.root = tk.Tk(); self.root.title("Stop-Grid Trader")
        self.status = tk.StringVar(value="Initializing…")
        ttk.Label(self.root, textvariable=self.status)\
            .grid(padx=12, pady=10)
        ttk.Button(self.root, text="Abort", command=self._abort)\
            .grid(pady=(0, 10))

    # ── MetaTrader 5 init ────────────────────────────────────
    def _mt5_init(self) -> None:
        if not mt5.initialize(path=self.path):
            c, m = mt5.last_error(); raise RuntimeError(f"MT5 init: {c} {m}")
        if not mt5.symbol_select(self.symbol, True):
            raise RuntimeError(f"Cannot select symbol {self.symbol}")

    # ── helpers ──────────────────────────────────────────────
    def _norm_vol(self, vol: float) -> float:
        info = mt5.symbol_info(self.symbol); step = info.volume_step or 0.01
        return round(max(info.volume_min, min(vol, info.volume_max)) / step) * step

    # place pending order
    def _pend(
        self, ord_type: int, price: float,
        sl: float, tp: float = 0.0,
        vol: float | None = None,
        tag: str = GRID_TAG
    ) -> None:
        if vol is None: vol = self.lot
        mt5.order_send({
            "action": mt5.TRADE_ACTION_PENDING,
            "symbol": self.symbol,
            "volume": self._norm_vol(vol),
            "type":   ord_type,
            "price":  price,
            "sl":     sl,
            "tp":     tp,
            "deviation": DEVIATION,
            "magic":  MAGIC_NUMBER,
            "comment": tag,
            "type_time": mt5.ORDER_TIME_GTC,
        })

    # ── grid creation ────────────────────────────────────────
    def _build_grid(self) -> None:
        tick = mt5.symbol_info_tick(self.symbol); info = mt5.symbol_info(self.symbol)
        self.mid      = round((tick.bid + tick.ask) / 2, self.digits)
        raw_spd_pts   = int(round((tick.ask - tick.bid) / info.point))
        self.step_pts = int(raw_spd_pts * self.mult)
        pt            = info.point

        self.tp_high = self.tp_low = None
        for i in range(1, self.side + 1):
            buy  = self.mid + i * self.step_pts * pt
            sell = self.mid - i * self.step_pts * pt
            if i == self.side:                       # outer layer
                self.tp_high = buy  + self.step_pts * pt
                self.tp_low  = sell - self.step_pts * pt
                tp_b, tp_s   = self.tp_high, self.tp_low
            else:
                tp_b = tp_s = 0.0
            self._pend(mt5.ORDER_TYPE_BUY_STOP , buy , self.mid, tp=tp_b)
            self._pend(mt5.ORDER_TYPE_SELL_STOP, sell, self.mid, tp=tp_s)

        self.status.set(f"Grid ready   (loop {self.done}/{self.loopN})")

    # place reverse stop at break-even
    def _place_be_rev(self, pos) -> None:
        info = mt5.symbol_info(self.symbol); pt = info.point
        be   = round(pos.price_open, self.digits)
        if pos.type == mt5.POSITION_TYPE_BUY:
            otype, sl = mt5.ORDER_TYPE_SELL_STOP, be + self.step_pts * pt
        else:
            otype, sl = mt5.ORDER_TYPE_BUY_STOP,  be - self.step_pts * pt
        self._pend(otype, be, round(sl, self.digits), vol=self.lot, tag="BE-REV")

    # after partial TP
    def _handle_partial(self, pos) -> None:
        tick = mt5.symbol_info_tick(self.symbol); bid, ask = tick.bid, tick.ask
        half = self.lot / 2
        be_price = round(pos.price_open, self.digits)

        if pos.comment.startswith("BE-REV"):
            beyond = (
                pos.type == mt5.POSITION_TYPE_BUY  and bid >= self.mid or
                pos.type == mt5.POSITION_TYPE_SELL and ask <= self.mid
            )
            if beyond:
                mt5.order_send({
                    "action": mt5.TRADE_ACTION_DEAL, "symbol": self.symbol,
                    "position": pos.ticket, "volume": half,
                    "type": mt5.ORDER_TYPE_SELL if pos.type == mt5.POSITION_TYPE_BUY
                           else mt5.ORDER_TYPE_BUY,
                    "price": bid if pos.type == mt5.POSITION_TYPE_BUY else ask,
                    "deviation": DEVIATION, "magic": MAGIC_NUMBER,
                    "comment": "mid-instant TP"
                })
                return
            mt5.order_send({
                "action":  mt5.TRADE_ACTION_SLTP, "symbol": self.symbol,
                "position": pos.ticket,
                "sl": be_price,                           # BE SL
                "tp": round(self.mid, self.digits),
                "deviation": DEVIATION
            })
        else:
            mt5.order_send({
                "action":  mt5.TRADE_ACTION_SLTP, "symbol": self.symbol,
                "position": pos.ticket,
                "sl": be_price,
                "tp": 0.0,
                "deviation": DEVIATION
            })
            self._place_be_rev(pos)

    # ── monitoring loop ──────────────────────────────────────
    def _monitor(self) -> None:
        info = mt5.symbol_info(self.symbol); pt = info.point
        half = self.lot / 2

        while self.running:
            time.sleep(CHECK_INTERVAL)
            tick = mt5.symbol_info_tick(self.symbol)
            mid_now = (tick.bid + tick.ask) / 2

            # global exit condition
            if (self.tp_high and mid_now >= self.tp_high) or \
               (self.tp_low  and mid_now <= self.tp_low):
                self._full_close()
                continue

            # partial-TP check
            for pos in mt5.positions_get(symbol=self.symbol) or []:
                trg = (
                    pos.price_open + self.step_pts * pt
                    if pos.type == mt5.POSITION_TYPE_BUY
                    else pos.price_open - self.step_pts * pt
                )
                hit = (
                    pos.type == mt5.POSITION_TYPE_BUY  and tick.bid >= trg or
                    pos.type == mt5.POSITION_TYPE_SELL and tick.ask <= trg
                )
                if hit and abs(pos.volume - self.lot) < 1e-6:
                    mt5.order_send({
                        "action": mt5.TRADE_ACTION_DEAL, "symbol": self.symbol,
                        "position": pos.ticket, "volume": half,
                        "type": mt5.ORDER_TYPE_SELL if pos.type == mt5.POSITION_TYPE_BUY
                               else mt5.ORDER_TYPE_BUY,
                        "price": tick.bid if pos.type == mt5.POSITION_TYPE_BUY else tick.ask,
                        "deviation": DEVIATION, "magic": MAGIC_NUMBER,
                        "comment": "partial TP"
                    })
                    time.sleep(0.3)
                    self._handle_partial(pos)

    # ── cancel orders & close positions ──────────────────────
    def _full_close(self) -> None:
        # cancel pending
        for o in mt5.orders_get(symbol=self.symbol) or []:
            if hasattr(mt5, "order_delete"):
                mt5.order_delete(o.ticket)
            else:
                mt5.order_send({
                    "action": mt5.TRADE_ACTION_REMOVE,
                    "order":  o.ticket,
                    "symbol": o.symbol
                })

        # close all positions
        tick = mt5.symbol_info_tick(self.symbol)
        for p in mt5.positions_get(symbol=self.symbol) or []:
            mt5.order_send({
                "action": mt5.TRADE_ACTION_DEAL, "symbol": self.symbol,
                "position": p.ticket, "volume": p.volume,
                "type": mt5.ORDER_TYPE_SELL if p.type == mt5.POSITION_TYPE_BUY
                       else mt5.ORDER_TYPE_BUY,
                "price": tick.bid if p.type == mt5.POSITION_TYPE_BUY else tick.ask,
                "deviation": DEVIATION, "magic": MAGIC_NUMBER,
                "comment": "grid exit"
            })

        self.done += 1
        if self.done <= self.loopN:
            self.status.set(f"Loop {self.done} finished – restarting…")
            time.sleep(2)
            self._build_grid()
        else:
            self.status.set("All loops done – exit")
            self.running = False
            self.root.after(800, self.root.quit)

    # ── GUI: abort button ────────────────────────────────────
    def _abort(self) -> None:
        if messagebox.askyesno("Abort", "Stop trading and exit?", parent=self.root):
            self.running = False
            self._full_close()

    # ── run bot ──────────────────────────────────────────────
    def run(self) -> None:
        self._mt5_init()
        self._build_grid()
        self.running = True
        threading.Thread(target=self._monitor, daemon=True).start()
        self.root.mainloop()
        mt5.shutdown()


# ══════════════════════════ MAIN ══════════════════════════════
def main() -> None:
    term = choose_terminal()
    if not term:
        sys.exit("No MT5 terminal selected – exiting.")

    root = tk.Tk(); root.withdraw()
    pd = ParamDialog(root); pd.wait_window(); root.destroy()
    if pd.res is None:
        sys.exit("Parameters dialog canceled – exiting.")

    sym, digs, lot, nside, mult, loops = pd.res
    StopGridTrader(
        terminal_path = term,
        symbol        = sym,
        digits        = digs,
        base_lot      = lot,
        orders_side   = nside,
        multiplier    = mult,
        loop_count    = loops
    ).run()


if __name__ == "__main__":
    main()

追加のソースコード(監視ループ)

以下のコードは、特定の金融商品(通貨ペアなど)の価格を常に監視し、特定の条件を満たしたときに注文の再構築を行う「監視ループ」の役割を担っています。

グリッドトレード戦略において、ポジションがない状態で待機注文の半数以上が約定した場合、残りの待機注文をすべてキャンセルし、グリッドを再設定するというロジックを実装しています。

例えばポジションが片側10段、両面20段の場合、その半分の10ポジションが既に約定している場合、既存の予約注文をすべてキャンセルして、また新たにストップ注文を設置し直すわけです。

これにより、レンジ相場が続く状況に適応し、取引機会を最適化することを目的としています。

# -- monitoring loop
def _monitor(self) -> None:
    info = mt5.symbol_info(self.symbol); pt = info.point
    half = self.lot / 2

    while self.running:
        time.sleep(CHECK_INTERVAL)
        tick = mt5.symbol_info_tick(self.symbol)
        mid_now = (tick.bid + tick.ask) / 2

        # ===== Measures against range-bound markets =====
        if not mt5.positions_total():
            pendings = [o for o in (mt5.orders_get(
                symbol=self.symbol) or []) if o.comment.startswith(GRID_TAG)]
            executed = self.side * 2 - len(pendings)
            if executed >= self.side // 2 and pendings:
                self.status.set("Half grid consumed - restarting...")
                for o in pendings:
                    if hasattr(mt5, "order_delete"):
                        mt5.order_delete(o.ticket)
                    else:
                        mt5.order_send({
                            "action": mt5.TRADE_ACTION_REMOVE,
                            "order": o.ticket,
                            "symbol": o.symbol
                        })
                time.sleep(1)
                self._build_grid()
                continue
        # =================================================

上記2つを合成したソースコード

上記2つを雑に合成したソースコードです。

デフォルトの銘柄は「XAUUSD」ですが、ビットコイン(BTCUSD)の方がボラティリティが激しくて、利益を出しやすいです。

"""
Stop-Grid Trader  – version 3.1
------------------------------------------------------------
Mini-GUI bot for MetaTrader 5 that trades a symmetric
stop-grid around the current mid-price.

Key workflow (unchanged trading logic, polished GUI):
  1.  Select a running MT5 terminal.
  2.  Enter parameters, then press **Start**:
        • Symbol name (incl. suffix)
        • Price-digits to round to
        • Base lot (even, ≥ 0.02)
        • Orders per side
        • Grid multiplier  (= spread × factor)
        • Loop count (#restarts after a full close)
  3.  The bot places alternating BUY_STOP / SELL_STOP orders
      at ±multiplier·spread intervals, outermost stops include
      a TP one grid-step farther out.
  4.  Every 0.02-lot fill is partially closed at +1 grid step
      (0.01 lot).  The remainder:
          – SL is moved to break-even (both grid & BE-REV).
          – A 0.02-lot reverse STOP is placed at the BE price.
          – If the reverse position’s 0.01 lot remainder finds
            price already beyond the initial mid-price, it is
            closed instantly; otherwise TP = initial mid-price.
  5.  Once the current mid-price touches the outer TP level,
      all pending orders are removed, every position is closed,
      and (optionally) the grid restarts up to *loop count*.
"""

import tkinter as tk
from tkinter import ttk, messagebox
import threading, time, sys
import MetaTrader5 as mt5
try:
    import psutil
except ImportError:
    psutil = None

# ── default GUI values ──────────────────────────────────────────
DEF_SYMBOL        = "XAUUSD"
DEF_DIGITS        = 2
DEF_LOT           = 0.02
DEF_ORDERS_SIDE   = 10
DEF_MULTIPLIER    = 2.0
DEF_LOOP          = 0              # 0 ⇒ run once
# ── constants (rarely changed) ─────────────────────────────────
DEVIATION         = 100
MAGIC_NUMBER      = 0
GRID_TAG          = "basic grid"
CHECK_INTERVAL    = 1.0
# ───────────────────────────────────────────────────────────────


# ═════════════════════════ GUI HELPERS ═════════════════════════
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 = p.info.get("exe") or ""
                if exe and exe not in paths:
                    paths.append(exe)
    return paths


def choose_terminal() -> str | None:
    """Modal: pick an MT5 terminal."""
    root = tk.Tk(); root.withdraw()
    win = tk.Toplevel(root); win.title("Select MT5 terminal"); win.grab_set()

    cols = ("exe", "login", "server", "balance", "currency", "name")
    tree = ttk.Treeview(win, columns=cols, show="headings", height=8)
    for c, w in zip(cols, (340, 80, 170, 100, 70, 150)):
        tree.heading(c, text=c); tree.column(c, width=w, anchor="w")

    for exe in _discover_terminals():
        mt5.initialize(path=exe)
        acc, _ = mt5.account_info(), mt5.terminal_info()
        if acc:
            tree.insert(
                "", tk.END,
                values=(
                    exe, acc.login, acc.server,
                    f"{acc.balance:.2f}", acc.currency, acc.name
                )
            )
        mt5.shutdown()

    tree.grid(row=0, column=0, columnspan=2, padx=6, pady=6)

    sel: dict[str, str | None] = {"path": None}

    def _use() -> None:
        if tree.selection():
            sel["path"] = tree.item(tree.selection()[0], "values")[0]
            win.destroy()

    ttk.Button(win, text="Use", command=_use)\
        .grid(row=1, column=1, pady=(0, 6), padx=6, sticky="e")

    if tree.get_children():
        tree.selection_set(tree.get_children()[0])

    win.wait_window(); root.destroy()
    return sel["path"]


class ParamDialog(tk.Toplevel):
    """Gather user parameters; returns None if canceled."""

    def __init__(self, parent: tk.Tk):
        super().__init__(parent)
        self.title("Grid parameters"); self.grab_set()
        self.res: tuple | None = None

        rows = (
            ("Symbol",          DEF_SYMBOL),
            ("Price digits",    str(DEF_DIGITS)),
            ("Base lot",        f"{DEF_LOT:.2f}"),
            ("Orders / side",   str(DEF_ORDERS_SIDE)),
            ("Grid multiplier", str(DEF_MULTIPLIER)),
            ("Loop count",      str(DEF_LOOP)),
        )
        self.vars: list[tk.StringVar] = []
        for r, (label, default) in enumerate(rows):
            ttk.Label(self, text=label).grid(row=r, column=0, sticky="w", padx=6, pady=4)
            var = tk.StringVar(value=default); self.vars.append(var)
            ttk.Entry(self, textvariable=var, width=15)\
                .grid(row=r, column=1, sticky="w", padx=6, pady=4)

        ttk.Button(self, text="Start", command=self._ok)\
            .grid(row=len(rows), column=1, pady=8, sticky="e")

    def _ok(self) -> None:
        try:
            sym   = self.vars[0].get().strip()
            digs  = int(self.vars[1].get())
            lot   = float(self.vars[2].get())
            nside = int(self.vars[3].get())
            mult  = float(self.vars[4].get())
            loops = int(self.vars[5].get())

            if digs < 0:               raise ValueError("digits ≥ 0")
            if lot < 0.02 or int(round(lot * 100)) % 2:
                raise ValueError("lot must be even ×0.01 and ≥0.02")
            if nside < 1:              raise ValueError("orders / side ≥ 1")
            if mult <= 0:              raise ValueError("multiplier > 0")
            if loops < 0:              raise ValueError("loop count ≥ 0")
        except Exception as err:
            messagebox.showerror("Invalid input", str(err), parent=self)
            return

        self.res = (sym, digs, lot, nside, mult, loops)
        self.destroy()


# ═════════════════════════ TRADER CLASS ════════════════════════
class StopGridTrader:

    def __init__(
        self,
        terminal_path: str,
        symbol: str,
        digits: int,
        base_lot: float,
        orders_side: int,
        multiplier: float,
        loop_count: int
    ):
        self.path   = terminal_path
        self.symbol = symbol
        self.digits = digits
        self.lot    = base_lot
        self.side   = orders_side
        self.mult   = multiplier
        self.loopN  = loop_count
        self.done   = 0  # loops completed

        # runtime
        self.mid: float | None = None
        self.step_pts: int | None = None
        self.tp_high = self.tp_low = None
        self.running = False

        # status window
        self.root = tk.Tk(); self.root.title("Stop-Grid Trader")
        self.status = tk.StringVar(value="Initializing…")
        ttk.Label(self.root, textvariable=self.status)\
            .grid(padx=12, pady=10)
        ttk.Button(self.root, text="Abort", command=self._abort)\
            .grid(pady=(0, 10))

    # ── MetaTrader 5 init ────────────────────────────────────
    def _mt5_init(self) -> None:
        if not mt5.initialize(path=self.path):
            c, m = mt5.last_error(); raise RuntimeError(f"MT5 init: {c} {m}")
        if not mt5.symbol_select(self.symbol, True):
            raise RuntimeError(f"Cannot select symbol {self.symbol}")

    # ── helpers ──────────────────────────────────────────────
    def _norm_vol(self, vol: float) -> float:
        info = mt5.symbol_info(self.symbol); step = info.volume_step or 0.01
        return round(max(info.volume_min, min(vol, info.volume_max)) / step) * step

    # place pending order
    def _pend(
        self, ord_type: int, price: float,
        sl: float, tp: float = 0.0,
        vol: float | None = None,
        tag: str = GRID_TAG
    ) -> None:
        if vol is None: vol = self.lot
        mt5.order_send({
            "action": mt5.TRADE_ACTION_PENDING,
            "symbol": self.symbol,
            "volume": self._norm_vol(vol),
            "type":   ord_type,
            "price":  price,
            "sl":     sl,
            "tp":     tp,
            "deviation": DEVIATION,
            "magic":  MAGIC_NUMBER,
            "comment": tag,
            "type_time": mt5.ORDER_TIME_GTC,
        })

    # ── grid creation ────────────────────────────────────────
    def _build_grid(self) -> None:
        tick = mt5.symbol_info_tick(self.symbol); info = mt5.symbol_info(self.symbol)
        self.mid      = round((tick.bid + tick.ask) / 2, self.digits)
        raw_spd_pts   = int(round((tick.ask - tick.bid) / info.point))
        self.step_pts = int(raw_spd_pts * self.mult)
        pt            = info.point

        self.tp_high = self.tp_low = None
        for i in range(1, self.side + 1):
            buy  = self.mid + i * self.step_pts * pt
            sell = self.mid - i * self.step_pts * pt
            if i == self.side:                       # outer layer
                self.tp_high = buy  + self.step_pts * pt
                self.tp_low  = sell - self.step_pts * pt
                tp_b, tp_s   = self.tp_high, self.tp_low
            else:
                tp_b = tp_s = 0.0
            self._pend(mt5.ORDER_TYPE_BUY_STOP , buy , self.mid, tp=tp_b)
            self._pend(mt5.ORDER_TYPE_SELL_STOP, sell, self.mid, tp=tp_s)

        self.status.set(f"Grid ready   (loop {self.done}/{self.loopN})")

    # place reverse stop at break-even
    def _place_be_rev(self, pos) -> None:
        info = mt5.symbol_info(self.symbol); pt = info.point
        be   = round(pos.price_open, self.digits)
        if pos.type == mt5.POSITION_TYPE_BUY:
            otype, sl = mt5.ORDER_TYPE_SELL_STOP, be + self.step_pts * pt
        else:
            otype, sl = mt5.ORDER_TYPE_BUY_STOP,  be - self.step_pts * pt
        self._pend(otype, be, round(sl, self.digits), vol=self.lot, tag="BE-REV")

    # after partial TP
    def _handle_partial(self, pos) -> None:
        tick = mt5.symbol_info_tick(self.symbol); bid, ask = tick.bid, tick.ask
        half = self.lot / 2
        be_price = round(pos.price_open, self.digits)

        if pos.comment.startswith("BE-REV"):
            beyond = (
                pos.type == mt5.POSITION_TYPE_BUY  and bid >= self.mid or
                pos.type == mt5.POSITION_TYPE_SELL and ask <= self.mid
            )
            if beyond:
                mt5.order_send({
                    "action": mt5.TRADE_ACTION_DEAL, "symbol": self.symbol,
                    "position": pos.ticket, "volume": half,
                    "type": mt5.ORDER_TYPE_SELL if pos.type == mt5.POSITION_TYPE_BUY
                           else mt5.ORDER_TYPE_BUY,
                    "price": bid if pos.type == mt5.POSITION_TYPE_BUY else ask,
                    "deviation": DEVIATION, "magic": MAGIC_NUMBER,
                    "comment": "mid-instant TP"
                })
                return
            mt5.order_send({
                "action":  mt5.TRADE_ACTION_SLTP, "symbol": self.symbol,
                "position": pos.ticket,
                "sl": be_price,                           # BE SL
                "tp": round(self.mid, self.digits),
                "deviation": DEVIATION
            })
        else:
            mt5.order_send({
                "action":  mt5.TRADE_ACTION_SLTP, "symbol": self.symbol,
                "position": pos.ticket,
                "sl": be_price,
                "tp": 0.0,
                "deviation": DEVIATION
            })
            self._place_be_rev(pos)

    # ── monitoring loop ──────────────────────────────────────
    def _monitor(self) -> None:
        info = mt5.symbol_info(self.symbol); pt = info.point
        half = self.lot / 2

        while self.running:
            time.sleep(CHECK_INTERVAL)
            tick = mt5.symbol_info_tick(self.symbol)
            mid_now = (tick.bid + tick.ask) / 2

            # ===== Measures against range-bound markets (CODE ADDED HERE) =====
            if not mt5.positions_total():
                pendings = [o for o in (mt5.orders_get(
                    symbol=self.symbol) or []) if o.comment.startswith(GRID_TAG)]
                executed = self.side * 2 - len(pendings)
                if executed >= self.side // 2 and pendings:
                    self.status.set("Half grid consumed - restarting...")
                    for o in pendings:
                        if hasattr(mt5, "order_delete"):
                            mt5.order_delete(o.ticket)
                        else:
                            mt5.order_send({
                                "action": mt5.TRADE_ACTION_REMOVE,
                                "order": o.ticket,
                                "symbol": o.symbol
                            })
                    time.sleep(1)
                    self._build_grid()
                    continue
            # ====================================================================

            # global exit condition
            if (self.tp_high and mid_now >= self.tp_high) or \
               (self.tp_low  and mid_now <= self.tp_low):
                self._full_close()
                continue

            # partial-TP check
            for pos in mt5.positions_get(symbol=self.symbol) or []:
                trg = (
                    pos.price_open + self.step_pts * pt
                    if pos.type == mt5.POSITION_TYPE_BUY
                    else pos.price_open - self.step_pts * pt
                )
                hit = (
                    pos.type == mt5.POSITION_TYPE_BUY  and tick.bid >= trg or
                    pos.type == mt5.POSITION_TYPE_SELL and tick.ask <= trg
                )
                if hit and abs(pos.volume - self.lot) < 1e-6:
                    mt5.order_send({
                        "action": mt5.TRADE_ACTION_DEAL, "symbol": self.symbol,
                        "position": pos.ticket, "volume": half,
                        "type": mt5.ORDER_TYPE_SELL if pos.type == mt5.POSITION_TYPE_BUY
                               else mt5.ORDER_TYPE_BUY,
                        "price": tick.bid if pos.type == mt5.POSITION_TYPE_BUY else tick.ask,
                        "deviation": DEVIATION, "magic": MAGIC_NUMBER,
                        "comment": "partial TP"
                    })
                    time.sleep(0.3)
                    self._handle_partial(pos)

    # ── cancel orders & close positions ──────────────────────
    def _full_close(self) -> None:
        # cancel pending
        for o in mt5.orders_get(symbol=self.symbol) or []:
            if hasattr(mt5, "order_delete"):
                mt5.order_delete(o.ticket)
            else:
                mt5.order_send({
                    "action": mt5.TRADE_ACTION_REMOVE,
                    "order":  o.ticket,
                    "symbol": o.symbol
                })

        # close all positions
        tick = mt5.symbol_info_tick(self.symbol)
        for p in mt5.positions_get(symbol=self.symbol) or []:
            mt5.order_send({
                "action": mt5.TRADE_ACTION_DEAL, "symbol": self.symbol,
                "position": p.ticket, "volume": p.volume,
                "type": mt5.ORDER_TYPE_SELL if p.type == mt5.POSITION_TYPE_BUY
                       else mt5.ORDER_TYPE_BUY,
                "price": tick.bid if p.type == mt5.POSITION_TYPE_BUY else tick.ask,
                "deviation": DEVIATION, "magic": MAGIC_NUMBER,
                "comment": "grid exit"
            })

        self.done += 1
        if self.done <= self.loopN:
            self.status.set(f"Loop {self.done} finished – restarting…")
            time.sleep(2)
            self._build_grid()
        else:
            self.status.set("All loops done – exit")
            self.running = False
            self.root.after(800, self.root.quit)

    # ── GUI: abort button ────────────────────────────────────
    def _abort(self) -> None:
        if messagebox.askyesno("Abort", "Stop trading and exit?", parent=self.root):
            self.running = False
            self._full_close()

    # ── run bot ──────────────────────────────────────────────
    def run(self) -> None:
        self._mt5_init()
        self._build_grid()
        self.running = True
        threading.Thread(target=self._monitor, daemon=True).start()
        self.root.mainloop()
        mt5.shutdown()


# ══════════════════════════ MAIN ══════════════════════════════
def main() -> None:
    term = choose_terminal()
    if not term:
        sys.exit("No MT5 terminal selected – exiting.")

    root = tk.Tk(); root.withdraw()
    pd = ParamDialog(root); pd.wait_window(); root.destroy()
    if pd.res is None:
        sys.exit("Parameters dialog canceled – exiting.")

    sym, digs, lot, nside, mult, loops = pd.res
    StopGridTrader(
        terminal_path = term,
        symbol        = sym,
        digits        = digs,
        base_lot      = lot,
        orders_side   = nside,
        multiplier    = mult,
        loop_count    = loops
    ).run()


if __name__ == "__main__":
    main()

Stop Grid Traderのトレードロジック

この「Stop-Grid Trader」のトレードロジックを、各ステップに分けてわかりやすく解説します。

このボットは何をするのか? (全体像)

このボットは、現在の価格を中心として、上下に等間隔で「罠」を仕掛けるようなグリッドトレードを行います。価格がどちらに動いても利益を狙えるように設計されています。

一番の特徴は、一度ポジションを持つと、一部を早めに利益確定し、残りはリスクを無くした上で、価格が反転した場合にも利益を狙う「リバース注文」を仕掛けるという、非常に巧妙なロジックを持っている点です。

以下で、具体的な流れを詳しく見ていきましょう。

トレードの基本的な流れ

ステップ1:グリッドの設置 (トレード開始時)

ボットをスタートさせると、まず以下の動作でグリッド(売買注文の網)を構築します。

  1. 中心価格の決定: 現在の買値(Ask)と売値(Bid)の平均価格を「グリッドの中心価格」とします。
  2. 間隔(グリッドステップ)の計算: スプレッド × Grid multiplier (設定値) で、注文を出す価格の間隔を決定します。例えば、スプレッドが10ポイントで、Multiplierが2.0なら、間隔は20ポイントになります。
  3. ストップ注文の配置:
    • 中心価格よりに、設定された本数分だけBUY_STOP(逆指値買い)注文を等間隔で配置します。
    • 中心価格よりに、設定された本数分だけSELL_STOP(逆指値売り)注文を等間隔で配置します。
  4. 利確(TP)と損切り(SL)の設定:
    • すべてのストップ注文の損切り(SL)は、グリッドの「中心価格」に設定されます。
    • 一番外側に置かれたBUY_STOPSELL_STOP注文にだけ、特別な利確(TP)が設定されます。このTPは、さらに1グリッドステップ分外側の価格です。このTPに価格が到達すると、グリッド全体の終了トリガーとなります。

【イメージ図】

      ↑ TP (グリッド全体の終了ポイント)
      ↑ BUY_STOP (一番外側、TPあり)
      ↑ BUY_STOP
      ↑ BUY_STOP
<---- 現在の価格 (グリッド中心) ----> SLはすべてここ
      ↓ SELL_STOP
      ↓ SELL_STOP
      ↓ SELL_STOP (一番外側、TPあり)
      ↓ TP (グリッド全体の終了ポイント)

ステップ2:注文が約定した時の複雑な処理

価格が動き、いずれかのストップ注文が約定(ポジションになる)と、以下の賢い処理が自動的に行われます。ここではBUY_STOPが約定した例で説明します。

  1. 一部利益確定 (Partial Take Profit)
    • ポジションができてから、価格がさらに1グリッドステップ分上昇すると、ポジションの半分(例:0.02ロットなら0.01ロット)を利益確定します。これにより、早めに利益を確保します。
  2. 残りのポジションの処理
    • 損切りを建値に移動 (Break-Even): 残った半分のポジションの損切り(SL)を、元のエントリー価格(建値)に移動します。これで、このポジションは最悪でもプラスマイナスゼロになり、負けることがなくなります
    • リバース注文の設置 (Reverse Order): ここが最も特徴的な部分です。ポジションのエントリー価格と同じ価格に、反対方向のストップ注文(この例ではSELL_STOP)を、元のロット数(0.02ロット)で新たに設置します。
      • 目的: もし価格が反転してエントリー価格まで戻ってきた場合、残りの買いポジションは建値で決済され(損益ゼロ)、同時に新しい売りポジションが作られ、今度は下落方向で利益を狙うことができます。

ステップ3:リバース注文が約定した後の特殊な処理

もし価格が反転し、ステップ2で設置したリバース注文(例:SELL_STOP)が約定した場合、さらに特殊な処理が行われます。

  • ケースA:価格がグリッドの「中心価格」を超えて反対側に行っていた場合
    • リバース注文が約定した時点で、価格がすでにグリッド全体の「中心価格」よりも下落していた場合。
    • これは「反転の勢いが弱い」と判断し、リバースポジションの半分を即座に決済します。深追いはしないという戦略です。
  • ケースB:価格がまだグリッドの「中心価格」に達していない場合
    • リバース注文が約定したが、価格はまだグリッドの「中心価格」まで戻っていない場合。
    • これは「中心価格への回帰が期待できる」と判断し、残ったリバースポジションの利益確定(TP)をグリッドの「中心価格」に設定します。

ステップ4:グリッド全体の終了条件

以下のいずれかの条件が満たされると、1回のトレードサイクルが終了します。

  1. 全体の利確: 価格が、ステップ1で設定した一番外側の注文のTPレベルに到達する。
  2. 手動停止: ユーザーがGUIの「Abort」ボタンを押す。

終了条件が満たされると、ボットはすべての保有ポジションを決済し、すべての待機注文をキャンセルします。

その後、Loop count(ループ回数)が設定されていれば、現在の価格で新しいグリッドを構築し、トレードを再開します。

パラメータの解説

  • Symbol name: トレードする通貨ペアや銘柄名 (例: XAUUSD, USDJPY)。
  • Price-digits: 価格の小数点以下の桁数。
  • Base lot: 基本となるロット数。ロジック上、0.02以上の偶数(0.02, 0.04, 0.06…)である必要があります。
  • Orders per side: 中心価格の片側に設置する注文の数。
  • Grid multiplier: グリッドの間隔を決めるための係数。スプレッド × この値 が間隔になります。
  • Loop count: グリッド全体が終了した後に、自動で再スタートする回数。0なら1回のみ実行。

まとめ

このボットは、単なるグリッドトレードではなく、

  • 早期の一部利確で着実に利益を確保し、
  • 残りを建値決済にすることでリスクを管理し、
  • 価格が反転しても利益を狙えるリバース注文を仕掛ける、

という複数の戦略を組み合わせた、非常に洗練されたロジックで動作します。

Stop Grid Traderの設定方法

このPythonスクリプトで書かれたbotの設定方法と実行手順を、ステップバイステップで解説します。

このbotはMT5のEA(Expert Advisor)ではなく、Pythonスクリプトとして外部からMT5を操作するタイプです。そのため、設定はMT5内ではなく、PC上で行います。

準備するもの (前提条件)

  1. MetaTrader 5 (MT5) ターミナル
  2. Pythonのインストール
    • このスクリプトはPythonで書かれているため、PCにPythonがインストールされている必要があります。
    • もしまだインストールしていなければ、Python公式サイトからダウンロードしてインストールしてください。
  3. 必要なPythonライブラリ
    • スクリプトがMT5と通信したり、PCの情報を取得したりするために、いくつかのライブラリが必要です。コマンドプロンプトやターミナルを開き、以下のコマンドを1行ずつ実行してインストールしてください。
    pip install MetaTrader5 pip install psutil
    • MetaTrader5はMT5と連携するための公式ライブラリです。
    • psutilは実行中のMT5ターミナルを自動で検出するために使われます。

設定と実行の手順

ステップ1:スクリプトファイルを保存する

  1. 提供されたPythonコード全体をコピーします。
  2. メモ帳やVSCodeなどのテキストエディタを開きます。
  3. コピーしたコードを貼り付けます。
  4. ファイルに名前を付けて保存します。名前は自由ですが、必ず拡張子を .py にしてください。(例: stop_grid_trader.py)

ステップ2:MetaTrader 5の設定

  1. PCでMT5ターミナルを起動し、取引したい口座にログインしておきます。(XM MT5AXIORY MT5Tradeview MT5
  2. MT5の上部メニューから ツールオプション を選択します。
  3. 開いたウィンドウで エキスパートアドバイザ タブを選択します。
  4. 「自動売買を許可する」 にチェックを入れてください。これは外部からのプログラムがMT5を操作するために必須の設定です。
  5. OK をクリックしてオプションウィンドウを閉じます。
  6. MT5のツールバーにある「自動売買」ボタンが緑色になっていることを確認します。(赤色の場合はクリックして緑色にします)

ステップ3:スクリプトを実行する

  1. コマンドプロンプト(またはPowerShell, ターミナル)を起動します。
  2. cd コマンドを使って、ステップ1で保存したPythonファイルがあるフォルダに移動します。
  3. 以下のコマンドを入力して、スクリプトを実行します。 python stop_grid_trader.py (ファイル名を変更した場合は、そのファイル名を入力してください)

ステップ4:MT5ターミナルを選択する

スクリプトを実行すると、最初に「Select MT5 terminal」というウィンドウが表示されます。

  • 現在PCで実行されているMT5ターミナルが一覧で表示されます。
  • 取引に使用したいMT5ターミナルを選択し、「Use」ボタンをクリックします。

ステップ5:パラメータを入力する

次に「Grid parameters」というウィンドウが表示されます。ここでトレードの具体的な設定を行います。

  • Symbol:
    • 取引したい通貨ペアや銘柄名を入力します。ブローカーのシンボル名と完全に一致させる必要があります。(例: XAUUSD, USDJPY.m など、お使いのMT5の気配値表示に表示されている通りに入力してください)
  • Price digits:
    • 価格の小数点以下の桁数です。ドル円なら3、ゴールドなら2、ユーロドルなら5が一般的です。
  • Base lot:
    • 1回の注文の基本ロット数です。このロジックでは必ず0.02以上の偶数(0.02, 0.04, 0.06…)に設定してください。半分に分割する処理があるためです。
  • Orders / side:
    • 価格の中心から片側(上または下)に何本の注文を出すかを設定します。10なら、上下に10本ずつ、合計20本のストップ注文が設置されます。
  • Grid multiplier:
    • グリッドの間隔を決める係数です。スプレッド × この値 が注文間の価格差になります。
      • 値を小さくする → グリッドが密になり、頻繁に約定する(レンジ相場向け)
      • 値を大きくする → グリッドが広くなり、約定しにくくなる(トレンド相場向け、リスク低減)
  • Loop count:
    • グリッド全体が終了した後に、何回自動で再スタートするかを設定します。0にすると1回実行したら終了します。5にすると、5回グリッドを繰り返します。

すべてのパラメータを入力したら、「Start」ボタンをクリックします。

ステップ6:取引開始とモニタリング

「Start」をクリックすると、パラメータ入力ウィンドウが消え、「Stop-Grid Trader」という小さなウィンドウが表示されます。

  • このウィンドウには現在のステータス(例: “Grid ready”)が表示されます。
  • この状態になったら、MT5のターミナルを確認してください。設定した通りにBUY_STOPSELL_STOP注文が発注されているはずです。
  • あとはbotが自動でロジックに従って取引を管理します。

ステップ7:取引を停止する方法

取引を途中で完全に停止したい場合は、「Stop-Grid Trader」ウィンドウの「Abort」ボタンをクリックします。

  • 確認ダイアログが表示されるので「はい」を選択すると、botはすべての待機注文をキャンセルし、すべての保有ポジションを決済して、安全にプログラムを終了します。
  • (注意)コマンドプロンプトやステータスウィンドウを直接閉じると、ポジションや注文が残ったままになる可能性があるので、必ず「Abort」ボタンを使ってください。

非常に重要な注意点

  • 必ずデモ口座で試す: このような自動売買botは、必ず最初にデモ口座で十分なテストを行い、そのロジックとリスクを完全に理解してから、自己責任でリアル口座に使用してください。
  • PCの電源: このbotはPC上で実行されている間だけ動作します。PCがスリープしたり、シャットダウンしたりするとbotも停止します。24時間稼働させたい場合は、VPS(仮想専用サーバー)の利用を検討してください。

Stop Grid Traderのおすすめ運用方法

この「Stop-Grid Trader」を最大限に活かすための、より具体的で実践的なおすすめ運用方法を提案します。

このbotは「放置して資産を増やす」タイプのEAではなく、「特定の相場状況を見極めて、短時間だけ稼働させる」ことで真価を発揮するツールです。猟師が獲物の通り道に罠を仕掛けるようなイメージで使いましょう。

おすすめ運用戦略:「重要指標発表」を狙ったスキャルピング

最もおすすめなのが、経済指標発表時のようなボラティリティが急上昇するが、方向感が定まらない(上下に荒れやすい)相場を狙う方法です。

なぜ指標発表時が有効なのか?

  • 高いボラティリティ: 発表直後は価格が乱高下しやすく、グリッド注文が次々と約定します。
  • 往復運動: しばしば価格は急騰した後に急落する(またはその逆)「往って来い」の動きを見せます。これは、このbotの「一部利確」と「リバース注文」のロジックが最も輝く瞬間です。
  • 短時間で決着: 指標トレードは数分~数十分で終わることが多く、長時間PCに張り付く必要がありません。

具体的な手順とパラメータ設定例

対象とする指標の例:

  • 米国の雇用統計 (NFP)
  • 消費者物価指数 (CPI)
  • 連邦公開市場委員会 (FOMC) の政策金利発表

トレードする銘柄:

  • XAUUSD (ゴールド): 指標発表時に最もボラティリティが高くなりやすく、この戦略に最適です。
  • USDJPY (ドル円): 日本円が絡む指標の場合。

【ステップ・バイ・ステップ運用法】

  1. 準備 (発表10分前):
    • 経済指標カレンダーで、注目度の高い指標の時間を確認します。
    • MT5を起動し、チャートを5分足または1分足で表示しておきます。
  2. 設定 (発表1~2分前):
    • botを起動し、パラメータを入力します。このタイミングが非常に重要です。
    • 直前の価格でグリッドを設置するため、発表ギリギリに起動します。
    ▼ パラメータ設定例 (XAUUSDの場合) ▼
    • Symbol: XAUUSD
    • Price digits: 2
    • Base lot: 0.02 (※最初は必ず最小ロットで。リスク許容度に応じて調整)
    • Orders / side: 15 (上下に15本ずつ。乱高下の範囲を広めにカバー)
    • Grid multiplier: 2.5
      • 解説: ゴールドのスプレッドが平常時20pips程度だと仮定すると、20 * 2.5 = 50pips ($0.5) の間隔になります。指標発表時はスプレッドが広がるため、これくらいが適切です。狭すぎるとスプレッド拡大で即座に不利なポジションを持ってしまう可能性があります。
    • Loop count: 0
      • 解説: 指標発表という「一時的なお祭り」だけを狙うため、ループはさせません。1回のグリッドで終了させます。
  3. 実行 (発表直前):
    • 「Start」ボタンを押して、グリッドを設置します。
    • MT5のチャートに、現在の価格を中心にストップ注文がずらっと並んだことを確認します。
  4. 監視 (発表後):
    • 指標が発表されると、価格が激しく動き、注文が次々と約定し、決済されていきます。
    • botが自動で処理するので、基本的には見ているだけでOKです。
    • 価格が一番外側のTP(利確ライン)に達するか、相場の動きが落ち着いたら、botは自動ですべてのポジションと注文をクローズします。
  5. 終了:
    • もし動きが収まったのにポジションが残っている場合は、手動で「Abort」ボタンを押して終了させても良いでしょう。

経済指標カレンダーはみんかぶがおすすめ。フィルターを「主要国+重要度4以上」だけにすることで、重要度の高いものだけを絞ることができます。

もう一つの運用戦略:「レンジ相場」でのデイトレード

東京時間の午前中や、大きな指標がない日のNY市場の深夜など、一定の値幅を行ったり来たりしているレンジ相場で稼働させる方法です。

▼ パラメータ設定例 (USDJPY レンジ相場) ▼

  • Symbol: USDJPY
  • Price digits: 3
  • Base lot: 0.02
  • Orders / side: 8 (レンジ幅が狭いので、本数は少なめでOK)
  • Grid multiplier: 1.8
    • 解説: レンジ相場ではボラティリティが低いため、multiplierを小さくしてグリッドを密にし、小さな値動きでも約定するように狙います。スプレッドが狭い通貨ペア(ドル円など)が向いています。
  • Loop count: 0 または 1
    • 解説: 「このレンジ相場が続くであろう数時間だけ」と決めて稼働させます。

非常に重要な注意点

  • リスク管理の徹底: この手法はハイリスク・ハイリターンです。必ず余剰資金の範囲内で行い、失っても問題ない金額で運用してください。
  • スプレッド拡大とスリッページ: 指標発表時はスプレッドが通常時の数倍に広がり、注文した価格と実際に約定する価格がずれる「スリッページ」が発生しやすくなります。これらはコストとなり、利益を圧迫する要因です。Grid multiplierを極端に小さくしてはいけないのはこのためです。
  • ブローカーの選定: スプレッドが狭く、約定力が高い(スリッページが少ない)ブローカーを選ぶことが、この戦略の成功率に直結します。
  • デモ口座での練習は必須: いきなりリアル口座で試すのは絶対にやめてください。最低でも数回、異なる指標でデモ口座で練習し、botの動き、スプレッドの広がり方、想定される損益を体感してください。

このbotは、相場の特性を理解した上で「使いどころ」を絞れば、非常に強力な武器になり得ます。ぜひデモ口座で試してみてください。

おすすめパラメーター

Stop Grid Traderのおすすめパラメーターはこちら。実際にbotで利益を出しているCishikiさんのパラメーターを参考にしています。(ソース1ソース2

DEF_SYMBOL(運用銘柄)BTCUSD
DEF_LOT(ロット)0.02
DEF_ORDERS_SIDE(片側のグリッド段数)10
DEF_MULTIPLIER(グリッド倍率)2.0(動的倍率も導入している?)
DEF_LOOP(ループ回数)0-100(お好みで)

運用銘柄は「BTCUSD(ビットコイン)」がおすすめ。ボラティリティが非常に激しく、高頻度運用でも利益を出しやすいです。ただしビットコインは対応しているブローカーが少なく、レバレッジも10倍と低めなので、0.02ロットの低ロット運用が前提となります。

運用ロットは、高頻度運用なら「0.02ロット」にします。ビットコインはハイレバレッジ取引ができないため(10倍)、ロットを増やすよりも、取引回数を増やしたほうが資本効率が良くなります。

グリッド段数は「10」、グリット倍率は「2.0」と低めに設定します。プログラミングの知識があるなら、動的グリット倍率を導入してもいいでしょう。

レンジ相場ではグリッド幅を狭めにすることで、小型レンジ相場の両端で利確できるようになります。一方トレンド相場ではグリッド幅を広めにすることで、大きなトレンドで利幅を伸ばしやすくなります。

収益性を高めるためのソースコード改善案

Stop Grid Traderの基本ロジックはピラミッディングトレードです。ボラティリティが激しいトレンド相場では大きな利益を出せますが、レンジ相場では手数料負けしやすくなります。なるべくボラティリティが高いタイミングで運用しましょう。

ソースコードの改善案としては、以下のものが挙げられます。

  • APIで出来高引用する(ソース
  • ボラティリティの高いタイミング(経済指標など)でピンポイント稼働する(ソース
  • 短期分析で倍率を動的に調整する(ソース
  • 機械学習を入れつつ、鮮度の高いデータだけを利用する(ソース
  • 稼働ウォレット率と資金量の推移から日上限の出来高探ってトラップ注文の到達率を上げる(ソース

プログラミングが苦手なら、LLMに指示を出してバイブコーティングで修正するといいでしょう。特にGoogle Geminiなら無料でお手軽にソースコードの修正ができます。

運用口座の選び方・おすすめは?

このbotの性能は、どのタイプの取引口座で運用するかによって天と地ほどの差が出ます。間違った口座を選ぶと、ロジックが正しくても利益が出ない、あるいは損失が拡大する可能性すらあります。

結論から申し上げますと、このbotを運用するのに最適なのは「ECN口座」または「Raw/Zeroスプレッド口座」です。

逆に、「スタンダード口座」は最も不向きです。

その理由と、口座選びの具体的なチェックポイントを詳しく解説します。

なぜ「ECN口座」や「Rawスプレッド口座」が最適なのか?

このbotの取引スタイルは、超短期売買(スキャルピング)です。スキャルピングの成否は、取引コストと約定スピードに直結します。ECN/Raw口座は、まさにそのために設計されています。

1. 取引コスト(スプレッド)が極めて低い

  • botのロジックとの関係: このbotは「スプレッド × multiplier」という非常に狭い値幅で一部利益確定を行います。スタンダード口座のようにスプレッドが広いと、この利益確定ターゲットが遠くなりすぎてしまい、なかなか到達しません。最悪の場合、スプレッドの広さだけで負けてしまいます。
  • ECN/Raw口座の利点: これらの口座は、スプレッドがほぼゼロに近い(0.0~0.3pipsなど)代わりに、取引ごとに固定の手数料(Commission)が発生します。変動するスプレッドよりも、固定の手数料の方が取引コストを正確に計算でき、スキャルピング戦略において圧倒的に有利です。

2. 約定スピードが速く、透明性が高い

  • botのロジックとの関係: 指標発表時などの激しい値動きの中では、注文した価格と実際に約定する価格がずれる「スリッページ」が発生しやすくなります。このbotはSTOP注文(逆指値)を多用するため、スリッページは致命的です。
  • ECN/Raw口座の利点: ECN方式では、ブローカーが取引に介入せず、投資家の注文を直接インターバンク市場に流します。そのため、約定拒否が起こりにくく、高速で透明性の高い取引が可能です。スリッページが発生する可能性はゼロではありませんが、そのリスクを最小限に抑えられます。

3. スキャルピング制限がない

  • botのロジックとの関係: このbotは短時間に多数の注文と決済を繰り返します。
  • ECN/Raw口座の利点: スタンダード口座を提供するブローカーの中には、サーバーに負荷がかかるという理由で、過度なスキャルピングを禁止あるいは制限している場合があります。ECN方式のブローカーは、取引量が増えるほど手数料で儲かるビジネスモデルなので、スキャルピングを歓迎する傾向にあります。

口座タイプの比較

特徴スタンダード口座 (不向き)ECN / Rawスプレッド口座 (最適)このbotにとってなぜ重要か?
スプレッド広い (例: 1.0~2.0 pips)極めて狭い (例: 0.0~0.3 pips)最重要。 利益確定の幅がスプレッドに依存するため、狭いほど有利。
手数料無料 (スプレッドに含まれる)有り (例: 1ロットあたり往復$7)固定コストなので計算しやすく、トータルコストはECNが安くなることが多い。
約定方式STP方式 or DD方式ECN方式高速・高透明性。スリッページのリスクを最小化できる。
スキャルピング制限がある場合も制限なし (歓迎される)botの取引スタイルと完全に合致している。

口座選びの具体的なチェックリスト

ブローカーを選ぶ際に、以下の6つのポイントを必ず確認してください。

  1. ✅ 口座タイプ: ECN, Raw, Zero, cTrader といった名前の口座を提供しているか?
  2. ✅ 平均スプレッド: 公式サイトに掲載されている「平均スプレッド」を確認します。「最低スプレッド0.0 pips〜」という宣伝文句だけでなく、実際の平均値が重要です。
  3. ✅ 取引手数料: 1ロット往復あたりの手数料はいくらか? (一般的に$5〜$8程度)
  4. ✅ ストップレベル: ゼロか、極めて小さいか?
    • ※ストップレベルとは、現在価格から最低限離さなければならないpips数のことで、これが大きいとグリッドを密に設置できません。多くのECN口座ではゼロに設定されています。
  5. ✅ ゼロカットシステムの有無: 万が一の急変動で口座残高がマイナスになった場合、追証が発生しないシステムがあるか? (日本のブローカー以外を利用する場合、特に重要)
  6. ✅ サーバーの場所: 可能であれば、利用するVPSと同じ国や地域にデータセンターがあるブローカーを選ぶと、約定スピードがさらに向上します。

結論と推奨アクション

  1. 今すぐ、ECN/Rawスプレッド口座を開設してください。 もし現在スタンダード口座しか持っていない場合、このbotの真価を発揮させることはできません。
  2. 必ずそのECN口座の「デモ口座」でテストしてください。
    • リアル口座と同じ手数料やスプレッド環境でテストすることで、本番でのパフォーマンスを正確に予測できます。
    • 指標発表時にスプレッドがどの程度広がるのかを、デモ口座で事前に確認しておくことが非常に重要です。

このbotは「優れたエンジン」です。その性能を最大限に引き出すためには、「F1マシン用のサーキット(=ECN口座)」が必要なのです。一般的な公道(=スタンダード口座)では、そのスピードを出すことはできません。

おすすめはTradeview ILC口座

運用口座はTradeview ILC口座がおすすめ。

スプレッド・取引手数料が非常に優秀で、高頻度運用でも利益を出しやすいです。またTradeviewはA-bookブローカーでもあるので、経済指標前後でスプレッドが恣意的に広げられる可能性も低いです。

さらにTradeviewはBTCUSDの取引も可能です。ただし銘柄レバレッジは10倍と低めなので、資金10,000ドルあたりで0.02-0.04ロットの低ロット運用をすることが前提となります。

もっと取引コストを抑えたいなら、他の海外FXブローカーを探しても良いでしょう。特に海外FX調査兵団さんが高く評価しているブローカーは、取引コストが低めで、約定力にも優れている傾向があります。

ゴールドで運用する場合、スプレッドの目安は?

ゴールドのスプレッドは、「平常時」「高ボラティリティ時(指標発表など)」で大きく異なるため、分けて考える必要があります。

1. 平常時のスプレッド目安

ここで言う「平常時」とは、東京時間午後からロンドン時間、ニューヨーク時間にかけての、大きな指標発表がない時間帯を指します。

  • 理想的なスプレッド(超優良): 1.0 〜 2.5 pips (10〜25 ポイント)
    • このレベルのスプレッドを提供しているECN/Raw口座は、ゴールドのスキャルピングに非常に適しています。
    • 取引手数料(往復$7程度)を考慮しても、トータルコストを低く抑えることができます。
  • 許容範囲のスプレッド(標準的): 2.5 〜 4.0 pips (25〜40 ポイント)
    • 多くの海外FXブローカーのECN口座は、この範囲に収まります。
    • このbotの運用は十分可能ですが、Grid multiplierを少し大きめ(例: 3.0以上)に設定するなど、スプレッドコストを吸収するための調整が必要になるかもしれません。
  • 注意が必要なスプレッド(やや不利): 4.0 pips以上 (40 ポイント以上)
    • このレベルになると、スプレッドコストがかなり重くなります。
    • botの一部利確ターゲット(1グリッドステップ)が遠くなり、勝率が低下する可能性があります。このスプレッド環境しかない場合は、デイトレード以上の時間軸での取引を検討した方が良いかもしれません。

※pipsとポイントの換算: ゴールドの場合、価格が$1.00動くと100 pips(または1000 points)と数えるのが一般的です。$0.10の動きが10 pips100 points)に相当します。ブローカーによって呼び方が違うので、MT5の気配値表示でAskとBidの差を見て、実際の値幅で確認するのが確実です。

2. 高ボラティリティ時のスプレッド目安(最重要)

米国の重要経済指標(雇用統計、CPI、FOMCなど)の発表前後では、スプレッドは劇的に拡大します。この時のスプレッドを把握しておくことが、指標トレードの成功の鍵です。

  • 非常に優秀なブローカー: 10 〜 20 pips (100〜200 ポイント)
    • 指標発表のピーク時でも、スプレッドの拡大をこの程度に抑えられるブローカーは、サーバーが非常に強く、流動性供給も安定しています。指標スキャルピングを行う上で、最も信頼できるパートナーとなります。
  • 一般的なブローカー: 20 〜 50 pips (200〜500 ポイント)
    • 多くのブローカーでは、発表の瞬間から数分間、このレベルまでスプレッドが広がります。
    • この拡大を見越してGrid multiplierを設定する必要があります。例えば、平常時スプレッド2.0 pips、multiplier 2.5で運用していた場合、グリッド幅は5 pipsです。しかし、指標時にスプレッドが20 pipsに広がると、最初の注文は約定した瞬間に20 pipsの含み損を抱えることになり、戦略が破綻しかねません。
    • そのため、指標トレードではmultiplierを大きめに設定することがセオリーとなります。
  • 避けるべきブローカー: 50 pips以上、または一時的に取引停止になる
    • 指標時にスプレッドが異常に拡大したり、レート配信が止まって取引できなくなったりするブローカーは、指標トレードには絶対に向いていません。

【実践的アドバイス】自分の利用するブローカーのスプレッドを確認する方法

  1. デモ口座で指標を体験する:
    • これが最も確実な方法です。次の重要指標発表時に、必ずデモ口座でMT5を起動しておき、気配値表示ウィンドウのスプレッドがどれだけ広がるかを自分の目で直接確認します。
    • 可能であれば、画面録画ソフトでその瞬間を録画しておくと、後で冷静に分析できます。
  2. スプレッド表示インジケーターを利用する:
    • MT5には、チャート上に現在のスプレッドや最大/最小スプレッドを常時表示してくれるインジケーターが無料で多数公開されています。
    • これをチャートに入れておけば、平常時のスプレッドの変動パターンや、特定の時間帯のスプレッドの傾向を把握しやすくなります。

結論

  • 平常時: 目標は2.5 pips以下。これより広い場合は、より条件の良いブローカーを探す価値があります。
  • 指標時: 事前にデモ口座で最大スプレッドを確認し、それに耐えうるGrid multiplierを設定する戦略が必須です。優秀なブローカーなら20 pips以内に収まるはずです。

良いブローカー選びは、このbotの成功確率を50%以上引き上げる要素だと考えてください。

Stop-Grid Traderはバックテストできる?

Stop-Grid TraderのPythonスクリプトは、GUI (tkinter) を持ち、リアルタイムでMT5ターミナルと連携して動作するライブトレーディング(またはフォワードテスト)用のボットです。

そのため、このスクリプトをそのままバックテストすることはできません

バックテストを行うには、スクリプトを大幅にリファクタリング(再構成)し、取引ロジックと**実行環境(GUIやMT5との通信)**を分離する必要があります。

バックテスト化の課題 (なぜそのままでは動かないか)

  1. GUI (tkinter): バックテストは過去のデータに対して自動で高速に実行されるべきです。パラメータ入力を求めるGUIや、実行中のステータスを表示するウィンドウは不要であり、処理を妨げます。
  2. リアルタイムのMT5関数: mt5.order_send, mt5.positions_get, mt5.symbol_info_tick などの関数は、現在稼働しているMT5ターミナルに依存します。バックテストでは、これらの関数を、過去のデータ上で仮想的に動作するシミュレーターに置き換える必要があります。
  3. 時間とイベント駆動: threading や time.sleep はリアルタイムでの監視に使われます。バックテストは、ローソク足データを1本ずつ進めるシーケンシャルなループで時間をシミュレートするため、これらの仕組みは不要です。

バックテスト化への戦略

  1. シミュレーターの作成: MT5の動作を模倣するクラス(BacktestSimulator)を作成します。このクラスは以下の役割を担います。
    • 過去の価格データ(OHLC)を保持する。
    • 仮想の口座残高、ポジション、待機注文(Pending Order)を管理する。
    • order_sendやpositions_getのような関数をシミュレートするメソッドを提供する。
    • 時間を1本ずつ進め、その間の価格変動(高値・安値)で注文がトリガーされるかを判定する。
  2. 取引ロジックの抽出と改造: StopGridTraderクラスから、純粋な取引ロジック(グリッドをどう作り、どう決済するか)を抜き出し、新しいバックテスト用のクラス(StopGridTraderBacktest)に移植します。
    • GUI、スレッド、time.sleepに関するコードをすべて削除します。
    • mt5.* で始まる関数呼び出しを、上記で作成したBacktestSimulatorのメソッド呼び出しに置き換えます。
    • _monitorメソッドは、継続的にループするのではなく、ローソク足が1本進むごとに呼び出されるon_barのようなメソッドに改造します。
役に立った記事はSNSシェア!
  • URLをコピーしました!
  • URLをコピーしました!

おすすめ海外FX口座

最初の運用口座はXM スタンダード口座がおすすめ。ボラティリティの激しいゴールドをレバレッジ1000倍で運用できるため、資本効率に優れています。資金10万円からでも短期間で100万円に増やすことは可能でしょう。

さらにXMは3種ボーナスにも対応しています。入金額が10万円なら、およそ7万円ほどのボーナスがもらえて、資金17万円からトレードを始められます。

損失カバーボーナスを提供している海外FXブローカーは、利益を出しすぎると不正取引などの難癖をつけて利益を没収してくるケースがあります。ただXMは利用者が多く、資金面にも余裕があるため、そういったリスクは低めです。

取引コストを安く抑えたいなら、AXIORY テラ口座もおすすめ。ただしAXIORYはゴールドの提供レバレッジが100倍と低めなので、運用ロットを大きくしすぎないようにしましょう。

取引コストを最小限に抑えたいなら、Tradeview ILC口座もおすすめ。取引手数料が1ロット往復5ドルと業界最安値クラスです。

ただ最近はゴールドのスプレッドが30ポイントと標準的になってしまったので、20ポイント前後を狙うならもっと優秀なブローカーを探したほうがいいかもしれません。

この記事を書いた人

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

目次