Python Launches an Extra Terminal: initialize(path=) Is Not Attach
A function named initialize does two things in one call: it connects to a running program, or starts that program itself if none is running. A script intended only to read data ends up controlling the terminal's lifecycle without its author intending it.
1-minute summary
- Symptom / problem
- A script calling initialize(path=) in MT5 starts a second terminal that fights over the data folder and the account.
- Who should read
- Python script authors who only want to read data from MT5.
- Fix in short
- Check the process before calling initialize, pin the account at startup and compare it every cycle, and handle sessions that die silently after a terminal restart.
I have a Python script that collects data from a MetaTrader 5 terminal into a data store every five minutes for historical analysis. Another program on the same machine also uses that terminal. One day, I found a terminal window appearing on its own even though nobody had opened it, overlapping with the instance that the login launcher was about to open. I ended up with two terminal instances using the same data folder and account. The culprit was my own data collection script.
I had always understood the path parameter of initialize() to mean "connect to the terminal located here," which was only half right. The other half is that if no process is running there, the library starts one itself. I have replaced all broker names, server names, account numbers, and paths in this article with fictional ones. This system collects data for historical analysis.
Symptoms: a terminal appears on its own and competes with the intended instance
My original code looked like this, the same as examples commonly found online.
import MetaTrader5 as mt5
TERMINAL = r"C:\terminals\broker-a\terminal64.exe"
# ตั้งใจว่า "ต่อเข้า terminal ตัวนี้" แต่ถ้ามันยังไม่รัน บรรทัดนี้จะเปิดให้เอง
if not mt5.initialize(path=TERMINAL, portable=True):
raise SystemExit(f"init failed: {mt5.last_error()}")
As long as the terminal was already open, this line behaved as I expected: it simply connected. The problem arose in three situations I had not considered. The first was just after the machine booted, when the script ran before the launcher opened the terminal. The second was when I closed the terminal myself to do something else, and the script opened it again on its next run. The third was when the terminal shut down to update.
All three situations had the same result: a terminal opened by the data collection script, rather than the person or program that should own it. If the actual owner then opened another instance, I ended up with two instances pointing to the same portable data folder and logged into the same account.
The problem is not that the library is doing something wrong. This behavior is very convenient for standalone scripts. The problem is that my system has more than one terminal user. Once a data reader can start the terminal, it can also start it at the wrong time, in the wrong order, and alongside an instance someone else is using. The rule I set after that was that only one component on the whole machine could open the terminal: the login launcher, and only when no instance was already running.
Check the process before calling initialize
The most direct fix is to check whether the terminal is running first. If it is not, do not call initialize() at all. I have two ways to check in my setup. The first uses tasklist filtered by image name.
# เวอร์ชันหยาบ: มี terminal64.exe ตัวไหนก็ได้รันอยู่หรือเปล่า
tasklist /fi "imagename eq terminal64.exe" /fo csv
# เวอร์ชันที่บอกได้ว่าเป็นตัวไหน: ดูพาธเต็มของแต่ละ process
Get-Process terminal64 -ErrorAction SilentlyContinue | Select-Object Id, Path
This rough version works when the machine has only one terminal installation. But on a machine with terminals for broker A and broker B in separate folders, both executables are named terminal64.exe. Checking by image name can report "running" even though the running instance belongs to the other broker, and initialize(path=) will still start the intended instance itself. My data collection daemon therefore uses the second approach: comparing the full exe path for each process.
import os, time
import psutil
import MetaTrader5 as mt5
TERMINAL = os.environ.get("MT5_PATH", r"C:\terminals\broker-a\terminal64.exe")
def terminal_running(path):
want = os.path.normcase(os.path.abspath(path))
for p in psutil.process_iter(["name", "exe"]):
try:
exe = p.info.get("exe")
if exe and os.path.normcase(os.path.abspath(exe)) == want:
return True
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
return False
def attach(wait_s=1800):
"""ต่อเข้า terminal ที่รันอยู่แล้วเท่านั้น ไม่มีวันเปิดเอง"""
waited = 0
while not terminal_running(TERMINAL):
if wait_s <= 0:
print("[ERR] terminal ไม่ได้รันอยู่ สคริปต์นี้ไม่เปิดให้")
return False
if waited % 300 == 0:
print(f"[WARN] รอ terminal ({waited}s)")
time.sleep(10)
waited += 10
if waited >= wait_s:
print(f"[ERR] รอครบ {wait_s}s แล้ว terminal ยังไม่มา")
return False
if not mt5.initialize(path=TERMINAL):
print("[ERR] attach ไม่สำเร็จ:", mt5.last_error())
return False
return True
Both normcase and abspath are necessary because Windows paths are case-insensitive, and a path from an environment variable may be written differently from the path reported by the system even when both refer to the same file.
wait_s = 0
Suitable for scripts people run manually at the screen. If the terminal is not open yet, the script exits with a message explaining why. The person running it knows immediately that they need to open the terminal first. Nothing is left waiting in the background.
wait_s = 1800
Suitable for daemons started at boot, which often start before the terminal. It checks again every 10 seconds, prints a status message every 300 seconds, and gives up after 1800 seconds. It does not wait forever without anyone knowing.
I have to acknowledge that this approach still leaves a small gap between the line that checks the process and the line that calls initialize(). If the terminal happens to close in that fraction of a second, the library can still start a new instance. I have no way to close this gap completely from Python. What I can do is make the window as narrow as possible by placing the check immediately before the call, with no other work in between.
Pin the account at startup, then compare it every cycle
A successful attach is not the end of it, because a single terminal can switch accounts. The person at the screen can log into another account or server at any time while the script's session remains connected as usual. The data retrieved afterward belongs to another server, but the script still writes it to the original server's data store.
I therefore have the script record which account it is connected to at startup, then compare it every cycle.
ai = mt5.account_info()
PIN = {
"login": int(ai.login) if ai else 0,
"server": ai.server if ai else "",
"trade_mode": int(getattr(ai, "trade_mode", -1)) if ai else -1,
}
def account_still_same():
now = mt5.account_info() # อ่านสดทุกครั้ง ไม่ใช้ค่าที่จำไว้
if now is None:
return False # อ่านไม่ได้ = ไม่ทำงานรอบนี้
if int(now.login) != PIN["login"] or now.server != PIN["server"]:
return False # สลับบัญชีหรือสลับเซิร์ฟเวอร์
if int(getattr(now, "trade_mode", -1)) != PIN["trade_mode"]:
return False # ประเภทบัญชีเปลี่ยน
return True
There are three pinned values. The account number and server name detect account switches, while trade_mode is the account type code reported by the API. I pin that too so that a situation like "starting work with one account type and ending up with another without anyone requesting it" is impossible, not merely unlikely.
When an account change is detected, continuing with the old values is the wrong thing to do, because many things in the program are tied to the server, such as the server clock offset, cache files named after the server, and the data store destination. This is the sequence I use when I detect a switch.
- First, stop all work that writes data or sends commands, under the same lock used by that work, so nothing can slip in while settings are only partly loaded.
- Reload the full set of settings tied to the new server, including the clock offset and the cache stored in separate files for each server.
- Clear the state accumulated from the previous server, because numbers remembered from one server are not evidence about another.
- Save the new pinned values as the final step. If any earlier step fails, the pinned values remain unchanged. The next cycle will detect the mismatch and retry the whole sequence.
- Require a person to resume work; do not resume automatically.
The fourth step is the easiest to overlook. If I save the pinned values first and then load everything else, a failure partway through loading will leave the program believing the switch is complete even though half its settings still belong to the old server. Saving them last allows the whole switch to retry automatically without additional state recovery code.
A session silently dies after a terminal restart
The issue that cost me the most data was not duplicate terminals, but the opposite: the actual terminal was running fine while the script remained connected to a dead session.
When the terminal closes and reopens, whether because of a person, an update, or a settings change that requires a restart, the interprocess communication channel the script opened to the old terminal remains in a half-dead state. The new terminal is working normally alongside it, but every call the script makes returns None or False without a single exception.
From the script's side, the symptoms are that account_info() returns nothing or an account number of 0, and symbol data requests return None. My code interpreted that as "this symbol has no data" and skipped it. This repeated every cycle. One of my daemons ran blind like this for three days. Every cycle reported completion, but it collected no new data at all.
The misleading part is that the process check I wrote above still passes, because a terminal really is running. The decisive check is terminal_info(). If it returns None, nobody is answering on this session anymore.
def ensure_session():
"""คืน 'ok' / 'reattached' / 'down' เรียกทุกรอบก่อนทำงาน"""
if mt5.terminal_info() is not None and terminal_running(TERMINAL):
return "ok"
print("[WARN] session ตาย หรือ terminal หายไป กำลังต่อใหม่")
try:
mt5.shutdown() # ทิ้ง session เก่าให้ขาดจริงก่อน
except Exception:
pass
return "reattached" if attach(wait_s=1800) else "down"
while True:
state = ensure_session()
if state == "down":
time.sleep(60) # ลองใหม่ทุก 60 วินาที ไม่ออกจากโปรแกรม
continue
if state == "reattached":
reload_server_state() # ค่าที่วัดไว้จาก session ก่อนอาจไม่จริงแล้ว
if account_still_same():
run_cycle()
time.sleep(300)
I must always call shutdown() before calling initialize() again. Otherwise, the library still holds the old session, and the three possible return values have different meanings. The reattached status tells the higher layer that values measured from the previous session, such as the server clock offset, must be measured again, because someone may have switched accounts while the terminal was closed.
My older daemon used initialize(path=, portable=True) in its reconnection function. As a result, when someone closed the terminal completely, the daemon would reopen it on the next cycle. In a system with only one daemon, that is the intended behavior. But on a machine where another program owns the terminal, it means competing to open it. Before copying a reconnection function from one project to another, I need to establish who owns the terminal on that machine.
Getting the startup sequence right
There is another related detail. Attaching can take tens of seconds, and with waiting enabled, it can block there for up to half an hour. If the program has a guard against duplicate instances, that guard must come before attaching, not after. Otherwise, two instances started close together will wait there together, then both emerge to check the duplicate-instance guard at the same time.
The startup sequence for my data collection daemon is therefore: verify that the destination data store exists, acquire the lock against duplicate instances, attach with waiting enabled, pin the account, and then enter the work loop. The mode that runs once and exits does not need to acquire the lock because it does not stay running.
The numbers for this setup
To sum up, initialize(path=) can both connect and launch. Programs that only want to connect must guard against launching by checking the process first. A process check alone is also not enough to establish that the session works. I need to call terminal_info() every cycle and compare the account using freshly read values every cycle too. I now use attach-only mode for every script other than the login launcher. On a machine with just one terminal and one script, letting the library launch it is fine, depending on the setup. I still have not fully closed the gap between checking the process and calling initialize. If I find a better way, I will come back and update this article.
Hit something like this? Stuck somewhere?
Come trade notes on the LinuxThai board



Comments 0
No comments yet — be the first