Python-MetaTrader 5ストラテジーテスター(第3回):MetaTrader 5風の取引操作 — 処理と管理
内容
- はじめに
- order_sendメソッド
- 取引検証クラス
- order_send内のすべてのバリデーター
- シミュレーター内のCTradeクラス
- シミュレーターで取引操作を実行する
- シミュレーターで注文とポジションを管理する
- 結論
はじめに
前回の記事では、Python-MetaTraderモジュールが提供する構文や関数と同等の機能をシミュレーターに実装し、注文、取引、ポジションといったMetaTraderに近い形で扱えるようにしました。本記事では、それらの取引操作(取引処理)をMetaTrader 5にできるだけ近い形で扱う仕組みを実装していきます。

order_sendメソッド
MetaTrader 5におけるすべての取引操作(指値注文の発注、買いおよび売りポジションの新規建て、注文の変更、注文の削除など)は、単一の標準関数を通じて実行されます。
MQL5ではその関数はOrderSend、Python-MetaTrader 5ではorder_sendと呼ばれます。
ドキュメントには次のように記述されています。
order_sendメソッドは、取引サーバーに対して取引操作を実行するリクエストを送信します。OrderSendと同様の機能を持ちます。
order_send(
request // request structure
); この関数は単一の引数としてrequestを受け取ります。requestはMqlTradeRequest型の構造体であり、実行したい取引内容をすべて記述します。
以下の表は、request構造体に必ず含まれるフィールドを示しています。
| フィールド | 説明 |
|---|---|
| action | 取引操作の種類。TRADE_REQUEST_ACTIONS列挙型のいずれかの値を取ります。 |
| magic | EA ID。取引注文を分析的に処理できるようにします。各EAは、取引リクエストを送信する際に固有のIDを設定できます。 |
| order | 注文チケット。未決注文の変更などに使用されます。 |
| symbol | 注文の対象となる取引商品の名称。注文の変更やポジションの決済時には不要です。 |
| volume | 取引の希望数量(ロット単位)。取引をおこなう際の実際の取引量は注文執行タイプによって異なります。 |
| price | 注文執行価格。TRADE_ACTION_DEAL型を持つSYMBOL_TRADE_EXECUTION_MARKETのような成行執行の場合、価格は指定されないことがあります。 |
| stoplimit | ストップリミット注文用の価格。指定条件に到達した時点で注文が有効化されます。 |
| sl | ストップロス価格。不利な方向への価格変動時に損失を制限するために使用されます。 |
| tp | 有利な方向への価格変動時に利益確定をおこないます。 |
| deviation | 許容スリッページ(points)。指定価格からの最大許容乖離を示します。 |
| type | 注文タイプ。ORDER_TYPE列挙型のいずれかの値を取ります。 |
| type_filling | 注文執行方式。ORDER_TYPE_FILLING列挙型のいずれかの値を取ります。 |
| type_time | 注文の有効期限タイプ。ORDER_TYPE_TIME列挙型のいずれかの値を取ります。 |
| expiration | 未決注文の有効期限(TIME_SPECIFIEDタイプの注文の場合) |
| comment | 注文コメント |
| position | ポジションチケット。ポジションを変更したり、閉じたりする際に入力され、明確に識別できるようにします。通常はポジションを開いた注文のチケットと一致します。 |
| position_by | 反対ポジションのチケット。同一銘柄の逆方向ポジションを建てることで既存ポジションを相殺し、決済する場合に使用されます。 |
私たちのクラスにも同様の関数が必要です。
def order_send(self, request: dict): """ Sends a request to perform a trading operation from the terminal to the trade server. The function is similar to OrderSend in MQL5. """ if not self.IS_TESTER: result = self.mt5_instance.order_send(request) if result is None or result.retcode != self.mt5_instance.TRADE_RETCODE_DONE: self.__GetLogger().warning(f"MT5 failed: {self.mt5_instance.last_error()}") return None return result
前回の記事では、シミュレータークラスにストラテジーテスター用のモードを導入しました。これはIS_TESTER = trueの場合に有効になるモードです。このモードが有効でない場合、シミュレーターはMetaTrader 5クライアントから提供される情報に依存し、すべての取引操作は実際のクライアント環境で処理されます。
上記のコードスニペットでは、ユーザーがテスターモードを使用していない場合に、MetaTrader 5へ注文リクエストを送信します。
そうでない場合は、リクエストから各パラメータを取得します。
action = request.get("action") order_type = request.get("type") symbol = request.get("symbol") volume = float(request.get("volume", 0)) price = float(request.get("price", 0)) sl = float(request.get("sl", 0)) tp = float(request.get("tp", 0)) ticket = int(request.get("ticket", -1)) ticks_info = self.tick_cache[symbol] now = utils.ensure_utc(ticks_info.time) ts = int(now.timestamp()) msc = int(now.timestamp() * 1000)
この関数内では、ポジションの新規建てや決済、取引をコンテナに記録する処理(本質的にはMetaTrader 5でおこなわれる処理)など、すべての操作を手動で処理する必要があります。
I:保留注文の発注
シミュレーターでは、保留注文は、一時的な注文コンテナ配列(self.__orders_container__)に格納されている注文に関する情報にすぎません。
if action == self.mt5_instance.TRADE_ACTION_PENDING: order_ticket = self.__generate_order_ticket() order = self.TradeOrder( ticket=order_ticket, time_setup=ts, time_setup_msc=msc, time_done=0, time_done_msc=0, time_expiration=request.get("expiration", 0), type=order_type, type_time=request.get("type_time", 0), type_filling=request.get("type_filling", 0), state=self.mt5_instance.ORDER_STATE_PLACED, magic=request.get("magic", 0), position_id=0, position_by_id=0, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume_initial=volume, volume_current=volume, price_open=price, sl=sl, tp=tp, price_current=price, price_stoplimit=request.get("price_stoplimit", 0), symbol=symbol, comment=request.get("comment", ""), external_id="", )
注文を作成した後、それを注文コンテナ配列に追加し、注文履歴配列(コンテナ)にも記録します。
self.__orders_container__.append(order) self.__orders_history_container__.append(order) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "order": order_ticket, }
II:ポジションの新規建て
MetaTrader 5では、ポジションとは金融商品における売買契約のことを指します。ロングポジション(買い)は価格上昇を見込んで買いをおこなうことで形成され、ショートポジション(売り)は将来的な価格下落を見込んで資産を売却することで形成されます。
シミュレーターにおいては、ポジションは、コンテナに保存された疑似的なポジション情報として扱われます。
if action == self.mt5_instance.TRADE_ACTION_DEAL: position_ticket = self.__generate_position_ticket() order_ticket = self.__generate_order_ticket() deal_ticket = self.__generate_deal_ticket() position = self.TradePosition( ticket=position_ticket, time=ts, time_msc=msc, time_update=ts, time_update_msc=msc, type=order_type, magic=request.get("magic", 0), identifier=position_ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price_open=price, sl=sl, tp=tp, price_current=price, swap=0, profit=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) self.__positions_container__.append(position)
ポジションの新規建てと決済のプロセスには、TRADE_ACTION_DEALというアクションが使用されます。 このような操作の結果は「取引」として扱われるため、その内容は取引コンテナに履歴として記録されます。
self.__deals_history_container__.append( self.TradeDeal( ticket=deal_ticket, order=order_ticket, time=ts, time_msc=msc, type=order_type, entry=self.mt5_instance.DEAL_ENTRY_IN, magic=request.get("magic", 0), position_id=position_ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price=price, commission=self.__calc_commission(), swap=0, profit=0, fee=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) ) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "deal": deal_ticket, "order": order_ticket, "position": position_ticket, }
取引とは、MetaTrader 5ターミナルにおけるポジションの新規建てと決済の記録(履歴)のことです。

