この記事では無料EA「Stop-Grid Trader(Chisiki Grid)」の使い方を解説していきます。
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()ハーフオーダーでグリッド再展開ロジック
オーダーが半分になると、グリッドを再展開するロジック。
# -- 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
# =================================================Stop-Grid Trader(グリッド再展開バージョン)
上記2つを雑に合成したソースコード。
"""
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()GUI廃止→CLIバージョン
"""
Stop-Grid Trader CLI – version 3.2
------------------------------------------------------------
MT5用 対称ストップグリッド・トレーディング・ボット (CLI版)
・起動中のMT5に自動接続
・Ctrl+Cで全ポジション決済&予約注文削除して終了
"""
import time
import sys
import MetaTrader5 as mt5
from datetime import datetime
# =================================================================
# SETTING (パラメータ設定)
# =================================================================
SYMBOL = "XAUUSD" # 取扱通貨ペア
PRICE_DIGITS = 2 # 価格の小数点桁数
BASE_LOT = 0.02 # ベースロット (0.02以上の偶数推奨)
ORDERS_PER_SIDE = 10 # 片側の注文数
GRID_MULTIPLIER = 2.0 # スプレッドに対するグリッド幅の倍率
LOOP_COUNT = 100 # 全決済後の再開回数 (0なら1回のみ実行)
# 固定定数
DEVIATION = 100
MAGIC_NUMBER = 10 # マジックナンバー
GRID_TAG = "basic grid"
CHECK_INTERVAL = 1.0 # 監視間隔(秒)
# =================================================================
class StopGridTraderCLI:
def __init__(self):
self.symbol = SYMBOL
self.digits = PRICE_DIGITS
self.lot = BASE_LOT
self.side = ORDERS_PER_SIDE
self.mult = GRID_MULTIPLIER
self.loopN = LOOP_COUNT
self.done = 0
self.mid = None
self.step_pts = None
self.tp_high = None
self.tp_low = None
self.running = False
self.aborting = False # 手動終了フラグ
def log(self, message):
now = datetime.now().strftime("%H:%M:%S")
print(f"[{now}] {message}")
def _mt5_init(self):
if not mt5.initialize():
print("MT5初期化失敗: ターミナルが起動しているか確認してください。")
sys.exit()
if not mt5.symbol_select(self.symbol, True):
self.log(f"エラー: 通貨ペア {self.symbol} が見つかりません。")
mt5.shutdown()
sys.exit()
acc = mt5.account_info()
self.log(f"MT5接続成功: Account={acc.login}, Server={acc.server}")
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
def _pend(self, ord_type, price, sl, tp=0.0, vol=None, tag=GRID_TAG):
if vol is None: vol = self.lot
request = {
"action": mt5.TRADE_ACTION_PENDING,
"symbol": self.symbol,
"volume": self._norm_vol(vol),
"type": ord_type,
"price": round(price, self.digits),
"sl": round(sl, self.digits),
"tp": round(tp, self.digits) if tp > 0 else 0.0,
"deviation": DEVIATION,
"magic": MAGIC_NUMBER,
"comment": tag,
"type_time": mt5.ORDER_TIME_GTC,
}
mt5.order_send(request)
def _build_grid(self):
tick = mt5.symbol_info_tick(self.symbol)
info = mt5.symbol_info(self.symbol)
if tick is None or info is None: return
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.log(f"グリッド構築: Mid={self.mid}, Step={self.step_pts}pts")
for i in range(1, self.side + 1):
buy = self.mid + i * self.step_pts * pt
sell = self.mid - i * self.step_pts * pt
tp_b = (buy + self.step_pts * pt) if i == self.side else 0.0
tp_s = (sell - self.step_pts * pt) if i == self.side else 0.0
if i == self.side:
self.tp_high, self.tp_low = tp_b, tp_s
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.log("グリッド配置完了。監視中... (中止は Ctrl+C)")
def _full_close(self):
"""全ポジション決済と全予約注文削除"""
self.log("全ポジション決済および予約注文の削除を実行中...")
# 1. 予約注文の削除
orders = mt5.orders_get(symbol=self.symbol) or []
for o in orders:
mt5.order_send({"action": mt5.TRADE_ACTION_REMOVE, "order": o.ticket})
# 2. ポジションの決済
positions = mt5.positions_get(symbol=self.symbol) or []
for p in positions:
tick = mt5.symbol_info_tick(self.symbol)
side = 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
mt5.order_send({
"action": mt5.TRADE_ACTION_DEAL, "symbol": self.symbol,
"position": p.ticket, "volume": p.volume, "type": side,
"price": price, "deviation": DEVIATION, "magic": MAGIC_NUMBER, "comment": "grid exit"
})
self.log(f"クリーンアップ完了: 決済={len(positions)}件, 削除={len(orders)}件")
# 手動終了(abortingフラグ)でなければループ継続を確認
if not self.aborting:
self.done += 1
if self.done <= self.loopN:
self.log(f"ループ継続 ({self.done}/{self.loopN})。再起動します。")
time.sleep(2)
self._build_grid()
else:
self.log("全ループ完了。終了します。")
self.running = False
else:
self.running = False
def monitor(self):
self.running = True
try:
while self.running:
time.sleep(CHECK_INTERVAL)
tick = mt5.symbol_info_tick(self.symbol)
if not tick: continue
mid_now = (tick.bid + tick.ask) / 2
pt = mt5.symbol_info(self.symbol).point
# 利確ライン到達チェック
if (self.tp_high and mid_now >= self.tp_high) or \
(self.tp_low and mid_now <= self.tp_low):
self.log("ターゲット到達。")
self._full_close()
continue
# 部分利確監視
positions = mt5.positions_get(symbol=self.symbol) or []
for pos in positions:
if abs(pos.volume - self.lot) > 1e-6: continue
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:
self._close_partial(pos, self.lot / 2, "partial TP")
time.sleep(0.3)
self._handle_partial(pos)
except KeyboardInterrupt:
self.log("\n--- [中断検知] プログラムを安全に終了します ---")
self.aborting = True
self._full_close()
finally:
mt5.shutdown()
self.log("MT5から切断されました。終了します。")
def _close_partial(self, pos, vol, comment):
tick = mt5.symbol_info_tick(self.symbol)
side = 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
mt5.order_send({
"action": mt5.TRADE_ACTION_DEAL, "symbol": self.symbol,
"position": pos.ticket, "volume": self._norm_vol(vol),
"type": side, "price": price, "deviation": DEVIATION, "magic": MAGIC_NUMBER, "comment": comment
})
def _handle_partial(self, pos):
be_price = round(pos.price_open, self.digits)
mt5.order_send({
"action": mt5.TRADE_ACTION_SLTP, "symbol": self.symbol,
"position": pos.ticket, "sl": be_price, "tp": 0.0
})
# BE-REV注文の配置
info = mt5.symbol_info(self.symbol)
pt = info.point
if pos.type == mt5.POSITION_TYPE_BUY:
otype, sl = mt5.ORDER_TYPE_SELL_STOP, be_price + self.step_pts * pt
else:
otype, sl = mt5.ORDER_TYPE_BUY_STOP, be_price - self.step_pts * pt
self._pend(otype, be_price, sl, vol=self.lot, tag="BE-REV")
if __name__ == "__main__":
print("=======================================")
print(" Stop-Grid Trader CLI v3.2")
print(" 終了するには [Ctrl + C] を押してください")
print("=======================================")
trader = StopGridTraderCLI()
trader._mt5_init()
trader._build_grid()
trader.monitor()Stop-Grid Trader(グリッド再展開バージョン)のトレードロジック
提供されたコード「Stop-Grid Trader – version 3.1」は、MetaTrader 5 (MT5) で動作する、対称型のストップ・グリッド(逆指値グリッド)戦略を用いた自動売買ボットです。
このロジックの最大の特徴は、単なるグリッドトレードではなく、**「部分利確」「ブレイクイーブン(同値撤退設定)」「リバース(ドテン)注文」**を組み合わせた非常に精巧なリスク管理と追撃の仕組みにあります。
詳しくトレードロジックを解説します。
1. グリッドの初期設定 (_build_grid)
プログラムを開始すると、現在の価格を中心に上下に対称的なグリッドを生成します。
- 基準価格 (Mid Price): 現在のBidとAskの中間値。
- グリッド幅 (Step): スプレッド × 倍率(Multiplier) で決定されます。
- 注文の種類:
- 価格の上側に BUY_STOP (買い逆指値) を配置。
- 価格の下側に SELL_STOP (売り逆指値) を配置。
- (※通常の指値グリッドとは逆で、価格が動いた方向に付いていく「順張り型」です)
- 損切り (SL): すべての注文の初期SLは、グリッドの中央(Mid Price)に設定されます。
- 全体利確 (Global TP): 最外殻の注文からさらに1ステップ先に、全ポジションを決済する最終的なTPが設定されます。
2. ポジション発生と部分利確 (_monitor & _handle_partial)
いずれかの逆指値が刺さり、ポジション(0.02ロットと仮定)を持つと、以下の監視が始まります。
- 部分利確のトリガー: 価格がエントリー地点から「1ステップ分」順行した瞬間に発動します。
- 半分決済: ポジションの半分(0.01ロット)を成行で利確します。
- 残りのポジションの処理:
- SLの移動: 残りのポジションの損切り(SL)をエントリー価格(建値)に移動します。これで、このポジションで負けることはなくなります。
- BE-REV(リバース注文)の設置: ここがユニークな点で、建値の位置に逆方向の0.02ロットの逆指値注文を新たに出します。
3. BE-REV (Break-Even Reverse) の役割
価格が順行した後に反転し、建値(損切りライン)まで戻ってきた場合の保険と利益確保の仕組みです。
- もし価格が逆行して建値に戻ると、保有中の0.01ロットはプラスマイナスゼロで決済されます。
- 同時に、設置しておいた逆方向の逆指値(0.02ロット)が約定します。
- このリバースポジションのTP: 最初の中央値(Mid Price)に設定されます。
- 価格が中央に戻れば利益。
- 中央を突き抜けていれば即座に利確。
4. レンジ相場対策 (追加ロジック)
グリッドトレードの弱点である「狭い範囲での乱高下(レンジ)」で注文だけが溜まっていくのを防ぐ仕組みが組み込まれています。
- 条件: ポジションが一つもない状態で、グリッドの半分以上の注文が消化(あるいはキャンセル)されている場合。
- アクション: 停滞していると判断し、残りの待機注文をすべて削除して、**現在の価格でグリッドを再構築(リスタート)**します。
5. 全決済とループ (_full_close)
最終的な終了条件です。
- 終了条件: 価格がグリッドの最外郭にある tp_high または tp_low に到達した場合。
- アクション:
- すべての待機注文(Pending Orders)を取り消す。
- 保有しているすべてのポジションを成行で決済する。
- loop_count の設定に従い、必要であれば新しいグリッドを組んで最初から繰り返す。
ロジックのまとめ(戦略的意味)
- トレンド追随: 価格が伸びる方向にポジションを持つため、強いトレンドが出ると連続的に利益が積み上がります。
- 即座のリスク排除: 1ステップ動いただけで半分利確し、残りを建値決済にするため、勝率よりも「負けないこと」を重視しています。
- 往復ビンタの回避: 建値にリバース注文を置くことで、トレンドが反転して「行って来い」の動きになった場合でも、戻りの動きで利益を取れるように設計されています。
- 幾何学的管理: スプレッドを基準にグリッド幅を決めるため、ボラティリティに応じた柔軟な設定が可能です。
注意点
- ロット数: コード上、Base lot は偶数(0.02, 0.04…)かつ 0.02 以上である必要があります(半分に分割するため)。
- 手数料: 頻繁に部分利確を行うため、取引手数料が高い口座やスプレッドが極端に広い銘柄では、コスト負けする可能性があります。
このボットは、**「トレンドが発生すれば爆発的に稼ぎ、レンジであれば微損または微益で耐え、大きなトレンド転換もリバース注文でカバーする」**という、非常に防御力の高いグリッド戦略と言えます。
Stop-Grid Trader(CLI版)の使い方
提供されたコードは、MetaTrader 5 (MT5) をPythonから制御して、**「対称型ストップグリッド(逆指値グリッド)」**という手法で自動売買を行うボットです。
このボットは、現在の価格から上下に網(グリッド)を張り、トレンドが発生した方向に次々とエントリーし、利益が出たら半分を利確、残りを建値(同値)ストップに置いて利益を伸ばす仕組みを持っています。
使い方の手順を分かりやすく解説します。
1. 準備するもの
- MetaTrader 5 (MT5) のインストール: PC版のMT5が必要です。
- Python のインストール: PythonがPCに入っている必要があります。
- ライブラリのインストール: ターミナル(コマンドプロンプト)で以下のコマンドを実行してください。 code Bashdownloadcontent_copyexpand_less
pip install MetaTrader5 - MT5の設定:
- MT5を起動し、ログインしておきます。
- 「ツール」→「オプション」→「エキスパートアドバイザー」タブで、**「アルゴリズム取引を許可する」**にチェックを入れてください。
2. コードの設定(パラメータ変更)
コード内の SETTING セクションを自分のトレード環境に合わせて書き換えます。
- SYMBOL: 取引したい通貨ペア名(例: “USDJPY”, “XAUUSD” など)。
- PRICE_DIGITS: その銘柄の小数点桁数(ゴールドなら 2、ドル円なら 3 など)。
- BASE_LOT: 1つの注文のロット数。**0.02以上(0.02, 0.04…)**が推奨です(半分決済する機能があるため)。
- ORDERS_PER_SIDE: 上下に何本ずつ注文を出すか(10なら上に10本、下に10本)。
- GRID_MULTIPLIER: グリッドの間隔。現在のスプレッドの何倍にするかを指定します。
- LOOP_COUNT: 利確ターゲット(一番端のグリッド)に到達した後、何回再起動するか。
3. 実行方法
- MT5を起動した状態にします。
- VSCodeやメモ帳などでコードを保存(例: grid_bot.py)します。
- ターミナルで実行します: code Bashdownloadcontent_copyexpand_less
python grid_bot.py
4. このボットの挙動(ロジック)
このプログラムは起動すると以下の動作を自動で行います。
- グリッド設置: 現在価格を中心に、上下に「Buy Stop(買い逆指値)」と「Sell Stop(売り逆指値)」を並べます。
- 部分利確(重要):
- 価格が動いて注文がヒットし、含み益が「グリッド1つ分」に達すると、ロットの半分を決済します。
- 残りの半分は、ストップロスを建値(エントリー価格)に移動し、負けを無くします。
- 同時に、建値の位置に再度逆指値注文を置き直します。
- 全決済:
- 一番外側のターゲット価格(TP)に到達すると、全てのポジションと注文をクリアして、設定された回数分だけ最初からやり直します。
- 安全停止:
- プログラムを実行している画面で Ctrl + C を押すと、即座に全てのポジションを成行決済し、予約注文をすべて削除して終了します。
5. 注意点
- デモ口座でテスト: リアル口座で動かす前に、必ずデモ口座で挙動を確認してください。
- スプレッドの影響: GRID_MULTIPLIER が小さすぎると、スプレッドの影響で注文がすぐに約定したり、エラーになったりする可能性があります。
- PCの起動: プログラムを実行している間は、PCとMT5を起動し続ける必要があります(VPSが推奨されます)。
- マジックナンバー: MAGIC_NUMBER = 10 となっています。他のEAと同じ番号にならないよう注意してください。
トラブルシューティング
- 「MT5初期化失敗」と出る場合: MT5が起動しているか、ログインされているか確認してください。
- 注文が入らない場合: MT5の「操作履歴」タブを見て、エラーコード(10014など)が出ていないか確認してください。銘柄名(SYMBOL)が、お使いの証券会社のものと完全に一致しているか(例: XAUUSD か GOLD か)も重要です。
