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のおすすめ運用方法はこちら。

DEF_SYMBOL(運用銘柄)XAUUSD
DEF_LOT(ロット)0.2-1.0
DEF_ORDERS_SIDE(片側のグリッド段数)10
DEF_MULTIPLIER(グリッド倍率)10.0-20.0以上
DEF_LOOP(ループ回数)0-1

運用銘柄については「XAUUSD」がおすすめ。ゴールドはボラティリティが非常に激しく、ピラミッディングトレードで利益を出しやすいです。

もっとボラティリティの激しい銘柄としてビットコイン(BTCUSD)も挙げられますが、対応ブローカーが少なく、適用レバレッジも10倍と低いため、FX初心者では利益を出しにくいです。

運用ロットについては「0.2」がおすすめ。ゴールド0.2ロットのスイング軸ピラミッディングトレードなら、1日で1000ドルから2000ドルほどの利益を狙えます。

これがビットコインだとレバレッジ10倍でしか運用できないため、運用ロット目安も0.02ロットと低くなり、1日で得られる利益も50ドルから100ドルと控えめになります。

グリッド段数については「10」がおすすめ。ゴールドのスプレッドが30ポイント、グリッド段数が10段の場合、一括TPラインは3000ポイントとなります。ゴールドの値動きも1日2000ポイントから3000ポイントほどなので、1日で得られる利幅としては妥当なラインです。

グリッド倍率については「10.0」がおすすめ。ゴールドのスプレッドが30ポイントなら、グリット幅は1段あたり300ポイントとなります。それが10段であれば3000ポイントとなり、後述のゴールドの1日の値動き2000-3000ポイントに刺さりやすくなります。

グリット倍率2倍の高頻度運用は、レンジ相場だと手数料負けしやすく、非常に難易度が高い。ソースコードを作った本人でない限り、安定した利益を出すのは難しい。

ループ回数については「0-1」がおすすめ。設定が「0」ならループは1回だけ、設定が「1」ならループは2回行います。

ボットの再設定が面倒なら100回以上に設定してもいいですが、週末にポジション持ち越すと窓で大損することがあります。金曜日の閉場時間までにはポジションを決済して、botを停止しておきましょう。

おすすめ運用口座

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

XM スタンダード口座の詳細は、XM「取引口座タイプ」で確認できます。

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

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

XM ボーナスキャンペーンの詳細は、XM「プロモーション」で確認できます。

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

AXIORY テラ口座の詳細は、AXIORY「口座→口座タイプ」で確認できます。

取引コストを最小限に抑えたいなら、Tradeview ILC口座もおすすめ。取引手数料が1ロット往復5ドルと業界最安値クラスです。ただ最近はゴールドのスプレッドが30ポイントと標準的になってしまったので、20ポイント前後を狙うならもっと優秀なブローカーを探したほうがいいかもしれません。

Tradeview ILC口座の詳細は、英語版Tradeview「Accounts」で確認できます。

プログラム・ソースコードの概要

このPythonスクリプト 20250806-1930_fc348efa982b753899e488eafbf081a1.py は、MetaTrader 5 (MT5) プラットフォーム上で動作する自動売買ボットです。GUI(グラフィカル・ユーザー・インターフェース)を備えており、比較的簡単に操作できます。

以下に、このコードの機能、取引戦略、使い方を詳しく説明します。

1. このプログラムの概要

このスクリプトは「ストップ・グリッド・トレーダー」と名付けられており、現在の価格を中心に上下に複数の逆指値注文(ストップ注文)を格子状(グリッド)に配置して取引を行います。

主な特徴は以下の通りです。

  • GUIによる簡単操作: 起動するとウィンドウが表示され、マウス操作でMT5ターミナルを選び、取引パラメータ(通貨ペア、ロット数など)を設定できます。
  • 自動グリッド生成: 設定に基づき、買い(BUY_STOP)と売り(SELL_STOP)の注文を自動で配置します。
  • 洗練された利益確定・リスク管理: 注文が成立すると、利益を段階的に確保し(部分決済)、損失を限定する(ストップロスを建値に移動)ロジックが組み込まれています。
  • ループ機能: グリッド全体の取引が完了した後、指定した回数だけ自動で取引を再開できます。

