Result[T]¶
Every client method returns Result[T]. No exceptions are raised for operational failures.
Structure¶
class Result(Generic[T]):
success: bool # True = operation succeeded
data: T | None # set on success, always None on failure
error_code: int | None # MT5 error code on failure
error_message: str | None # human-readable description on failure
context: str | None # MT5 API function name (e.g. "positions_get")
operation: str | None # logical operation name
Strict invariant
success=True→datais set, error fields areNonesuccess=False→dataisNone,error_codeanderror_messageare set
Basic usage¶
from syntiq_mt5 import constants
res = mt5.get_candles("EURUSD", timeframe=constants.TIMEFRAME_H1, count=10)
if res.success:
for candle in res.data:
print(f"O={candle.open} H={candle.high} L={candle.low} C={candle.close}")
else:
print(f"[{res.context}] error {res.error_code}: {res.error_message}")
Success path¶
res = mt5.positions()
if res.success:
print(f"{len(res.data)} open positions")
for p in res.data:
print(f" {p.symbol} {p.pips_profit:+.1f} pips")
res.data is always the correct type — list[Position] here. No casting needed.
Failure path¶
res = mt5.positions()
if not res.success:
print(f"operation: {res.operation}")
print(f"error code: {res.error_code}")
print(f"message: {res.error_message}")
Empty vs failure¶
A successful call with no data is not a failure:
res = mt5.positions()
if res.success:
if not res.data:
print("No open positions") # success, but empty list
else:
print(f"{len(res.data)} positions")
Tip
res.success tells you whether the call worked. res.data tells you what came back.
Chaining calls¶
def get_position_symbols(mt5: MetaTrader5Client) -> list[str]:
res = mt5.positions()
if not res.success:
return []
return [p.symbol for p in res.data]
Keep the pattern consistent: check success, then use data.
Real failure scenarios¶
Scenario 1: Lifecycle violation¶
from syntiq_mt5 import MetaTrader5Client
mt5 = MetaTrader5Client()
res = mt5.positions() # ❌ Called before initialize()
print(f"Success: {res.success}")
print(f"Error code: {res.error_code}")
print(f"Message: {res.error_message}")
print(f"Operation: {res.operation}")
Output:
Success: False
Error code: -10
Message: Client not initialized. Call initialize() first.
Operation: positions_get
Why this fails: The client enforces strict lifecycle ordering. You must call initialize() → login() before any trading operations.
Fix:
with MetaTrader5Client() as mt5:
mt5.initialize(creds)
mt5.login(creds)
res = mt5.positions() # ✅ Now it works
Scenario 2: Invalid symbol¶
res = mt5.symbol_info("INVALID_SYMBOL")
if not res.success:
print(f"Error: {res.error_message}")
print(f"Code: {res.error_code}")
Output:
Why this fails: The symbol doesn't exist in your broker's symbol list, or it's not enabled in Market Watch.
Fix:
# First, check available symbols
symbols = mt5.symbols_get(group="*USD*")
if symbols.success:
print([s.name for s in symbols.data])
# Or enable the symbol first
mt5.symbol_select("EURUSD", True)
Scenario 3: Insufficient margin¶
from syntiq_mt5 import TradeRequest, constants
request = TradeRequest(
action=constants.TRADE_ACTION_DEAL,
symbol="EURUSD",
volume=100.0, # ❌ Way too large
type=constants.ORDER_TYPE_BUY,
price=1.08500,
)
check = mt5.order_check(request)
if not check.success:
print(f"Validation failed: {check.error_message}")
elif not check.data.is_successful:
print(f"Order rejected: {check.data.comment}")
print(f"Retcode: {check.data.retcode}")
Output:
Why this fails: The account doesn't have enough margin to open a 100-lot position.
Fix:
# Calculate required margin first
margin_res = mt5.order_calc_margin(
constants.ORDER_TYPE_BUY,
"EURUSD",
100.0,
1.08500,
)
if margin_res.success:
print(f"Required margin: {margin_res.data}")
# Check account balance
acc = mt5.account_info()
if acc.success:
print(f"Available margin: {acc.data.margin_free}")
Scenario 4: Market closed¶
from datetime import datetime, timezone
# Trying to fetch ticks when market is closed
date_from = datetime(2024, 12, 25, tzinfo=timezone.utc) # Christmas
res = mt5.copy_ticks_from(
"EURUSD",
date_from,
count=100,
flags=constants.COPY_TICKS_ALL,
)
if not res.success:
print(f"Failed: {res.error_message}")
elif not res.data:
print("No ticks available (market was closed)")
Output:
Why this happens: The operation succeeded (success=True), but no data exists for that time period.
Key distinction: success=True with empty data means "the call worked, but there's nothing to return."
Debugging with Result[T]¶
Pattern 1: Log all failures¶
def log_failure(res: Result) -> None:
if not res.success:
print(f"❌ {res.operation} failed")
print(f" Code: {res.error_code}")
print(f" Message: {res.error_message}")
if res.context:
print(f" Context: {res.context}")
res = mt5.positions()
log_failure(res)
Pattern 2: Retry with backoff¶
import time
from syntiq_mt5 import Result
def retry_operation(func, max_attempts: int = 3) -> Result:
for attempt in range(max_attempts):
res = func()
if res.success:
return res
print(f"Attempt {attempt + 1} failed: {res.error_message}")
if attempt < max_attempts - 1:
time.sleep(2 ** attempt) # Exponential backoff
return res # Return last failure
res = retry_operation(lambda: mt5.symbol_info("EURUSD"))
Pattern 3: Collect all errors¶
def fetch_multiple_symbols(mt5, symbols: list[str]) -> dict[str, Result]:
results = {}
for symbol in symbols:
results[symbol] = mt5.symbol_info(symbol)
return results
results = fetch_multiple_symbols(mt5, ["EURUSD", "GBPUSD", "INVALID"])
# Check which ones failed
for symbol, res in results.items():
if not res.success:
print(f"{symbol}: {res.error_message}")
Output:
Pattern 4: Assert in tests¶
def test_positions():
mt5 = MetaTrader5Client()
mt5.initialize(creds)
mt5.login(creds)
res = mt5.positions()
assert res.success, f"positions() failed: {res.error_message}"
assert isinstance(res.data, list)
Error code reference¶
Common error codes you'll encounter:
| Code | Meaning | Typical cause |
|---|---|---|
-10 |
Client not initialized | Called method before initialize() |
-11 |
Client not logged in | Called method before login() |
1 |
Generic error | Check error_message for details |
4301 |
Symbol not found | Invalid symbol or not in Market Watch |
10004 |
Requote | Price changed, retry with new price |
10006 |
Request rejected | Broker rejected the request |
10013 |
Invalid request | Check request parameters |
10014 |
Invalid volume | Volume outside min/max limits |
10015 |
Invalid price | Price outside allowed range |
10016 |
Invalid stops | SL/TP too close to market price |
10019 |
Not enough money | Insufficient margin |
When to check error codes
Most of the time, error_message is enough. Check error_code when you need to handle specific failures differently (e.g., retry on requote, abort on insufficient margin).