III:ポジションの決済
ポジションを決済するリクエストは、新規ポジションを開設するリクエストと非常によく似ています。どちらも取引ですが、エントリー種別が異なります。
ポジションの決済リクエストを検出するために、リクエストにポジションキー(既存ポジションのチケット)が含まれているかどうかを確認します。
if action == self.mt5_instance.TRADE_ACTION_DEAL: # ---------- CLOSE POSITION ---------- ticket = request.get("position", -1) if ticket != -1: pos = next( (p for p in self.__positions_container__ if p.ticket == ticket), None, ) if not pos: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} self.__positions_container__.remove(pos) deal_ticket = self.__generate_deal_ticket() self.__deals_history_container__.append( self.TradeDeal( ticket=deal_ticket, order=0, time=ts, time_msc=msc, type=order_type, entry=self.mt5_instance.DEAL_ENTRY_OUT, magic=request.get("magic", 0), position_id=pos.ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price=price, commission=self.__calc_commission(), swap=0, profit=0, fee=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) ) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "deal": deal_ticket, }
しかし、ポジション決済のリクエストをすべて無条件では受け入れられません。リクエスト内容の検証が必要です。
現時点では、取引を実行してコンテナからポジションを削除する前に、主に2点を確認する必要があります。
- リクエストに有効な価格が含まれているかどうかを確認します。MetaTrader 5では、買いポジションはBid価格で決済され、売りポジションはAsk価格で決済されます。
- リクエスト内で指定された注文タイプが、既存ポジションと反対方向であるかどうかを確認します。つまり、既存の買いポジション(ORDER_TYPE_BUY)に対しては、決済リクエストとしてORDER_TYPE_SELLが指定されている必要があります。
if action == self.mt5_instance.TRADE_ACTION_DEAL: # ---------- CLOSE POSITION ---------- ticket = request.get("position", -1) if ticket != -1: pos = next( (p for p in self.__positions_container__ if p.ticket == ticket), None, ) if not pos: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} # validate position close request if pos.type == order_type: self.__GetLogger().critical("Failed to close an order. Order type must be the opposite") return None if order_type == self.mt5_instance.ORDER_TYPE_BUY: # For a sell order/position if not TradeValidators.price_equal(a=price, b=ticks_info.ask, eps=pow(10, -symbol_info.digits)): self.__GetLogger().critical(f"Failed to close ORDER_TYPE_SELL. Price {price} is not equal to bid {ticks_info.bid}") return None elif order_type == self.mt5_instance.ORDER_TYPE_SELL: # For a buy order/position if not TradeValidators.price_equal(a=price, b=ticks_info.bid, eps=pow(10, -symbol_info.digits)): self.__GetLogger().critical(f"Failed to close ORDER_TYPE_BUY. Price {price} is not equal to bid {ticks_info.bid}") return None self.__positions_container__.remove(pos) deal_ticket = self.__generate_deal_ticket() self.__deals_history_container__.append( self.TradeDeal( ticket=deal_ticket, order=0, time=ts, time_msc=msc, type=order_type, entry=self.mt5_instance.DEAL_ENTRY_OUT, magic=request.get("magic", 0), position_id=pos.ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price=price, commission=self.__calc_commission(), swap=0, profit=0, fee=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) ) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "deal": deal_ticket, }
IV:ポジションの修正
ポジションを修正するには、ストップロスとテイクプロフィットの2つの情報にのみに注目します。
elif action == self.mt5_instance.TRADE_ACTION_SLTP: ticket = request.get("position", -1) pos = next((p for p in self.__positions_container__ if p.ticket == ticket), None) if not pos: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} # --- Correct reference prices --- entry_price = pos.price_open market_price = ticks_info.bid if pos.type == self.mt5_instance.POSITION_TYPE_BUY else ticks_info.ask # --- Validate SL / TP relative to ENTRY --- if sl > 0: if not trade_validators.is_valid_sl(entry=entry_price, sl=sl, order_type=pos.type): return None if tp > 0: if not trade_validators.is_valid_tp(entry=entry_price, tp=tp, order_type=pos.type): return None # --- Validate freeze level against MARKET --- if sl > 0: if not trade_validators.is_valid_freeze_level(entry=market_price, stop_price=sl, order_type=pos.type): return None if tp > 0: if not trade_validators.is_valid_freeze_level(entry=market_price, stop_price=tp, order_type=pos.type): return None # --- APPLY MODIFICATION --- idx = self.__positions_container__.index(pos) updated_pos = pos._replace( sl=sl, tp=tp, time_update=ts, time_update_msc=msc ) self.__positions_container__[idx] = updated_pos return {"retcode": self.mt5_instance.TRADE_RETCODE_DONE}
V:未決注文の削除
単にコンテナ配列から注文を削除する処理です。追加の検証は不要です。
if action == self.mt5_instance.TRADE_ACTION_REMOVE: ticket = request.get("order", -1) self.__orders_container__ = [ o for o in self.__orders_container__ if o.ticket != ticket ] return {"retcode": self.mt5_instance.TRADE_RETCODE_DONE}
VI:未決注文の修正
未決注文を変更するには、開始価格、ストップロス、テイクプロフィット、有効期限、ストップリミットの5つの重要な項目に注目します。
elif action == self.mt5_instance.TRADE_ACTION_SLTP: ticket = request.get("position", -1) pos = next((p for p in self.__positions_container__ if p.ticket == ticket), None) if not pos: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} # --- Correct reference prices --- entry_price = pos.price_open market_price = ticks_info.bid if pos.type == self.mt5_instance.POSITION_TYPE_BUY else ticks_info.ask # --- Validate SL / TP relative to ENTRY --- if sl > 0: if not trade_validators.is_valid_sl(entry=entry_price, sl=sl, order_type=pos.type): return None if tp > 0: if not trade_validators.is_valid_tp(entry=entry_price, tp=tp, order_type=pos.type): return None # --- Validate freeze level against MARKET --- if sl > 0: if not trade_validators.is_valid_freeze_level(entry=market_price, stop_price=sl, order_type=pos.type): return None if tp > 0: if not trade_validators.is_valid_freeze_level(entry=market_price, stop_price=tp, order_type=pos.type): return None # --- APPLY MODIFICATION --- idx = self.__positions_container__.index(pos) updated_pos = pos._replace( sl=sl, tp=tp, time_update=ts, time_update_msc=msc ) self.__positions_container__[idx] = updated_pos return {"retcode": self.mt5_instance.TRADE_RETCODE_DONE}
しかし、そもそもリクエストの認証情報を検証するための基本的な仕組みが存在しなければ、これらの動作はすべて誤りとなります。
本連載の最初に実装したシミュレーターでは、注文の認証情報を体系的に検証する方法が確立されていませんでした。今回は、その処理を専用に担当するクラスを実装することで、この点を改善します。
取引検証クラス
ご存じのとおり、MetaTrader 5はすべてのリクエストを無条件に受け入れるわけではありません。無効な認証情報が含まれていないかを検証し、問題がある場合はエラーを発生させ、そのような注文は拒否されます。
これらの認証情報は、特定の金融商品の仕様、口座タイプ、ブローカーの要件、場合によってはMetaTrader 5クライアント側の制限に基づいて検証されます。
I:ロットサイズの有効性確認
MetaTrader 5で受け入れられるロットサイズは、以下の条件を満たす必要があります。
- 特定の金融商品における最小許容ロットサイズ以上であること
- 同じ金融商品における最大許容ロットサイズ以下であること
- 取引量のステップサイズの倍数であること
validators.pyの内部
class TradeValidators: def __init__(self, symbol_info: namedtuple, ticks_info: any, logger: any, mt5_instance: mt5=mt5): self.symbol_info = symbol_info self.ticks_info = ticks_info self.logger = logger self.mt5_instance = mt5_instance def is_valid_lotsize(self, lotsize: float) -> bool: # Validate lotsize if lotsize < self.symbol_info.volume_min: # check if the received lotsize is smaller than minimum accepted lot of a symbol self.logger.info(f"Trade validation failed: lotsize ({lotsize}) is less than minimum allowed ({self.symbol_info.volume_min})") return False if lotsize > self.symbol_info.volume_max: # check if the received lotsize is greater than the maximum accepted lot self.logger.info(f"Trade validation failed: lotsize ({lotsize}) is greater than maximum allowed ({self.symbol_info.volume_max})") return False step_count = lotsize / self.symbol_info.volume_step if abs(step_count - round(step_count)) > 1e-7: # check if the stoploss is a multiple of the step size self.logger.info(f"Trade validation failed: lotsize ({lotsize}) must be a multiple of step size ({self.symbol_info.volume_step})") return False return True
II:新規ポジションを建てるための十分な資金の確認
MetaTrader 5ターミナルは、新しいポジションを保有するために必要な余剰証拠金が口座に十分に存在しているかどうかを確認します。
以下に、このタスクに対応する同様の関数を示します。
def is_there_enough_money(self, margin_required: float, free_margin: float) -> bool: if margin_required < 0: self.logger.info("Trade validation failed: Cannot calculate margin requirements") return False # Check free margin if margin_required > free_margin: self.logger.info(f'Trade validation failed: Not enough money to open trade. ' f'Required: {margin_required:.2f}, ' f'Free margin: {free_margin:.2f}') return False return True
III:エントリーの有効性確認
買いポジションの場合、価格はAsk価格と等しくなければならず、売りポジションの場合、価格はBid価格と等しくなければなりません。この確認はポジションのみを対象としています。
def is_valid_entry(self, price: float, order_type: int) -> bool: eps = pow(10, -self.symbol_info.digits) if order_type == self.mt5_instance.ORDER_TYPE_BUY: # BUY if not self.price_equal(a=price, b=self.ticks_info.ask, eps=eps): self.logger.info(f"Trade validation failed: Buy price {price} != ask {self.ticks_info.ask}") return False elif order_type == self.mt5_instance.ORDER_TYPE_SELL: # SELL if not self.price_equal(a=price, b=self.ticks_info.bid, eps=eps): self.logger.info(f"Trade validation failed: Sell price {price} != bid {self.ticks_info.bid}") return False else: self.logger.error("Unknown MetaTrader 5 position type") return False return True
IV:ストップロス価格とテイクプロフィット価格が市場価格に近すぎないことの確認
すべての銘柄には、ストップロスおよびテイクプロフィットの値を市場価格からどれだけ離して設定する必要があるかを示す最小距離が定義されています。
この最小距離はSYMBOL_TRADE_STOPS_LEVELと呼ばれます。
def is_valid_stops_level(self, entry: float, stop_price: float, stops_type: str='') -> bool: point = self.symbol_info.point stop_level = self.symbol_info.trade_stops_level * point distance = abs(entry-stop_price) if stop_price <= 0: return True if distance < stop_level: self.logger.info(f"{'Either SL or TP' if stops_type=='' else stops_type} is too close to the market. Min allowed distance = {stop_level}") return False return True
V:ストップロス値とテイクプロフィット値の有効性確認
買い注文の場合、ストップロスは建値より下に設定し、テイクプロフィットは建値より上に設定する必要があります。
売り注文の場合は逆に、テイクプロフィットは建値より下に設定し、ストップロスは建値より上に設定する必要があります。
def is_valid_sl(self, entry: float, sl: float, order_type: int) -> bool: if not self.is_valid_stops_level(entry, sl, "Stoploss"): # check for stops levels return False if sl > 0: if order_type in self.BUY_ACTIONS: # buy action if sl >= entry: self.logger.info(f"Trade validation failed: Buy-based order's stop loss ({sl}) must be below order opening price ({entry})") return False elif order_type in self.SELL_ACTIONS: # sell action if sl <= entry: self.logger.info(f"Trade validation failed: Sell-based order's stop loss ({sl}) must be above order opening price ({entry})") return False else: self.logger.error("Unknown MetaTrader 5 order type") return False return True def is_valid_tp(self, entry: float, tp: float, order_type: int) -> bool: if not self.is_valid_stops_level(entry, tp, "Takeprofit"): # check for stops and freeze levels return False if tp > 0: if order_type in self.BUY_ACTIONS: # buy position if tp <= entry: self.logger.info(f"Trade validation failed: {self.ORDER_TYPES_MAP[order_type]} take profit ({tp}) must be above order opening price ({entry})") return False elif order_type in self.SELL_ACTIONS: # sell position if tp >= entry: self.logger.info(f"Trade validation failed: {self.ORDER_TYPES_MAP[order_type]} take profit ({tp}) must be below order opening price ({entry})") return False else: self.logger.error("Unknown MetaTrader 5 order type") return False return True
VI:銘柄ごとの最大ロットサイズに達していないことの確認
一部のブローカーおよび一部の銘柄では、新規注文の個別ロットサイズや、ポジション全体の合計ロットサイズに対して上限が設定されています。
def is_symbol_volume_reached(self, symbol_volume: float, volume_limit: float) -> bool: """Checks if the maximum allowed volume is reached for a particular instrument Returns: bool: True if the condition is reached and False when it is not. """ if symbol_volume >= volume_limit and volume_limit > 0: self.logger.critical(f"Symbol Volume limit of {volume_limit} is reached!") return True return False
VII:注文数の上限に達していないことの確認
口座によっては、同時に保有できる保留注文の数に制限が設けられている場合があります。MetaTrader 5プラットフォームが実際の口座ルール違反を許容しないのと同様に、シミュレーション側でも同じ制約を再現し、ルール違反が起こらないように確認する必要があります。
def is_max_orders_reached(self, open_orders: int, ac_limit_orders: int) -> bool: """Checks whether the maximum number of orders for the account is reached Args: open_orders (int): The number of opened orders ac_limit_orders (int): Maximum number of orders allowed for the account Returns: bool: True if the threshold is reached, otherwise, it returns false. """ if open_orders >= ac_limit_orders and ac_limit_orders > 0: self.logger.critical(f"Pending Orders limit of {ac_limit_orders} is reached!") return True return False
VIII:凍結レベルの確認
| 注文/ポジションの種類 | アクティベーション価格 | チェック |
|---|---|---|
| 買い指値注文 | Ask | Ask-OpenPrice >= SYMBOL_TRADE_FREEZE_LEVEL |
| 買い逆指値注文 | Ask | OpenPrice-Ask >= SYMBOL_TRADE_FREEZE_LEVEL |
| 売り指値注文 | Bid | OpenPrice-Bid >= SYMBOL_TRADE_FREEZE_LEVEL |
| 売り逆指値注文 | Bid | Bid-OpenPrice >= SYMBOL_TRADE_FREEZE_LEVEL |
| 買いポジション | Bid | TakeProfit-Bid >= SYMBOL_TRADE_FREEZE_LEVEL Bid-StopLoss >= SYMBOL_TRADE_FREEZE_LEVEL |
| 売りポジション | Ask | Ask-TakeProfit >= SYMBOL_TRADE_FREEZE_LEVEL StopLoss-Ask >= SYMBOL_TRADE_FREEZE_LEVEL |
def is_valid_freeze_level(self, entry: float, stop_price: float, order_type: int) -> bool: """ Check SYMBOL_TRADE_FREEZE_LEVEL for pending orders and open positions. """ freeze_level = self.symbol_info.trade_freeze_level if freeze_level <= 0: return True # No freeze restriction point = self.symbol_info.point freeze_distance = freeze_level * point bid = self.ticks_info.bid ask = self.ticks_info.ask def log_fail(msg: str, dist: float): self.logger.info( f"{msg} | distance={dist/point:.1f} pts < " f"freeze_level={freeze_level} pts" ) # ---------------- Pending Orders ---------------- if order_type == self.mt5_instance.ORDER_TYPE_BUY_LIMIT: dist = ask - entry if dist < freeze_distance: log_fail("BuyLimit cannot be modified: Ask - OpenPrice", dist) return False return True if order_type == self.mt5_instance.ORDER_TYPE_SELL_LIMIT: dist = entry - bid if dist < freeze_distance: log_fail("SellLimit cannot be modified: OpenPrice - Bid", dist) return False return True if order_type == self.mt5_instance.ORDER_TYPE_BUY_STOP: dist = entry - ask if dist < freeze_distance: log_fail("BuyStop cannot be modified: OpenPrice - Ask", dist) return False return True if order_type == self.mt5_instance.ORDER_TYPE_SELL_STOP: dist = bid - entry if dist < freeze_distance: log_fail("SellStop cannot be modified: Bid - OpenPrice", dist) return False return True # ---------------- Open Positions (SL / TP modification) ---------------- # Buy position if order_type == self.mt5_instance.ORDER_TYPE_BUY: if stop_price <= 0: return True if stop_price < entry: # StopLoss dist = bid - stop_price if dist < freeze_distance: log_fail("Buy position SL cannot be modified: Bid - SL", dist) return False else: # TakeProfit dist = stop_price - bid if dist < freeze_distance: log_fail("Buy position TP cannot be modified: TP - Bid", dist) return False return True # Sell position if order_type == self.mt5_instance.ORDER_TYPE_SELL: if stop_price <= 0: return True if stop_price > entry: # StopLoss dist = stop_price - ask if dist < freeze_distance: log_fail("Sell position SL cannot be modified: SL - Ask", dist) return False else: # TakeProfit dist = ask - stop_price if dist < freeze_distance: log_fail("Sell position TP cannot be modified: Ask - TP", dist) return False return True self.logger.error("Unknown MetaTrader 5 order type") return False
order_send内のすべてのバリデーター(TL;DR)
これらすべての関数はvalidators.pyファイル内にあるTradeValidatorsというクラスにまとめられており、order_send関数に対して適用されます。以下に、これらの要素がどのように組み合わさるかを示します。
def order_send(self, request: dict): """ Sends a request to perform a trading operation from the terminal to the trade server. The function is similar to OrderSend in MQL5. """ # ----------------------------------------------------- if not self.IS_TESTER: result = self.mt5_instance.order_send(request) if result is None or result.retcode != self.mt5_instance.TRADE_RETCODE_DONE: self.__GetLogger().warning(f"MT5 failed: {self.mt5_instance.last_error()}") return None return result # -------------------- Extract request ----------------------------- action = request.get("action") order_type = request.get("type") symbol = request.get("symbol") volume = float(request.get("volume", 0)) price = float(request.get("price", 0)) sl = float(request.get("sl", 0)) tp = float(request.get("tp", 0)) ticket = int(request.get("ticket", -1)) ticks_info = self.tick_cache[symbol] symbol_info = self.symbol_info(symbol) ac_info = self.account_info() now = ticks_info.time ts = int(now) msc = int(now * 1000) if order_type not in self.ORDER_TYPES: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} trade_validators = TradeValidators(symbol_info=symbol_info, ticks_info=ticks_info, logger=self.__GetLogger(), mt5_instance=self.mt5_instance) # -------------------- REMOVE pending order ------------------------ if action == self.mt5_instance.TRADE_ACTION_REMOVE: ticket = request.get("order", -1) self.__orders_container__ = [ o for o in self.__orders_container__ if o.ticket != ticket ] return {"retcode": self.mt5_instance.TRADE_RETCODE_DONE} # --------------------- PENDING order -------------------------- if action == self.mt5_instance.TRADE_ACTION_PENDING: if trade_validators.is_max_orders_reached(open_orders=len(self.__orders_container__), ac_limit_orders=ac_info.limit_orders): return None if not trade_validators.is_valid_sl(entry=price, sl=sl, order_type=order_type) or not trade_validators.is_valid_tp(entry=price, tp=tp, order_type=order_type): return None total_volume = sum([pos.volume for pos in self.__positions_container__]) + sum([order.volume for order in self.__orders_container__]) if trade_validators.is_symbol_volume_reached(symbol_volume=total_volume, volume_limit=symbol_info.volume_limit): return None order_ticket = self.__generate_order_ticket() order = self.TradeOrder( ticket=order_ticket, time_setup=ts, time_setup_msc=msc, time_done=0, time_done_msc=0, time_expiration=request.get("expiration", 0), type=order_type, type_time=request.get("type_time", 0), type_filling=request.get("type_filling", 0), state=self.mt5_instance.ORDER_STATE_PLACED, magic=request.get("magic", 0), position_id=0, position_by_id=0, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume_initial=volume, volume_current=volume, price_open=price, sl=sl, tp=tp, price_current=price, price_stoplimit=request.get("price_stoplimit", 0), symbol=symbol, comment=request.get("comment", ""), external_id="", ) self.__orders_container__.append(order) self.__orders_history_container__.append(order) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "order": order_ticket, } # ------------------ MARKET DEAL (open or close) ------------------ if action == self.mt5_instance.TRADE_ACTION_DEAL: # ---------- CLOSE POSITION ---------- ticket = request.get("position", -1) if ticket != -1: pos = next( (p for p in self.__positions_container__ if p.ticket == ticket), None, ) if not pos: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} # validate position close request if pos.type == order_type: self.__GetLogger().critical("Failed to close an order. Order type must be the opposite") return None if order_type == self.mt5_instance.ORDER_TYPE_BUY: # For a sell order/position if not TradeValidators.price_equal(a=price, b=ticks_info.ask, eps=pow(10, -symbol_info.digits)): self.__GetLogger().critical(f"Failed to close ORDER_TYPE_SELL. Price {price} is not equal to bid {ticks_info.bid}") return None elif order_type == self.mt5_instance.ORDER_TYPE_SELL: # For a buy order/position if not TradeValidators.price_equal(a=price, b=ticks_info.bid, eps=pow(10, -symbol_info.digits)): self.__GetLogger().critical(f"Failed to close ORDER_TYPE_BUY. Price {price} is not equal to bid {ticks_info.bid}") return None self.__positions_container__.remove(pos) deal_ticket = self.__generate_deal_ticket() self.__deals_history_container__.append( self.TradeDeal( ticket=deal_ticket, order=0, time=ts, time_msc=msc, type=order_type, entry=self.mt5_instance.DEAL_ENTRY_OUT, magic=request.get("magic", 0), position_id=pos.ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price=price, commission=self.__calc_commission(), swap=0, profit=0, fee=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) ) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "deal": deal_ticket, } # ---------- OPEN POSITION ---------- # validate new stops if not trade_validators.is_valid_sl(entry=price, sl=sl, order_type=order_type): return None if not trade_validators.is_valid_tp(entry=price, tp=tp, order_type=order_type): return None # validate the lotsize if not trade_validators.is_valid_lotsize(lotsize=volume): return None total_volume = sum([pos.volume for pos in self.__positions_container__]) + sum([order.volume for order in self.__orders_container__]) if trade_validators.is_symbol_volume_reached(symbol_volume=total_volume, volume_limit=symbol_info.volume_limit): return None if not trade_validators.is_there_enough_money(margin_required=self.order_calc_margin(order_type=order_type, symbol=symbol, volume=volume, price=price), free_margin=ac_info.margin_free): return None position_ticket = self.__generate_position_ticket() order_ticket = self.__generate_order_ticket() deal_ticket = self.__generate_deal_ticket() position = self.TradePosition( ticket=position_ticket, time=ts, time_msc=msc, time_update=ts, time_update_msc=msc, type=order_type, magic=request.get("magic", 0), identifier=position_ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price_open=price, sl=sl, tp=tp, price_current=price, swap=0, profit=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) self.__positions_container__.append(position) self.__deals_history_container__.append( self.TradeDeal( ticket=deal_ticket, order=order_ticket, time=ts, time_msc=msc, type=order_type, entry=self.mt5_instance.DEAL_ENTRY_IN, magic=request.get("magic", 0), position_id=position_ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price=price, commission=self.__calc_commission(), swap=0, profit=0, fee=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) ) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "deal": deal_ticket, "order": order_ticket, "position": position_ticket, } elif action == self.mt5_instance.TRADE_ACTION_MODIFY: # Modifying pending orders ticket = request.get("order", -1) order = next( (o for o in self.__orders_container__ if o.ticket == ticket), None, ) if not order: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} # validate new stops if not trade_validators.is_valid_freeze_level(entry=price, stop_price=sl, order_type=order_type): return None if not trade_validators.is_valid_freeze_level(entry=price, stop_price=tp, order_type=order_type): return None # Modify ONLY allowed fields order.price_open = price order.sl = sl order.tp = tp order.time_expiration = request.get("expiration", order.time_expiration) order.price_stoplimit = request.get("price_stoplimit", order.price_stoplimit) return {"retcode": self.mt5_instance.TRADE_RETCODE_DONE} elif action == self.mt5_instance.TRADE_ACTION_SLTP: # Modifying an open position ticket = request.get("position", -1) pos = next( (p for p in self.__positions_container__ if p.ticket == ticket), None, ) if not pos: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} # Check for valid stoplosses and TPs ensuring they are not too close to the market if pos.type == self.mt5_instance.ORDER_TYPE_BUY: if not trade_validators.is_valid_sl(entry=ticks_info.bid, sl=sl, order_type=order_type) or not trade_validators.is_valid_tp(entry=ticks_info.bid, tp=tp, order_type=order_type): return None elif pos.type == self.mt5_instance.ORDER_TYPE_SELL: if not trade_validators.is_valid_sl(entry=ticks_info.ask, sl=sl, order_type=order_type) or not trade_validators.is_valid_tp(entry=ticks_info.ask, tp=tp, order_type=order_type): return None if not trade_validators.is_valid_freeze_level(entry=price, stop_price=sl, order_type=order_type): return None if not trade_validators.is_valid_freeze_level(entry=price, stop_price=sl, order_type=order_type): return None pos.sl = sl pos.tp = tp pos.time_update = ts pos.time_update_msc = msc return {"retcode": self.mt5_instance.TRADE_RETCODE_DONE} return { "retcode": self.mt5_instance.TRADE_RETCODE_INVALID, "comment": "Unsupported trade action", }
シミュレーター内のCTradeクラス
MQL5と同様に、Python-MetaTrader 5モジュールはMetaTrader 5ターミナルと通信するための低レベルインターフェースのように機能します。これまで見てきたように、注文や取引リクエストを送信するには多くの手順が必要であり、そのプロセスは長く煩雑になりがちです。
MQL5にはいわゆる取引クラスが用意されており、これにより取引の開始や管理のためのよりシンプルなインターフェースが提供されています。Pythonでも同様のクラスをすでに作成しているため、ここではそのCTradeクラスをシミュレーターの要件に合わせて調整していきます。
import MetaTrader5 as mt5 from datetime import datetime, timezone import config class CTrade: def __init__(self, simulator, magic_number: int, filling_type_symbol: str, deviation_points: int): self.simulator = simulator self.mt5_instance = simulator.mt5_instance self.magic_number = magic_number self.deviation_points = deviation_points self.filling_type = self._get_type_filling(filling_type_symbol) if self.filling_type == -1: print("Failed to initialize the class, Invalid filling type. Check your symbol") return
クラスにシミュレーターのインスタンスを渡し、そこからいくつかのメソッドを抽出します。これにより、MetaTrader 5から直接メソッドを呼び出すのではなく、前の記事で説明したオーバーライドされたメソッドをクラス側で利用できるようになります。
CTradeクラスには多くのメソッドが用意されていますが、それらはすべてPython-MetaTrader 5モジュールのorder_sendメソッドに依存しています。
このクラスでは、Python-MetaTrader 5モジュールのメソッドを呼び出す代わりに、Simulatorクラスでオーバーライドされたメソッドを呼び出すようにします。
class CTrade: def __init__(self, simulator, magic_number: int, filling_type_symbol: str, deviation_points: int): self.simulator = simulator self.mt5_instance = simulator.mt5_instance self.magic_number = magic_number self.deviation_points = deviation_points self.filling_type = self._get_type_filling(filling_type_symbol) if self.filling_type == -1: print("Failed to initialize the class, Invalid filling type. Check your symbol") return def _get_type_filling(self, symbol): symbol_info = self.simulator.symbol_info(symbol) if symbol_info is None: print(f"Failed to get symbol info for {symbol}") filling_map = { 1: self.mt5_instance.ORDER_FILLING_FOK, 2: self.mt5_instance.ORDER_FILLING_IOC, 4: self.mt5_instance.ORDER_FILLING_BOC, 8: self.mt5_instance.ORDER_FILLING_RETURN } return filling_map.get(symbol_info.filling_mode, f"Unknown Filling type") def __GetLogger(self): if self.simulator.IS_TESTER: return config.tester_logger return config.simulator_logger def position_open(self, symbol: str, volume: float, order_type: int, price: float, sl: float=0.0, tp: float=0.0, comment: str="") -> bool: """ Open a market position (instant execution). Executes either a buy or sell order at the current market price. This is for immediate position opening, not pending orders. Args: symbol: Trading symbol (e.g., "EURUSD", "GBPUSD") volume: Trade volume in lots (e.g., 0.1 for micro lot) order_type: Trade direction (either ORDER_TYPE_BUY or ORDER_TYPE_SELL) price: Execution price. For market orders, this should be the current: - Ask price for BUY orders - Bid price for SELL orders sl: Stop loss price (set to 0.0 to disable) tp: Take profit price (set to 0.0 to disable) comment: Optional order comment (max 31 characters, will be truncated automatically) Returns: bool: True if position was opened successfully, False otherwise """ request = { "action": self.mt5_instance.TRADE_ACTION_DEAL, "symbol": symbol, "volume": volume, "type": order_type, "price": price, "deviation": self.deviation_points, "magic": self.magic_number, "comment": comment, "type_time": self.mt5_instance.ORDER_TIME_GTC, "type_filling": self.filling_type, } if sl > 0.0: request["sl"] = sl if tp > 0.0: request["tp"] = tp if self.simulator.order_send(request) is None: return False self.__GetLogger().info(f"Position Opened successfully!") return True def order_open(self, symbol: str, volume: float, order_type: int, price: float, sl: float = 0.0, tp: float = 0.0, type_time: int = mt5.ORDER_TIME_GTC, expiration: datetime = None, comment: str = "") -> bool: """ Opens a pending order with full control over order parameters. Args: symbol: Trading symbol (e.g., "EURUSD") volume: Order volume in lots order_type: Order type (ORDER_TYPE_BUY_LIMIT, ORDER_TYPE_SELL_STOP, etc.) price: Activation price for pending order sl: Stop loss price (0 to disable) tp: Take profit price (0 to disable) type_time: Order expiration type (default: ORDER_TIME_GTC). Possible values: - ORDER_TIME_GTC (Good-Til-Canceled) - ORDER_TIME_DAY (Good for current day) - ORDER_TIME_SPECIFIED (expires at specific datetime) - ORDER_TIME_SPECIFIED_DAY (expires at end of specified day) expiration: Expiration datetime (required for ORDER_TIME_SPECIFIED types) comment: Optional order comment (max 31 characters) Returns: bool: True if order was placed successfully, False otherwise """ # Validate expiration for time-specific orders if type_time in (self.mt5_instance.ORDER_TIME_SPECIFIED, self.mt5_instance.ORDER_TIME_SPECIFIED_DAY) and expiration is None: print(f"Expiration required for order type {type_time}") return False request = { "action": self.mt5_instance.TRADE_ACTION_PENDING, "symbol": symbol, "volume": volume, "type": order_type, "price": price, "sl": sl, "tp": tp, "deviation": self.deviation_points, "magic": self.magic_number, "comment": comment[:31], # MT5 comment max length is 31 chars "type_time": type_time, "type_filling": self.filling_type, } # Add expiration if required if type_time in (self.mt5_instance.ORDER_TIME_SPECIFIED, self.mt5_instance.ORDER_TIME_SPECIFIED_DAY) and expiration is not None: # Convert to broker's expected format (UTC timestamp in milliseconds) expiration_utc = expiration.astimezone(timezone.utc) if expiration.tzinfo else expiration.replace(tzinfo=timezone.utc) request["expiration"] = int(expiration_utc.timestamp() * 1000) # Send order if self.simulator.order_send(request) is None: return False self.__GetLogger().info(f"Order opened successfully!") return True def buy(self, volume: float, symbol: str, price: float, sl: float=0.0, tp: float=0.0, comment: str="") -> bool: """ Opens a buy (market) position. Args: volume: Trade volume (lot size) symbol: Trading symbol (e.g., "EURUSD") price: Execution price sl: Stop loss price (optional, default=0.0) tp: Take profit price (optional, default=0.0) comment: Position comment (optional, default="") Returns: bool: True if order was sent successfully, False otherwise """ return self.position_open(symbol=symbol, volume=volume, order_type=self.mt5_instance.ORDER_TYPE_BUY, price=price, sl=sl, tp=tp, comment=comment)
このモジュールをシミュレーターの要件に合わせて調整したことで、ポジションの新規作成や指値注文の生成が容易になりました。これらのメソッドをテストする前に、クラスに加えられた変更点と、シミュレーターを正しく動作させるために必要な手順を理解しておく必要があります。
前回の記事では、Startというメソッドを紹介しました。このメソッドは、シミュレータークラスのインスタンスをストラテジーテスターモードに設定し、クラス内のすべての要素およびほぼすべてのデータを仮想化します。
def Start(self, IS_TESTER: bool) -> bool: # simulator start self.IS_TESTER = IS_TESTER
このモードをTrueに設定すると、シミュレーターはMetaTrader 5ストラテジーテスターの動作を模倣します。一方でFalseに設定した場合、シミュレーターはMetaTrader 5クライアントに直接依存してすべての情報を取得し、その環境上で取引を実行するため、純粋なシミュレーターとしての動作ではなくなります。このモードは、シミュレーター内での処理がMetaTrader 5クライアント上での挙動と一致するようにするため、テスト目的で導入されました。
その後の変更により、このメソッドは削除されました。現在はデフォルトでこのクラスがストラテジーテスターモード(シミュレーター)として動作します。MetaTrader 5モード(主にデバッグ用途)へ切り替えるには、最終スクリプト実行時に引数「--mt5」を渡す必要があります。
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py --mt5
シミュレーターで取引操作を実行する
シミュレーターの最新バージョンをテストする手順は以下のとおりです(ファイルは記事末尾に添付されています)。
test.pyの内部
01:MetaTrader 5ターミナルの初期化
import MetaTrader5 as mt5 from Trade.Trade import CTrade from datetime import datetime, timedelta import time import pytz from simulator import Simulator, CTrade if not mt5.initialize(): # Initialize MetaTrader5 instance print(f"Failed to Initialize MetaTrader5. Error = {mt5.last_error()}") mt5.shutdown() quit()
02:シミュレーターインスタンスの呼び出し
シミュレータークラスのインスタンスを呼び出し、MetaTrader 5アプリのインスタンス、預金としてラベル付けされたアカウントの残高、およびレバレッジ値を指定します。
sim = Simulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500")
次に、CTradeクラスのシミュレーターインスタンスを使用します。
03:(オプション)CTradeクラスのインスタンス化
symbol = "EURUSD"
timeframe = mt5.TIMEFRAME_H1 m_trade = CTrade(simulator=sim, magic_number=112233, filling_type_symbol=symbol, deviation_points=100)
04:シミュレータへのティックデータの割り当てと抽出
mt5_ticks = mt5.symbol_info_tick(symbol) # tick source sim.TickUpdate(symbol=symbol, tick=mt5_ticks) # very important tick_from_sim = sim.symbol_info_tick(symbol=symbol) # we get ticks back from a class ask = tick_from_sim.ask bid = tick_from_sim.bid
05:最後に、いくつかの取引操作
symbol_info = sim.symbol_info(symbol=symbol) lotsize = symbol_info.volume_min m_trade.buy( volume=lotsize, symbol=symbol, price=ask, sl=ask - 100 * symbol_info.point, tp=ask + 150 * symbol_info.point, comment="Market Buy" ) m_trade.sell( volume=lotsize, symbol=symbol, price=bid, sl=bid + 100 * symbol_info.point, tp=bid - 150 * symbol_info.point, comment="Market Sell" ) buy_limit_price = ask - 200 * symbol_info.point m_trade.buy_limit( volume=lotsize, symbol=symbol, price=buy_limit_price, sl=buy_limit_price - 100 * symbol_info.point, tp=buy_limit_price + 200 * symbol_info.point, comment="Buy Limit" ) sell_limit_price = bid + 200 * symbol_info.point m_trade.sell_limit( volume=lotsize, symbol=symbol, price=sell_limit_price, sl=sell_limit_price + 100 * symbol_info.point, tp=sell_limit_price - 200 * symbol_info.point, comment="Sell Limit" ) buy_stop_price = ask + 150 * symbol_info.point m_trade.buy_stop( volume=lotsize, symbol=symbol, price=buy_stop_price, sl=buy_stop_price - 100 * symbol_info.point, tp=buy_stop_price + 300 * symbol_info.point, comment="Buy Stop" ) sell_stop_price = bid - 150 * symbol_info.point m_trade.sell_stop( volume=lotsize, symbol=symbol, price=sell_stop_price, sl=sell_stop_price + 100 * symbol_info.point, tp=sell_stop_price - 300 * symbol_info.point, comment="Sell Stop" )
これらの未決済ポジションと注文がシミュレーター上に存在するかどうかを確認できます。
print(f"positions in a simulator = {sim.positions_total()}:\n",sim.positions_get()) print(f"orders in a simulator = {sim.orders_total()}:\n", sim.orders_get())
出力(テスターモード):
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py
2026-01-05 15:09:24,504 | INFO | tester | [Trade.py:85 - position_open() ] => Position Opened successfully!
2026-01-05 15:09:24,513 | INFO | tester | [Trade.py:85 - position_open() ] => Position Opened successfully!
2026-01-05 15:09:24,515 | INFO | tester | [Trade.py:148 - order_open() ] => Order opened successfully!
2026-01-05 15:09:24,515 | INFO | tester | [Trade.py:148 - order_open() ] => Order opened successfully!
2026-01-05 15:09:24,515 | INFO | tester | [Trade.py:148 - order_open() ] => Order opened successfully!
2026-01-05 15:09:24,515 | INFO | tester | [Trade.py:148 - order_open() ] => Order opened successfully!
positions in a simulator = 2:
(TradePosition(ticket=113127357728313068862, time=1767622158, time_msc=1767622158000, time_update=1767622158, time_update_msc=1767622158000, type=0, magic=112233, identifier=113127357728313068862, reason=3, volume=0.01, price_open=1.16792, sl=1.1669200000000002, tp=1.1694200000000001, price_current=1.16792, swap=0, profit=0, symbol='EURUSD', comment='Market Buy', external_id=''),
TradePosition(ticket=113127357728890995262, time=1767622158, time_msc=1767622158000, time_update=1767622158, time_update_msc=1767622158000, type=1, magic=112233, identifier=113127357728890995262, reason=3, volume=0.01, price_open=1.16792, sl=1.16892, tp=1.16642, price_current=1.16792, swap=0, profit=0, symbol='EURUSD', comment='Market Sell', external_id=''))
orders in a simulator = 4:
(TradeOrder(ticket=113127357729019468800, time_setup=1767622158, time_setup_msc=1767622158000, time_done=0, time_done_msc=0, time_expiration=0, type=2, type_time=0, type_filling=1, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16592, sl=1.1649200000000002, tp=1.16792, price_current=1.16592, price_stoplimit=0, symbol='EURUSD', comment='Buy Limit', external_id=''),
TradeOrder(ticket=113127357729019468835, time_setup=1767622158, time_setup_msc=1767622158000, time_done=0, time_done_msc=0, time_expiration=0, type=3, type_time=0, type_filling=1, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16992, sl=1.17092, tp=1.16792, price_current=1.16992, price_stoplimit=0, symbol='EURUSD', comment='Sell Limit', external_id=''),
TradeOrder(ticket=113127357729019468836, time_setup=1767622158, time_setup_msc=1767622158000, time_done=0, time_done_msc=0, time_expiration=0, type=4, type_time=0, type_filling=1, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.1694200000000001, sl=1.1684200000000002, tp=1.17242, price_current=1.1694200000000001, price_stoplimit=0, symbol='EURUSD', comment='Buy Stop', external_id=''),
TradeOrder(ticket=113127357729019468803, time_setup=1767622158, time_setup_msc=1767622158000, time_done=0, time_done_msc=0, time_expiration=0, type=5, type_time=0, type_filling=1, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16642, sl=1.16742, tp=1.1634200000000001, price_current=1.16642, price_stoplimit=0, symbol='EURUSD', comment='Sell Stop', external_id='')) 出力(MetaTrader 5モード):
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py --mt5
2026-01-05 15:09:29,171 | INFO | simulator | [Trade.py:85 - position_open() ] => Position Opened successfully!
2026-01-05 15:09:30,270 | INFO | simulator | [Trade.py:85 - position_open() ] => Position Opened successfully!
2026-01-05 15:09:31,110 | INFO | simulator | [Trade.py:148 - order_open() ] => Order opened successfully!
2026-01-05 15:09:31,711 | INFO | simulator | [Trade.py:148 - order_open() ] => Order opened successfully!
2026-01-05 15:09:33,000 | INFO | simulator | [Trade.py:148 - order_open() ] => Order opened successfully!
2026-01-05 15:09:33,952 | INFO | simulator | [Trade.py:148 - order_open() ] => Order opened successfully!
positions in a simulator = 2:
(TradePosition(ticket=1393244663, time=1767622166, time_msc=1767622166713, time_update=1767622166, time_update_msc=1767622166713, type=0, magic=112233, identifier=1393244663, reason=3, volume=0.01, price_open=1.16791, sl=1.1669100000000001, tp=1.16941, price_current=1.16791, swap=0.0, profit=0.0, symbol='EURUSD', comment='Market Buy', external_id=''),
TradePosition(ticket=1393244666, time=1767622167, time_msc=1767622167817, time_update=1767622167, time_update_msc=1767622167817, type=1, magic=112233, identifier=1393244666, reason=3, volume=0.01, price_open=1.16791, sl=1.16891, tp=1.16641, price_current=1.16791, swap=0.0, profit=0.0, symbol='EURUSD', comment='Market Sell', external_id=''))
orders in a simulator = 4:
(TradeOrder(ticket=1393244672, time_setup=1767622168, time_setup_msc=1767622168661, time_done=0, time_done_msc=0, time_expiration=0, type=2, type_time=0, type_filling=2, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16591, sl=1.16491, tp=1.16791, price_current=1.16791, price_stoplimit=0.0, symbol='EURUSD', comment='Buy Limit', external_id=''),
TradeOrder(ticket=1393244676, time_setup=1767622169, time_setup_msc=1767622169494, time_done=0, time_done_msc=0, time_expiration=0, type=3, type_time=0, type_filling=2, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16991, sl=1.1709100000000001, tp=1.16791, price_current=1.16791, price_stoplimit=0.0, symbol='EURUSD', comment='Sell Limit', external_id=''),
TradeOrder(ticket=1393244679, time_setup=1767622170, time_setup_msc=1767622170093, time_done=0, time_done_msc=0, time_expiration=0, type=4, type_time=0, type_filling=2, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16941, sl=1.16841, tp=1.17241, price_current=1.16791, price_stoplimit=0.0, symbol='EURUSD', comment='Buy Stop', external_id=''),
TradeOrder(ticket=1393244687, time_setup=1767622171, time_setup_msc=1767622171748, time_done=0, time_done_msc=0, time_expiration=0, type=5, type_time=0, type_filling=2, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16641, sl=1.16741, tp=1.16341, price_current=1.16791, price_stoplimit=0.0, symbol='EURUSD', comment='Sell Stop', external_id=''))
シミュレーターでの注文とポジションの管理
シミュレータークラスのコンテナである配列内にポジション情報が格納されている場合、特定の動作や条件を検出したり、配列を変更したり、さらには配列を閉じたりするなど、それらの配列に対して様々な操作を実行できます。
01:ポジションの決済
2つの異なるポジションを開き、1つを閉じましょう。
m_trade.buy(volume=lotsize, symbol=symbol, price=ask, comment="buy pos") m_trade.sell(volume=lotsize, symbol=symbol, price=bid, comment="sell pos") print(f"positions in a simulator = {sim.positions_total()}:\n",sim.positions_get()) positions = sim.positions_get() for pos in positions: if pos.symbol == symbol and pos.type == sim.mt5_instance.POSITION_TYPE_BUY: # close a buy position m_trade.position_close(ticket=pos.ticket, deviation=10) print("positions remaining: ", sim.positions_get())
出力
2026-01-05 15:54:50,114 | INFO | tester | [Trade.py:85 - position_open() ] => Position Opened successfully! 2026-01-05 15:54:50,114 | INFO | tester | [Trade.py:85 - position_open() ] => Position Opened successfully! positions in a simulator = 2: (TradePosition(ticket=113127532167305337632, time=1767624887, time_msc=1767624887000, time_update=1767624887, time_update_msc=1767624887000, type=0, magic=112233, identifier=113127532167305337632, reason=3, volume=0.01, price_open=1.16743, sl=0.0, tp=0.0, price_current=1.16743, swap=0, profit=0, symbol='EURUSD', comment='buy pos', external_id=''), TradePosition(ticket=113127532167305337651, time=1767624887, time_msc=1767624887000, time_update=1767624887, time_update_msc=1767624887000, type=1, magic=112233, identifier=113127532167305337651, reason=3, volume=0.01, price_open=1.16743, sl=0.0, tp=0.0, price_current=1.16743, swap=0, profit=0, symbol='EURUSD', comment='sell pos', external_id='')) 2026-01-05 15:54:50,114 | INFO | tester | [Trade.py:397 - position_close() ] => Position 113127532167305337632 closed successfully! positions remaining: (TradePosition(ticket=113127532167305337651, time=1767624887, time_msc=1767624887000, time_update=1767624887, time_update_msc=1767624887000, type=1, magic=112233, identifier=113127532167305337651, reason=3, volume=0.01, price_open=1.16743, sl=0.0, tp=0.0, price_current=1.16743, swap=0, profit=0, symbol='EURUSD', comment='sell pos', external_id=''),)
買いポジションのみが決済されました。
02:ポジションの変更
ポジションを決済する場合と同様に、変更依頼を送信する前に、いずれかを選択する必要があります。
m_trade.buy(volume=lotsize, symbol=symbol, price=ask, comment="buy pos") m_trade.sell(volume=lotsize, symbol=symbol, price=bid, comment="sell pos") print(f"positions in a simulator = {sim.positions_total()}:\n",sim.positions_get()) positions = sim.positions_get() for pos in positions: if pos.sl == 0: if pos.type == sim.mt5_instance.POSITION_TYPE_BUY: m_trade.position_modify(ticket=pos.ticket, sl=pos.price_open - 100 * symbol_info.point, tp=pos.tp) if pos.type == sim.mt5_instance.POSITION_TYPE_SELL: m_trade.position_modify(ticket=pos.ticket, sl=pos.price_open + 100 * symbol_info.point, tp=pos.tp) print("positions after modification\n: ", sim.positions_get())
以下は出力例です。
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py 2026-01-05 17:19:11,604 | INFO | tester | [Trade.py:85 - position_open() ] => Position Opened successfully! 2026-01-05 17:19:11,604 | INFO | tester | [Trade.py:85 - position_open() ] => Position Opened successfully! positions in a simulator = 2: (TradePosition(ticket=113127856102580262436, time=1767629948, time_msc=1767629948000, time_update=1767629948, time_update_msc=1767629948000, type=0, magic=112233, identifier=113127856102580262436, reason=3, volume=0.01, price_open=1.16789, sl=0.0, tp=0.0, price_current=1.16789, swap=0, profit=0, symbol='EURUSD', comment='buy pos', external_id=''), TradePosition(ticket=113127856102710534416, time=1767629948, time_msc=1767629948000, time_update=1767629948, time_update_msc=1767629948000, type=1, magic=112233, identifier=113127856102710534416, reason=3, volume=0.01, price_open=1.16789, sl=0.0, tp=0.0, price_current=1.16789, swap=0, profit=0, symbol='EURUSD', comment='sell pos', external_id='')) 2026-01-05 17:19:11,604 | INFO | tester | [Trade.py:469 - position_modify() ] => Position 113127856102580262436 modified successfully! 2026-01-05 17:19:11,606 | INFO | tester | [Trade.py:469 - position_modify() ] => Position 113127856102710534416 modified successfully! positions after modification : (TradePosition(ticket=113127856102580262436, time=1767629948, time_msc=1767629948000, time_update=1767629948, time_update_msc=1767629948000, type=0, magic=112233, identifier=113127856102580262436, reason=3, volume=0.01, price_open=1.16789, sl=1.1668900000000002, tp=0.0, price_current=1.16789, swap=0, profit=0, symbol='EURUSD', comment='buy pos', external_id=''), TradePosition(ticket=113127856102710534416, time=1767629948, time_msc=1767629948000, time_update=1767629948, time_update_msc=1767629948000, type=1, magic=112233, identifier=113127856102710534416, reason=3, volume=0.01, price_open=1.16789, sl=1.16889, tp=0.0, price_current=1.16789, swap=0, profit=0, symbol='EURUSD', comment='sell pos', external_id=''))
03:未決注文の処理
未決注文を変更および削除する方法は以下のとおりです。
m_trade.buy_stop(symbol=symbol, volume=symbol_info.volume_min, price=ask+500*symbol_info.point) for order in sim.orders_get(): print("order curr price: ", order.price_open) m_trade.order_modify(ticket=order.ticket, price=order.price_open+10*symbol_info.point, sl=order.sl, tp=order.tp) print("order moved 10 points upward", order.price_open) if m_trade.order_delete(ticket=order.ticket) is None: continue print("orders remaining: ", sim.orders_total())
以下は出力例です。
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py 2026-01-05 17:43:15,677 | INFO | tester | [Trade.py:148 - order_open() ] => Order opened successfully! order curr price: 1.17281 2026-01-05 17:43:15,677 | INFO | tester | [Trade.py:536 - order_modify() ] => Order 113127948523388723206 modified successfully! order moved 10 points upward 1.17291 2026-01-05 17:43:15,677 | INFO | tester | [Trade.py:431 - order_delete() ] => Order 113127948523388723206 deleted successfully! orders remaining: 0
次のステップ
過去の特定期間におけるティックデータを用いて取引操作をシミュレーションできるようになったことで、カスタムシミュレーターの完成は目前に迫っています。現時点で、指定した時間範囲内のすべての利用可能なティックを走査しながらシミュレーションを実行するループを構築するために必要な要素はすべて揃っています。これは、いわゆる戦略テスト、あるいは完全なシミュレーション処理に相当します。
本記事では、あらゆる取引シミュレーターにおいて中核となる要素である「取引リクエストの送信および管理」について解説しました。次回は、これまでに実装してきた内容を統合し、Python上で初めてのストラテジーテストを実行します。
次回をお楽しみに。
ご意見やフィードバックがあれば、GitHubで共有していただけると本プロジェクトの改善に役立ちます:https://github.com/MegaJoctan/PyMetaTester
添付ファイルの表
| ファイル名 | 説明と使用法 |
|---|---|
| Trade/Trade.py | CTradeクラスを含むファイル。取引操作を簡単に実行するためのクラスを提供します。 |
| config.py | プロジェクト全体で再利用される主要な設定値を定義するPython設定ファイルです。 |
| utils.py | 各種処理を補助するためのシンプルなユーティリティ関数(ヘルパー)をまとめたファイルです。 |
| simulator.py | Simulatorというクラスを含むファイルで、シミュレーターのコアロジックがすべてここに実装されています。 |
| test.py | 本記事で解説したすべてのコードおよび関数をテストするためのファイルです。 |
| error_description.py | MetaTrader 5のエラーコードを、人間が読める形式のメッセージに変換する関数を提供します。 |
| requirements.txt | 本プロジェクトで使用するPython依存ライブラリとそのバージョンを定義したファイルです。 |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/20782
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
MQL5でカスタムインジケーターを作成する(第5回):WaveTrend Crossover Evolution:Canvasを用いたフォグ状グラデーション、シグナルバブル、リスク管理
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
ラリー・ウィリアムズの『市場の秘密』(第6回):市場変動を利用したボラティリティブレイクアウトの測定
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索