2. 取引戦略(ロジック)の詳細

このボットが行う取引は、以下のステップで進められます。

ステップ1: グリッドの構築

  1. 仲値の計算: 現在の売値(Bid)と買値(Ask)の平均価格(仲値)を計算し、これをグリッドの中心とします。
  2. 注文の配置:
    • 仲値から上下に、等間隔で逆指値注文を設置します。間隔は「スプレッド × 乗数(Multiplier)」で決まります。
    • 仲値より上には「買い逆指値 (BUY_STOP)」、下には「売り逆指値 (SELL_STOP)」を配置します。
    • 最も外側の注文には、さらに1グリッド分外側に利食い(Take Profit, TP)が設定されます。このTPが、グリッド全体の終了目標となります。

ステップ2: 注文成立後の処理

  1. 部分的な利益確定: いずれかの注文が成立(約定)し、価格が1グリッド分有利な方向に動くと、ポジションの半分を決済して利益を確定します。
  2. 残りのポジションの管理:
    • 損切り(Stop Loss, SL)を建値(ポジションを持った価格)に移動します。これにより、残りのポジションは少なくとも損はしない状態(ブレークイーブン)になります。
    • 同時に、その建値に反対方向の逆指値注文(リバース注文)を新たに設置します。これは、価格が反転した場合に備えるためのものです。
  3. リバース注文成立後の特殊処理:
    • このリバース注文が成立した場合、もし価格が最初のグリッド中心(仲値)を越えていれば、即座にポジションを決済します。
    • そうでなければ、最初のグリッド中心を利食い目標とします。

ステップ3: グリッド全体の終了

  • 価格が、ステップ1で設定した最も外側の利食い(TP)レベルに到達すると、ボットは以下の処理を行います。
    1. 全ての待機中の注文をキャンセルします。
    2. 全ての保有中のポジションを決済します。
    3. ループ回数が残っていれば、新しいグリッドを構築して取引を再開します。残っていなければ、プログラムは終了します。

3. コードの構造

このスクリプトは、いくつかの要素から構成されています。

  • GUIヘルパー (choose_terminal, ParamDialog):
    • choose_terminal: PC上で実行されているMT5ターミナルを検出し、ユーザーに選択させるためのダイアログ画面です。
    • ParamDialog: 取引に必要なパラメータ(通貨ペア、ロット数、グリッドの注文数など)を入力させるためのダイアログ画面です。
  • StopGridTraderクラス:
    • これが取引ロジックの本体です。
    • __init__: パラメータを保存し、ステータス表示用のGUIウィンドウを準備します。
    • _mt5_init: MT5への接続を初期化します。
    • _build_grid: 上記の戦略に基づき、注文グリッドを構築します。
    • _monitor: バックグラウンドで価格を常に監視し、部分決済や全体決済のタイミングを判断します。
    • _handle_partial, _place_be_rev: 注文成立後の複雑な処理を担当します。
    • _full_close: 全ての注文とポジションを決済し、ループ処理を行います。
    • _abort: GUIの「中止」ボタンが押されたときに、安全に取引を停止します。
    • run: ボット全体の実行フローを管理します。
  • main関数:
    • プログラムが実行されると最初に呼び出され、choose_terminalParamDialogを順に表示し、得られた情報でStopGridTraderを起動します。

4. 使い方

  1. MT5を起動しておきます。(XM MT5AXIORY MT5Tradeview MT5
  2. このPythonスクリプトを実行します。
  3. 「Select MT5 terminal」というウィンドウが表示されたら、取引に使用したいMT5ターミナルを選択して「Use」ボタンを押します。
  4. 「Grid parameters」ウィンドウで、以下の項目を入力し、「Start」ボタンを押します。
    • Symbol: 通貨ペア名(例: XAUUSD, USDJPY
    • Price digits: 価格の小数点以下の桁数
    • Base lot: 基本となる取引量(0.02以上の偶数である必要があります)
    • Orders / side: グリッドの片側に配置する注文数
    • Grid multiplier: グリッド間隔を決めるための乗数
    • Loop count: 全決済後に取引を繰り返す回数(0なら1回のみ)
  5. 「Stop-Grid Trader」という小さなステータスウィンドウが表示され、自動売買が開始されます。
  6. 途中で取引を終了したい場合は、そのウィンドウの「Abort」ボタンを押してください。

スクリプトの使い方・設定方法(丁寧版)

この「ストップ・グリッド・トレーダー」の使い方と設定方法について、ステップごとに丁寧に解説します。

事前準備

このスクリプトを実行する前に、以下の準備が必要です。

  1. Python環境の構築:
    • お使いのPCにPythonをインストールしてください。
    • ターミナル(またはコマンドプロンプト)で以下のコマンドを実行し、必要なライブラリをインストールします。 bash pip install MetaTrader5 pip install psutil
      • MetaTrader5: MT5と連携するための公式ライブラリです。
      • psutil: 実行中のMT5ターミナルを自動で検出するために使用します。
  2. MetaTrader 5 (MT5) の準備:
    • PCにMT5プラットフォームをインストールし、取引したい証券会社の口座でログインしておきます。
    • MT5のメニューから ツール -> オプション を開き、エキスパートアドバイザ タブを選択します。
    • 「自動売買を許可」「DLLの使用を許可する」 の2つにチェックを入れてOKを押してください。これは、外部プログラム(このPythonスクリプト)からの取引操作を許可するために必須の設定です。

MT5のダウンロードリンクはこちら。

使い方:ステップ・バイ・ステップ

ステップ1: スクリプトの実行

  1. 取引に使用したいMT5ターミナルを起動しておきます。
  2. このPythonスクリプト (20250806-1930_fc348efa982b753899e488eafbf081a1.py) を実行します。
    • ターミナルで python 20250806-1930_fc348efa982b753899e488eafbf081a1.py と入力するか、Python対応のエディタから実行してください。

ステップ2: MT5ターミナルの選択

  1. スクリプトを実行すると、最初に 「Select MT5 terminal」 というタイトルのウィンドウが表示されます。
  2. このウィンドウには、現在PC上で起動しているMT5ターミナルの一覧(口座番号、サーバー名、残高など)が表示されます。
  3. 取引に使用したいMT5ターミナルをマウスでクリックして選択し、「Use」 ボタンを押します。
    • もし一覧に何も表示されない場合は、MT5が正しく起動しているか、またpsutilライブラリがインストールされているかを確認してください。

ステップ3: 取引パラメータの設定

  1. 次に 「Grid parameters」 というタイトルのウィンドウが表示されます。ここで取引の具体的なルールを設定します。
  2. 各項目を慎重に入力し、「Start」 ボタンを押すと取引が開始されます。

設定項目の詳細解説

各パラメータの意味と設定のヒントを以下に示します。

パラメータ名デフォルト値説明設定のヒント
SymbolXAUUSD取引する通貨ペアや商品の名前です。MT5の「気配値表示」に表示されている正式名称(サフィックス含む)を正確に入力してください。例: USDJPY.pro, EURUSD
Price digits2利益計算や注文価格の丸めに使用する小数点以下の桁数です。通貨ペアに合わせて設定します。例: XAUUSD (金) は 2USDJPY3EURUSD5 が一般的です。
Base lot0.021つの注文あたりの基本取引量(ロット数)です。必ず0.01で割り切れる偶数(0.02, 0.04, 0.06…) にしてください。ロジックが半分(0.01単位)を部分決済するためです。証券会社の最小ロット数も確認してください。
Orders / side10グリッドの片側(上下それぞれ)に配置する注文の数です。この数が多いほど、広い値幅をカバーできますが、同時に多くのポジションを持つリスクも増えます。最初は少なめ(例: 5~10)で試すのが安全です。
Grid multiplier2.0グリッドの注文間隔を決めるための乗数です。間隔 = スプレッド × この乗数 で計算されます。1.0より大きい値を設定してください。1.0以下だと、スプレッドより狭い間隔で注文を出そうとしてエラーになる可能性があります。ボラティリティが高い相場では大きめの値(例: 3.0以上)、低い相場では小さめの値(例: 1.5~2.5)が適しています。
Loop count0グリッド全体の取引が完了した後に、自動で取引を再開する回数です。0 を設定すると、1回のグリッド取引が終了したらプログラムも終了します。1 を設定すると、1回終了後、もう1回新しいグリッドで取引を開始します。無限にループさせたい場合は、非常に大きな数を設定します。

入力値のチェック:

  • 入力内容に誤りがある場合(例: ロット数が奇数、マイナスの値を入力したなど)、エラーメッセージが表示されます。内容を修正して再度「Start」ボタンを押してください。

ステップ4: 取引の監視と終了

  1. 「Start」ボタンを押すと、パラメータ設定ウィンドウが閉じ、「Stop-Grid Trader」 という小さなステータスウィンドウが表示されます。
  2. このウィンドウには、現在のボットの状態(「Grid ready」「Loop 1 finished – restarting…」など)が表示されます。
  3. ボットはバックグラウンドで自動的に取引を開始・管理します。
  4. 取引を途中で強制終了したい場合は、このステータスウィンドウにある 「Abort」 ボタンを押してください。確認ダイアログが表示され、「はい」を選択すると、全ての待機注文と保有ポジションを決済して安全にプログラムを終了します。
  5. 設定した Loop count に達すると、ボットは自動的に全決済を行い、「All loops done – exit」というメッセージを表示して数秒後に自動で終了します。

トレードロジックについて

このボットのトレードロジックは、「対称的なストップ注文グリッド」と「段階的な利益確定とリスク管理」という2つの主要な要素で構成されています。

1. グリッドの構築(初期設定)

このロジックの出発点です。

  • 基準価格: まず、現在の「売値(Bid)」と「買値(Ask)」の平均価格(仲値)を計算します。これがすべての取引の基準点 mid となります。
  • 注文間隔 (Grid Step): 次に、注文を配置する間隔を決めます。これは 現在のスプレッド × ユーザーが設定した乗数(Multiplier) で計算されます。この間隔が、グリッドの1ステップの幅になります。
  • 注文の配置:
    • 基準価格 mid から上下に、計算された「注文間隔」で 逆指値注文(STOP注文) を格子状に配置します。
    • 買い注文 (BUY STOP): mid よりに配置されます。価格が上昇して注文価格に達すると、買いポジションが成立します。
    • 売り注文 (SELL STOP): mid よりに配置されます。価格が下落して注文価格に達すると、売りポジションが成立します。
    • 損切り (Stop Loss, SL): 全ての逆指値注文の損切りラインは、最初の基準価格 mid に設定されます。もし価格が逆行して mid に戻れば、ポジションは自動的に決済されます。
    • 利食い (Take Profit, TP):
      • ほとんどの注文には、個別のTPは設定されません(TP=0)。
      • ただし、グリッドの最も外側に配置される買い注文と売り注文には、さらに1グリッド分外側にTPが設定されます。このTPが、グリッド全体の取引を終了させるための最終目標地点となります。

【図解イメージ】

      ↑ 価格上昇
      │
  TP  ├─ [最も外側のBUY STOPのTP]  <-- グリッド全体終了ライン
      │
 Grid ├─ [最も外側のBUY STOP注文]  (SLはmid)
      │
 Grid ├─ [2番目のBUY STOP注文]     (SLはmid)
      │
  mid ├─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ (基準価格 & 全注文のSL)
      │
 Grid ├─ [2番目のSELL STOP注文]    (SLはmid)
      │
 Grid ├─ [最も外側のSELL STOP注文] (SLはmid)
      │
  TP  ├─ [最も外側のSELL STOPのTP]  <-- グリッド全体終了ライン
      │
      ↓ 価格下落

2. ポジション成立後の処理(コアロジック)

いずれかの逆指値注文が約定(Fill)し、ポジションが成立すると、以下の自動処理が開始されます。このロジックは、利益を確保しつつリスクを管理することを目的としています。

ステップ A: 部分的な利益確定

  1. ポジションが成立後、価格が有利な方向に 1グリッド分 動いたとします。
    • 買いポジションの場合:建値 + 1グリッド分の値幅
    • 売りポジションの場合:建値 - 1グリッド分の値幅
  2. この条件が満たされると、ボットはポジションの半分(0.01ロット分)を決済し、利益を確定させます。

ステップ B: 残り半分のポジションの管理

部分決済直後、残った半分のポジションに対して以下の処理を行います。

  1. 損切り(SL)を建値に移動(Break-Even):
    • SLを、ポジションが成立した価格(建値)に設定し直します。
    • これにより、もし価格が不利な方向に反転しても、このポジションは損失を出さずに(±0で)決済されます。
  2. リバース注文の設置:
    • 建値と同じ価格に、反対方向の逆指値注文(リバース注文)を新たに設置します。
      • 元のポジションが「買い」なら、「売りSTOP注文」を建値に置きます。
      • 元のポジションが「売り」なら、「買いSTOP注文」を建値に置きます。
    • このリバース注文は、価格が建値まで戻ってきた場合に、ドテン(ポジションの反転)を狙うためのものです。

【図解イメージ:買いポジションの場合】

      ↑ 価格上昇
      │
 (A)  ├─ 1グリッド分上昇 → 半分利確
      │
      ├─ ポジション成立(建値)
      │   (B-1) 残り半分のSLをこの価格に移動
      │   (B-2) ここに「売りSTOP注文(リバース注文)」を新規設置
  mid ├─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
      │

ステップ C: リバース注文が成立した場合の特殊ロジック

ステップBで設置したリバース注文が成立した場合、さらに特殊な処理が行われます。

  1. 即時決済の判断:
    • リバース注文が成立した時点で、価格が最初のグリッド中心 mid を既に超えているかを判定します。
      • (例)買いリバース注文が成立した時に、現在価格が mid 以上になっている。
    • もし超えていれば、そのリバースポジションは即座に決済されます。これは、価格が中心付近で停滞する状況での無駄な往復取引を避けるためです。
  2. TPの設定:
    • もし mid を超えていなければ、そのリバースポジションのTP(利食い)を最初のグリッド中心 mid に設定します。価格がグリッドの中心に戻る動きを利益に変えようとします。

3. グリッド全体の終了

この一連の取引は、以下のいずれかの条件で終了します。

  • 価格が最終目標TPに到達: 価格が上昇または下落し、グリッドの最も外側に設定されたTPに到達した場合。
  • ユーザーによる手動停止: ユーザーがGUIの「Abort」ボタンを押した場合。

どちらの場合でも、ボットは以下のクリーンアップ処理を行います。

  1. 全ての待機中注文(Pending Orders)をキャンセルします。
  2. 全ての保有中ポジション(Positions)を決済します。

その後、ユーザーが設定したループ回数が残っていれば、再び最初のグリッド構築からロジックを再開します。残っていなければ、プログラムは完全に終了します。

Stop-Grid Trader(Chisiki Grid)の収益性を高めるには?

Chisikiさんのツイートを参考にする

ピラミッディングEAはボラティリティが高ければ高いほど、利益を出しやすくなります。ボラティリティの高いタイミングだけ運用したり、高頻度・高ロットの運用をすることで、全体のリスクリワードを高められます。

具体的なやり方はこちら。

  • APIで出来高引用する(ソース
  • ボラティリティの高いタイミング(経済指標など)でピンポイント稼働する(ソース
  • 短期分析で倍率を動的に調整する(ソース
  • 機械学習を入れつつ、鮮度の高いデータだけを利用する(ソース

具体的な実装方法については、LLMと相談して、バイブコーディングで行うといいでしょう。

取引コストの低いA-bookブローカーを使う

ピラミッディングEAは基本的にグリッド幅が狭ければ狭いほど利益率が高くなります。ただグリッド幅が狭いと、スプレッドなどで手数料負けしやすくなるので、少しでも取引コストの低いブローカーを使う必要があります。

またB-bookブローカーだとマイナススリッページが発生しやすくなるので、高ロット運用するならA-bookブローカーを使うことが前提になります。

優秀な海外FXブローカーを探すなら、調査兵団さんのブログが参考になります。

取引コスト・約定力の優秀なブローカーはこちら。

コピートレードツールでB-bookブローカーに並列化する

一部のB-bookブローカーは集客のためA-bookブローカーよりも低いスプレッドを提供していることがあります。またB-bookは注文を呑んでスプレッドを調整しているため、経済指標が発生してもスプレッドが広がりにくいです。高頻度ピラミッディングで大きく稼ぐチャンスになるでしょう。

ただB-bookブローカーはトレーダーで利益相反関係にあるため、トレーダーが勝ちすぎると、露骨にマイナススリッページを広げたり、不正取引扱いで利益没収してきたりすることがあります。

対策としては複数のブローカーや追加口座で分散して運用するのがおすすめ。例えば1つの口座で月100万円稼ぐのではなく、10個の口座で1口座あたり月10万円を稼ぐようにすると、嫌がらせされにくくなります。

コピートレードツール・サービスの代表例はこちら。

1つ目のPersonal Trade CopierはFX Blueに登録していれば無料で使えます。ただし口座の数だけソフトウェア版MT5を稼働させておく必要があるため、動作が重くなりやすいです。

最近ではMetaCopierTraders Connectなどクラウドタイプのコピートレードサービスもあります。料金は発生しますが、フォロワー口座でソフトウェア版MT5を稼働させる必要がないため、メモリの少ないVPSでも10口座以上でスケールさせやすいです。

追加口座はXMAXIORYTradeviewでも作れます。コピートレードサービスの練習台に使うといいでしょう。特にTradeviewの新しい会員ページでは複数の口座を同時管理できるようになっています。

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のおすすめな稼ぎ方

海外FXの稼ぎ方は、Pythonスクリプト「Stop Grid Trader」による自動売買がおすすめ。ソースコードが無料公開されており、VS Codeなどで簡単に実行できます。

基本のトレードロジックはピラミッディング。ボラティリティの激しいゴールド・ビットコインと相性がいいです。トラップ注文・ドテンも併用することで、苦手相場のレンジ相場でも利益を出せます。

グリッド倍率を「10.0」にすることでスイング軸運用できるようになり、スプレッドの広いハイレバレッジ口座でも利益を出しやすくなります。

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

XM スタンダード口座の詳細は、XM「取引口座タイプ」で確認できます。

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

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

XM ボーナスキャンペーンの詳細は、XM「プロモーション」で確認できます。

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

AXIORY テラ口座の詳細は、AXIORY「口座→口座タイプ」で確認できます。

取引コストを最小限に抑えたいなら、Tradeview ILC口座もおすすめ。取引手数料が1ロット往復5ドルと業界最安値クラスです。ただ最近はゴールドのスプレッドが30ポイントと標準的になってしまったので、20ポイント前後を狙うならもっと優秀なブローカーを探したほうがいいかもしれません。

Tradeview ILC口座の詳細は、英語版Tradeview「Accounts」で確認できます。

この記事を書いた人

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

目次