DEV Community

Cover image for AIOMQL-The Complete Guide to Building Algorithmic Trading Bots with Python & MetaTrader 5
Ichinga Samuel
Ichinga Samuel

Posted on

AIOMQL-The Complete Guide to Building Algorithmic Trading Bots with Python & MetaTrader 5

What is aiomql?

aiomql is a Python framework for building algorithmic trading bots on top of
MetaTrader 5. Rather than writing raw MT5 API calls and managing connection boilerplate, aiomql gives you:

  • An async-first MetaTrader 5 interface — every MT5 function wrapped with asyncio.to_thread and automatic reconnection on transient errors.
  • High-level abstractions — Strategy, Bot, Trader, Order, Symbol, Sessions, RAM, and more — so you can focus on trading logic.
  • A full synchronous API — every async class has a sync counterpart for scripts, notebooks, and quick prototyping.
  • Built-in technical analysis — pandas-ta integration with optional TA-Lib support.
  • Trade recording — persist results to CSV, JSON, or SQLite automatically.
  • Position tracking — monitor open positions with trailing stops, extending take-profits, hedging, and stacking.
  • Multi-process execution — run independent bots in parallel with a single call.

aiomql is designed for traders who want to build, test, and deploy algorithmic strategies in Python without reinventing the wheel.


Installation & Requirements

Prerequisites

Requirement Details
Python ≥ 3.13
OS Windows (MetaTrader 5 terminal requirement)
Account A MetaTrader 5 trading account (demo or live)

Install via pip

pip install aiomql
Enter fullscreen mode Exit fullscreen mode

Optional Extras

# TA-Lib technical indicators
pip install aiomql[talib]

# Optional accelerators (Cython, Numba, tqdm)
pip install aiomql[optional]

# Everything
pip install aiomql[all]
Enter fullscreen mode Exit fullscreen mode

Configuration

aiomql uses a singleton Config class that can load settings from a JSON file or be configured programmatically. This single configuration object is shared across all components.

Option 1: JSON Configuration File

Create an aiomql.json file in your project root:

{
  "login": 12345678,
  "password": "your_password",
  "server": "YourBroker-Demo",
  "trade_record_mode": "csv"
}
Enter fullscreen mode Exit fullscreen mode

The Config class automatically searches for aiomql.json in your project directory tree, starting from the current working directory and walking up to the root.

Option 2: Programmatic Configuration

from aiomql import Config

config = Config(
    login=12345678,
    password="your_password",
    server="YourBroker-Demo",
    trade_record_mode="csv"
)
Enter fullscreen mode Exit fullscreen mode

Key Configuration Options

Setting Type Description
login int MetaTrader account number
password str Account password
server str Broker server name
path str Path to the MT5 terminal executable
trade_record_mode str Trade logging format: "csv", "json", or "sql"
root str Project root directory

Because Config is a singleton, any component in your application that creates a Config() instance receives the same shared configuration.

State & Store

The Config class also provides access to two persistent storage mechanisms:

  • config.state — A State object for persistent key-value storage (backed by SQLite).
  • config.store — A Store object for in-memory shared state accessible across components. These are useful for sharing data between strategies, trackers, and other components at runtime.

Connecting to MetaTrader 5

The MetaTrader class wraps every MT5 API function with async execution and automatic retry logic. It supports both async context manager usage and direct calls.

Basic Connection

import asyncio
from aiomql import MetaTrader

async def main():
    async with MetaTrader() as mt5:
        # Get account information
        account = await mt5.account_info()
        print(f"Balance: {account.balance}")
        print(f"Equity: {account.equity}")

        # Get terminal info
        terminal = await mt5.terminal_info()
        print(f"Terminal: {terminal.name}")

        # Get available symbols
        symbols = await mt5.symbols_get()
        print(f"{len(symbols)} symbols available")


asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

How It Works

Under the hood, MetaTrader runs every MT5 call through asyncio.to_thread, which means your event loop stays responsive even during blocking I/O. If a connection
error occurs, the _handler method automatically retries initialization and login up to 3 times.

# The MetaTrader class is also a singleton — every instance shares the same state
mt5_a = MetaTrader()
mt5_b = MetaTrader()
assert mt5_a is mt5_b  # True
Enter fullscreen mode Exit fullscreen mode

Key Methods

Method Description
initialize() Connect to the MT5 terminal
login() Log in with credentials
shutdown() Close the connection
account_info() Get account details
terminal_info() Get terminal details
symbols_get() List available symbols
symbol_info() Get info for a specific symbol
copy_rates_from() Fetch OHLC bar data
copy_rates_from_pos() Fetch recent bars by count
copy_ticks_from() Fetch tick data
order_check() Check if an order can be executed
order_send() Send a trade order
positions_get() Get open positions
history_deals_get() Get historical deals
history_orders_get() Get historical orders

Working with Symbols

The Symbol class is a high-level wrapper around a financial instrument. It pulls properties from MT5 (spread, lot sizes, contract size, etc.) and provides methods to fetch market data.

Initializing a Symbol

from aiomql import Symbol

async def main():
    symbol = Symbol(name="EURUSD")
    await symbol.initialize()

    # Now you can access all symbol properties
    print(f"Spread: {symbol.spread}")
    print(f"Point: {symbol.point}")
    print(f"Min volume: {symbol.volume_min}")
    print(f"Max volume: {symbol.volume_max}")
    print(f"Volume step: {symbol.volume_step}")
    print(f"Contract size: {symbol.trade_contract_size}")
Enter fullscreen mode Exit fullscreen mode

Getting Price Data

# Get the current tick
tick = await symbol.info_tick()
print(f"Bid: {tick.bid}, Ask: {tick.ask}")

# Get the last 500 H1 candles
from aiomql import TimeFrame

candles = await symbol.copy_rates_from_pos(
    timeframe=TimeFrame.H1, count=500
)
Enter fullscreen mode Exit fullscreen mode

Volume Utilities

The Symbol class includes helpers for working with lot sizes:

# Check if a volume is valid
is_valid, adjusted = symbol.check_volume(volume=0.15)

# Round volume to the nearest step
rounded = symbol.round_off_volume(volume=0.153, round_down=True)
Enter fullscreen mode Exit fullscreen mode

Currency Conversion

# Convert between currencies using live rates
amount_in_usd = await symbol.convert_currency(
    amount=100.0,
    from_currency="EUR",
    to_currency="USD"
)
Enter fullscreen mode Exit fullscreen mode

ForexSymbol — Specialized for Forex

The ForexSymbol class extends Symbol with forex-specific calculations:

from aiomql import ForexSymbol

eurusd = ForexSymbol(name="EURUSD")
await eurusd.initialize()

# Pip value (point * 10)
print(f"Pip: {eurusd.pip}")  # 0.0001 for most pairs

# Compute volume based on risk amount and stop loss distance
volume = await eurusd.compute_volume_sl(
    amount=100.0,     # risk $100
    price=1.1000,     # entry price
    sl=1.0950,        # stop loss
    round_down=True
)

# Compute volume based on risk amount and points
volume = await eurusd.compute_volume_points(
    amount=100.0,
    points=500,
    round_down=True
)

# Compute points needed for a given profit at a given volume
points = eurusd.compute_points(amount=50.0, volume=0.1)
Enter fullscreen mode Exit fullscreen mode

Market Data: Candles & Ticks

Candles

The Candles class wraps pandas DataFrame with trading-specific functionality.
When you fetch rates from a Symbol, you receive a Candles object:

from aiomql import TimeFrame

candles = await symbol.copy_rates_from_pos(
    timeframe=TimeFrame.H1, count=500
)

# It's a DataFrame under the hood
print(candles.data.head())

# Access individual candles by index
last_candle = candles[-1]
print(f"Open: {last_candle.open}, Close: {last_candle.close}")

# Candle properties
print(f"Body: {last_candle.body}")
print(f"Range: {last_candle.range}")
print(f"Is Bullish: {last_candle.is_bullish()}")
print(f"Is Bearish: {last_candle.is_bearish()}")
print(f"Is Doji: {last_candle.is_doji()}")

# DataFrame columns are accessible directly
print(candles.open)   # Series of open prices
print(candles.close)  # Series of close prices
print(candles.high)   # Series of high prices
print(candles.low)    # Series of low prices
Enter fullscreen mode Exit fullscreen mode

Renaming Columns

# Rename DataFrame columns (useful after adding indicators)
candles.rename(
    EMA_34="fast_ema",
    EMA_55="slow_ema",
    inplace=True
)
Enter fullscreen mode Exit fullscreen mode

Ticks

The Tick class represents a single price tick, and Ticks is a collection:

# Get recent ticks
ticks = await symbol.copy_ticks_from(
    date_from=datetime.now() - timedelta(minutes=5),
    count=1000
)

# Access tick data
for tick in ticks:
    print(f"Time: {tick.time}, Bid: {tick.bid}, Ask: {tick.ask}")

# The Ticks object also supports technical analysis
ticks.ta.sma(length=20, append=True)
Enter fullscreen mode Exit fullscreen mode

Technical Analysis

aiomql integrates pandas-ta (classic) out of the box, available through the .ta accessor on Candles and Ticks objects. Every pandas-ta indicator is available
directly.

Using pandas-ta Indicators

candles = await symbol.copy_rates_from_pos(
    timeframe=TimeFrame.H1, count=500
)

# Moving averages
candles.ta.sma(length=20, append=True)
candles.ta.ema(length=50, append=True)

# RSI
candles.ta.rsi(length=14, append=True)

# MACD
candles.ta.macd(fast=12, slow=26, signal=9, append=True)

# Bollinger Bands
candles.ta.bbands(length=20, std=2, append=True)

# ATR
candles.ta.atr(length=14, append=True)

# Stochastic
candles.ta.stoch(k=14, d=3, append=True)

# Access the results as DataFrame columns
print(candles.data.columns.tolist())
Enter fullscreen mode Exit fullscreen mode

The ta_lib Helper

The Candles object also includes a ta_lib helper with convenience functions:

# Check if series A is above series B
above = candles.ta_lib.above(candles.fast_ema, candles.slow_ema)

# Check if series A is below series B
below = candles.ta_lib.below(candles.fast_ema, candles.slow_ema)
Enter fullscreen mode Exit fullscreen mode

Optional: TA-Lib Integration

If you install aiomql[talib], you get access to the full C-based TA-Lib library for high-performance indicator computation.


Placing Orders

The Order class handles all trade order operations. It's a subclass of
TradeRequest and provides methods for checking, sending, and managing orders.

Creating and Sending a Market Order

from aiomql import Order, OrderType, TradeAction

# Create a buy order
order = Order(
    symbol="EURUSD",
    type=OrderType.BUY,
    volume=0.1,
    price=1.1000,
    sl=1.0950,
    tp=1.1100
)

# Check if the order can be executed
check_result = await order.check()
print(f"Margin required: {check_result.margin}")

# Send the order
result = await order.send()
print(f"Order ticket: {result.order}")
print(f"Return code: {result.retcode}")
Enter fullscreen mode Exit fullscreen mode

Modifying an Order

order.modify(
    sl=1.0960,
    tp=1.1120
)
Enter fullscreen mode Exit fullscreen mode

Margin and Profit Calculations

# Calculate required margin
margin = await order.calc_margin()
print(f"Margin needed: {margin}")

# Calculate expected profit (at take-profit level)
profit = await order.calc_profit()
print(f"Expected profit: {profit}")

# Calculate expected loss (at stop-loss level)
loss = await order.calc_loss()
print(f"Expected loss: {loss}")
Enter fullscreen mode Exit fullscreen mode

Pending Order Management

# Get total pending orders
total = await Order.orders_total()

# Get all pending orders
pending = await Order.get_pending_orders()

# Get a specific pending order by ticket
order = await Order.get_pending_order(ticket=123456)

# Cancel a pending order
await Order.cancel_order(order=123456, symbol="EURUSD")
Enter fullscreen mode Exit fullscreen mode

Connection Retry Logic

The send_order method automatically retries on connection errors (retcode 10031)
with exponential backoff, up to 3 retries.


The Trader Abstraction

While Order handles the low-level mechanics, the Trader class provides a
higher-level interface for placing trades with proper risk management and trade
recording.

Why Use Trader?

Trader ties together the Order, Symbol, and RAM (risk manager) classes
into a cohesive workflow:

  1. Calculate position size based on your risk parameters
  2. Set stop-loss and take-profit levels
  3. Check the order before sending
  4. Send the order and handle errors
  5. Record the trade to your chosen storage format

Creating Orders with Stops

from aiomql import Trader, ForexSymbol, RAM, OrderType

symbol = ForexSymbol(name="EURUSD")
await symbol.initialize()

ram = RAM(risk=2, risk_to_reward=3)  # 2% risk, 1:3 R:R
trader = Trader(symbol=symbol, ram=ram)

# Create order with stop-loss and take-profit using absolute prices
await trader.create_order_with_stops(
    order_type=OrderType.BUY,
    sl=1.0950,
    tp=1.1150
)

# Create order with just a stop-loss (TP calculated from R:R ratio)
await trader.create_order_with_sl(
    order_type=OrderType.BUY,
    sl=1.0950
)

# Create order using points distance
await trader.create_order_with_points(
    order_type=OrderType.BUY,
    points=500
)

# Create a simple order with no stops
await trader.create_order_no_stops(
    order_type=OrderType.BUY,
    volume=0.01
)
Enter fullscreen mode Exit fullscreen mode

Checking and Sending

# Check if the order passes broker validation
can_trade = await trader.check_order()

if can_trade:
    result = await trader.send_order()
Enter fullscreen mode Exit fullscreen mode

Recording Trades

After sending, trades are automatically recorded via the record_trade method:

await trader.record_trade(
    result=result,
    parameters={"strategy": "EMA Crossover"},
    name="EMAXOver",
    expected_profit=25.50
)
Enter fullscreen mode Exit fullscreen mode

Pre-Built Traders

aiomql ships with two ready-to-use Trader subclasses in the contrib package:

SimpleTrader

Places trades with a specified stop-loss. Volume is calculated based on the RAM
risk amount and the distance to the stop-loss.

from aiomql import SimpleTrader, ForexSymbol, OrderType

symbol = ForexSymbol(name="EURUSD")
trader = SimpleTrader(symbol=symbol)

await trader.place_trade(
    order_type=OrderType.BUY,
    sl=1.0950,
    parameters={"name": "MyStrategy"}
)
Enter fullscreen mode Exit fullscreen mode

ScalpTrader

Places trades using minimum volume and no stop-loss/take-profit levels.
Ideal for scalping strategies where positions are managed manually.

from aiomql import ScalpTrader, ForexSymbol, OrderType

symbol = ForexSymbol(name="EURUSD")
trader = ScalpTrader(symbol=symbol)

await trader.place_trade(
    order_type=OrderType.BUY,
    parameters={"name": "ScalpBot"}
)
Enter fullscreen mode Exit fullscreen mode

Risk & Money Management (RAM)

The RAM (Risk Assessment & Money) class is at the heart of aiomql's risk
management. It calculates position sizes, enforces trade limits, and manages
your exposure.

Configuration

from aiomql import RAM

ram = RAM(
    risk=2,              # Risk 2% of free margin per trade
    risk_to_reward=3,    # 1:3 risk-to-reward ratio
    min_amount=5,        # Minimum $5 risk per trade
    max_amount=500,      # Maximum $500 risk per trade
    loss_limit=3,        # Max 3 losing positions at once
    open_limit=5,        # Max 5 total open positions
    fixed_amount=None    # Set to override percentage-based calculation
)
Enter fullscreen mode Exit fullscreen mode

Calculating Risk Amount

# Get the amount to risk per trade (based on free margin)
amount = await ram.get_amount()
print(f"Amount to risk: ${amount:.2f}")

# If fixed_amount is set, it always returns that
ram.modify_ram(fixed_amount=100.0)
amount = await ram.get_amount()  # Always $100
Enter fullscreen mode Exit fullscreen mode

Enforcing Position Limits

# Check if we can open a new position
can_open = await ram.check_open_positions()
if not can_open:
    print("Too many open positions!")

# Check if we've hit the loss limit
can_trade = await ram.check_losing_positions()
if not can_trade:
    print("Too many losing positions!")
Enter fullscreen mode Exit fullscreen mode

How the Amount is Calculated

The calculation flow is:

  1. If fixed_amount is set → return fixed_amount
  2. Otherwise → margin_free × (risk / 100)
  3. If min_amount and max_amount are set → clamp the result

Building a Strategy

The Strategy class is the cornerstone of aiomql. You subclass it, define your
parameters, and implement the trade() method. The framework handles everything
else — initialization, the execution loop, session management, and error handling.

Anatomy of a Strategy

from aiomql import Strategy, ForexSymbol, TimeFrame, Tracker, OrderType, Sessions, ScalpTrader


class MyStrategy(Strategy):
    # Type annotations for strategy-specific parameters
    ttf: TimeFrame
    fast_period: int
    slow_period: int
    tracker: Tracker

    # Default parameter values — these become instance attributes
    parameters = {
        "ttf": TimeFrame.H1,
        "fast_period": 20,
        "slow_period": 50,
    }

    def __init__(self, *, symbol: ForexSymbol, params: dict = None,
                 sessions: Sessions = None, name: str = "MyStrategy"):
        super().__init__(symbol=symbol, params=params, sessions=sessions, name=name)
        self.tracker = Tracker(snooze=self.ttf.seconds)
        self.trader = ScalpTrader(symbol=self.symbol)

    async def trade(self):
        """This method is called repeatedly by the execution loop."""
        # 1. Fetch market data
        candles = await self.symbol.copy_rates_from_pos(
            timeframe=self.ttf, count=200
        )

        # 2. Apply indicators
        candles.ta.sma(length=self.fast_period, append=True)
        candles.ta.sma(length=self.slow_period, append=True)
        candles.rename(
            **{f"SMA_{self.fast_period}": "fast",
               f"SMA_{self.slow_period}": "slow"},
            inplace=True
        )

        # 3. Check for signals
        if candles.ta_lib.above(candles.fast, candles.slow).iloc[-1]:
            self.tracker.update(order_type=OrderType.BUY, snooze=3600)
        elif candles.ta_lib.below(candles.fast, candles.slow).iloc[-1]:
            self.tracker.update(order_type=OrderType.SELL, snooze=3600)
        else:
            self.tracker.update(order_type=None, snooze=self.ttf.seconds)

        # 4. Execute or sleep
        if self.tracker.order_type is not None:
            await self.trader.place_trade(
                order_type=self.tracker.order_type,
                parameters=self.parameters
            )
            await self.delay(secs=self.tracker.snooze)
        else:
            await self.sleep(secs=self.tracker.snooze)
Enter fullscreen mode Exit fullscreen mode

Key Concepts

Parameters Dictionary

The parameters class attribute defines default values for your strategy. When you
instantiate the strategy, you can override any parameter via the params argument:

strategy = MyStrategy(
    symbol=ForexSymbol(name="EURUSD"),
    params={"fast_period": 10, "slow_period": 30}
)
Enter fullscreen mode Exit fullscreen mode

Parameters are also accessible as attributes thanks to __getattr__:

print(strategy.fast_period)  # 10
Enter fullscreen mode Exit fullscreen mode

The Tracker

The Tracker (from aiomql.contrib.utils) is a lightweight state holder for
strategies. It stores the current order_type, a snooze duration, and any extra
state you want to track:

from aiomql import Tracker

tracker = Tracker(snooze=300)  # default snooze of 5 minutes

# Update tracker state
tracker.update(
    order_type=OrderType.BUY,
    snooze=3600,
    trend="bullish",
    last_price=1.1050
)

# Access state
print(tracker.order_type)  # OrderType.BUY
print(tracker.trend)       # "bullish"
Enter fullscreen mode Exit fullscreen mode

Sleep vs Delay

  • self.sleep(secs=...) — Computes the exact time until the next bar opens, ensuring your strategy wakes up right at the start of a new candle. This is critical for cooperative multitasking.
  • self.delay(secs=...) — A simple asyncio.sleep for the given duration.

Initialization

Override initialize() to run one-time setup. By default, it initializes the
symbol:

async def initialize(self):
    """Called once before the strategy starts trading."""
    result = await self.symbol.initialize()
    # Load historical data, train models, etc.
    return result
Enter fullscreen mode Exit fullscreen mode

Stopping a Strategy

Raise StopTrading from within trade() to gracefully stop the strategy:

from aiomql.core.exceptions import StopTrading

async def trade(self):
    if some_condition:
        raise StopTrading("Reached daily profit target")
Enter fullscreen mode Exit fullscreen mode

Trading Sessions

Sessions let you restrict when a strategy can trade. This is essential for forex
markets where different sessions (London, New York, Tokyo) have different
characteristics.

Defining Sessions

from datetime import time
from aiomql import Session, Sessions

# Define individual sessions (times are in UTC)
london = Session(
    name="London",
    start=time(8, 0),
    end=time(16, 0)
)

new_york = Session(
    name="New York",
    start=time(13, 0),
    end=time(21, 0)
)

tokyo = Session(
    name="Tokyo",
    start=time(0, 0),
    end=time(9, 0)
)
Enter fullscreen mode Exit fullscreen mode

Session Actions

Sessions can automatically execute actions at their start and end:

# Close all positions when the London session ends
london = Session(
    name="London",
    start=time(8, 0),
    end=time(16, 0),
    on_end="close_all"
)

# Close only losing positions at session end
new_york = Session(
    name="New York",
    start=time(13, 0),
    end=time(21, 0),
    on_end="close_loss"
)

# Close only winning positions
tokyo = Session(
    name="Tokyo",
    start=time(0, 0),
    end=time(9, 0),
    on_end="close_win"
)

# Custom actions
async def my_start_action(session):
    print(f"{session.name} started!")

async def my_end_action(session):
    print(f"{session.name} ended!")

custom = Session(
    name="Custom",
    start=time(10, 0),
    end=time(18, 0),
    on_start="custom_start",
    on_end="custom_end",
    custom_start=my_start_action,
    custom_end=my_end_action
)
Enter fullscreen mode Exit fullscreen mode

Combining Sessions

Group sessions together with the Sessions class. When a strategy uses
Sessions, the framework automatically:

  1. Checks if the current time falls within any session
  2. Waits (sleeps) if outside all sessions
  3. Handles transitions between sessions (closing the previous, opening the next)
sessions = Sessions(sessions=[london, new_york])

strategy = MyStrategy(
    symbol=ForexSymbol(name="EURUSD"),
    sessions=sessions
)
Enter fullscreen mode Exit fullscreen mode

Session Utilities

# Check if currently in session
print(london.in_session())

# Get session duration
duration = london.duration()
print(f"Duration: {duration.hours}h {duration.minutes}m")

# Seconds until session starts
secs = london.until()
print(f"Session starts in {secs} seconds")
Enter fullscreen mode Exit fullscreen mode

The Bot Orchestrator

The Bot class is the top-level orchestrator that brings everything together.
It manages the MT5 terminal connection, initializes strategies, and coordinates
their execution.

Basic Usage

from aiomql import Bot, ForexSymbol
from my_strategies import EMAXOver, RSIStrategy

def main():
    # Create symbols
    symbols = [ForexSymbol(name=s) for s in ["EURUSD", "GBPUSD", "USDJPY"]]

    # Create strategies
    strategies = [EMAXOver(symbol=sym) for sym in symbols]

    # Create bot and add strategies
    bot = Bot()
    bot.add_strategies(strategies)

    # Run (blocks until shutdown)
    bot.execute()


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Adding Coroutines

You can add background coroutines that run alongside your strategies:

async def log_account_status():
    """Periodically log account balances."""
    from aiomql import Account
    account = Account()
    while True:
        await account.refresh()
        print(f"Balance: {account.balance}, Equity: {account.equity}")
        await asyncio.sleep(60)


bot = Bot()
bot.add_strategies(strategies)

# Runs in the same event loop
bot.add_coroutine(coroutine=log_account_status)

# Runs on a separate thread (for CPU-intensive or blocking tasks)
bot.add_coroutine(
    coroutine=some_blocking_task,
    on_separate_thread=True
)
Enter fullscreen mode Exit fullscreen mode

Adding Functions

For synchronous functions that should run in a thread pool:

import time

def heartbeat(interval=30):
    while True:
        print("Bot is alive...")
        time.sleep(interval)


bot.add_function(function=heartbeat, interval=30)
Enter fullscreen mode Exit fullscreen mode

Async Entry Point

If you're already inside an async context, use start() instead of execute():

async def main():
    bot = Bot()
    bot.add_strategies(strategies)
    await bot.start()

asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

What Happens Inside

When you call bot.execute():

  1. Terminal Start — connects to the MT5 terminal and logs in
  2. Strategy Initialization — each strategy's initialize() is called; strategies that fail are silently skipped
  3. Executor Start — the Executor runs all strategies, coroutines, and functions concurrently using asyncio tasks and thread pools

Position Tracking

aiomql provides a sophisticated position tracking system through the contrib.trackers
package. This allows you to monitor open positions and apply automated management rules.

OpenPosition

The OpenPosition class wraps a trade position with tracking, hedging, and stacking
capabilities:

from aiomql import OpenPosition, PositionTracker

# Create an OpenPosition from a trade result
pos = OpenPosition(
    ticket=123456,
    symbol="EURUSD",
    # ... other position details
)

# Modify stop-loss and take-profit
await pos.modify_stops(sl=1.0960, tp=1.1120)

# Close the position
success, result = await pos.close_position()

# Add custom trackers to the position
pos.add_tracker(tracker=my_tracker, name="trailing_stop")
Enter fullscreen mode Exit fullscreen mode

Position Tracking Functions

aiomql includes pre-built tracking functions:

  • exit_at_profit — Close a position when profit reaches a target price
  • extend_take_profit — Dynamically extend the take-profit as price moves favorably

OpenPositionsTracker

The OpenPositionsTracker manages all open positions and runs their trackers
automatically:

from aiomql import Bot, OpenPositionsTracker

bot = Bot()
bot.add_strategies(strategies)

# Track all open positions on a separate thread
bot.add_coroutine(
    coroutine=OpenPositionsTracker(autocommit=True).track,
    on_separate_thread=True
)

bot.execute()
Enter fullscreen mode Exit fullscreen mode

This automatically:

  • Discovers all open positions
  • Runs any registered trackers on each position
  • Handles position closure and cleanup
  • Manages hedges, stacks, and pending orders

Trade Result Recording

Every trade can be automatically recorded for analysis. The Result class supports
three storage formats:

CSV

# In your Config or aiomql.json
config = Config(trade_record_mode="csv")

# Trades are saved to: <root>/trade_records/<strategy_name>.csv
Enter fullscreen mode Exit fullscreen mode

JSON

config = Config(trade_record_mode="json")

# Trades are saved to: <root>/trade_records/<strategy_name>.json
Enter fullscreen mode Exit fullscreen mode

SQLite

config = Config(trade_record_mode="sql")

# Trades are saved to a SQLite database in your project root
Enter fullscreen mode Exit fullscreen mode

What Gets Recorded?

Each trade record includes:

  • Order result — ticket, deal, volume, price, retcode
  • Strategy parameters — all parameters from the strategy's parameters dict
  • Trade request — the actual request sent to the broker
  • Extras — timestamp, expected profit, strategy name

Manual Recording

from aiomql import Result
from aiomql.core.models import OrderSendResult

result = Result(
    result=order_send_result,
    parameters={"strategy": "EMA", "timeframe": "H1"},
    name="EMAXOver",
    expected_profit=25.0
)

await result.save(trade_record_mode="csv")
Enter fullscreen mode Exit fullscreen mode

Trade History

The History class lets you retrieve completed deals and orders from your account
history:

from datetime import datetime, timedelta
from aiomql import History

# Get the last 7 days of history
history = History(
    date_from=datetime.now() - timedelta(days=7),
    date_to=datetime.now()
)

# Fetch deals and orders
await history.initialize()

# Access deals
print(f"Total deals: {history.total_deals}")
for deal in history.deals:
    print(f"  {deal.symbol}: {deal.profit}")

# Access orders
print(f"Total orders: {history.total_orders}")

# Filter by ticket
deals = history.filter_deals_by_ticket(ticket=123456)

# Filter by position ID
deals = history.filter_deals_by_position(position=789012)

# Filter by symbol group (e.g., only USD pairs)
history = History(
    date_from=datetime.now() - timedelta(days=30),
    date_to=datetime.now(),
    group="*USD*"
)
Enter fullscreen mode Exit fullscreen mode

Contributed Extensions

The contrib package contains community extensions that build on the core framework.

Strategies

Chaos

A demo strategy that randomly buys or sells. Useful for testing your infrastructure:

from aiomql import Chaos, ForexSymbol

strategy = Chaos(symbol=ForexSymbol(name="EURUSD"))
Enter fullscreen mode Exit fullscreen mode

Symbols

ForexSymbol

Specialized Symbol subclass with pip calculations and volume computation based on
stop-loss distance. See the Symbols section above for details.

Traders

SimpleTrader

Trades with a stop-loss, calculating volume from risk.
See above.

ScalpTrader

Trades with minimum volume and no stops.
See above.

Trackers

Tracker (Strategy Tracker)

A lightweight state holder for tracking strategy signals and snooze timers.
See the Strategy section.

PositionTracker

Wraps a tracking function to execute on an open position.

OpenPositionsTracker

Automatically discovers and manages all open positions.
See the Position Tracking section.


Multi-Process Execution

For running completely independent bots in parallel, use Bot.process_pool():

from aiomql import Bot, ForexSymbol
from strategies import ForexStrategy, CryptoStrategy


def run_forex():
    bot = Bot()
    symbols = [ForexSymbol(name=s) for s in ["EURUSD", "GBPUSD"]]
    strategies = [ForexStrategy(symbol=s) for s in symbols]
    bot.add_strategies(strategies)
    bot.execute()


def run_crypto():
    bot = Bot()
    symbols = [ForexSymbol(name=s) for s in ["BTCUSD", "ETHUSD"]]
    strategies = [CryptoStrategy(symbol=s) for s in symbols]
    bot.add_strategies(strategies)
    bot.execute()


# Run both bots in separate processes
Bot.process_pool(
    processes={
        run_forex: {},
        run_crypto: {}
    },
    num_workers=2
)
Enter fullscreen mode Exit fullscreen mode

Each process gets its own event loop, MT5 connection, and strategy set. This is
useful when you want complete isolation between different trading systems.


Synchronous API

Every async class in aiomql has a synchronous counterpart. This makes the library
usable in Jupyter notebooks, simple scripts, or anywhere you don't want to deal with
asyncio.

Synchronous MetaTrader

from aiomql.core.sync import MetaTrader as SyncMetaTrader

mt5 = SyncMetaTrader()
mt5.initialize_sync()
mt5.login_sync(login=12345678, password="your_password", server="YourBroker-Demo")

account = mt5._account_info()
print(f"Balance: {account.balance}")

mt5.shutdown()
Enter fullscreen mode Exit fullscreen mode

Synchronous Strategy Initialization

# The Bot class handles sync initialization internally
bot = Bot()
bot.execute()  # Uses sync initialization under the hood
Enter fullscreen mode Exit fullscreen mode

Synchronous RAM

ram = RAM(risk=2)
amount = ram.get_amount_sync()
can_trade = ram.check_open_positions_sync()
Enter fullscreen mode Exit fullscreen mode

Full Example: EMA Crossover Bot

Here's a complete, production-ready example that puts everything together:

Project Structure

my_trading_bot/
├── aiomql.json
├── strategies/
│   ├── __init__.py
│   └── ema_crossover.py
└── bot.py
Enter fullscreen mode Exit fullscreen mode

aiomql.json

{
  "login": 12345678,
  "password": "your_password",
  "server": "YourBroker-Demo",
  "trade_record_mode": "csv"
}
Enter fullscreen mode Exit fullscreen mode

strategies/ema_crossover.py

from aiomql import (
    Strategy, ForexSymbol, TimeFrame, Tracker,
    OrderType, Sessions, ScalpTrader
)


class EMAXOver(Strategy):
    """EMA Crossover strategy.

    Buys when the fast EMA crosses above the slow EMA.
    Sells when the fast EMA crosses below the slow EMA.
    """

    ttf: TimeFrame
    tcc: int
    fast_ema: int
    slow_ema: int
    tracker: Tracker
    interval: TimeFrame
    timeout: int

    parameters = {
        "ttf": TimeFrame.H1,
        "tcc": 3000,
        "fast_ema": 34,
        "slow_ema": 55,
        "interval": TimeFrame.M15,
        "timeout": 3 * 60 * 60,  # 3 hours cooldown after a trade
    }

    def __init__(self, *, symbol: ForexSymbol, params: dict = None,
                 sessions: Sessions = None, name: str = "EMAXOver"):
        super().__init__(
            symbol=symbol, params=params,
            sessions=sessions, name=name
        )
        self.tracker = Tracker(snooze=self.interval.seconds)
        self.trader = ScalpTrader(symbol=self.symbol)

    async def find_entry(self):
        # Fetch candle data
        candles = await self.symbol.copy_rates_from_pos(
            timeframe=self.ttf, count=self.tcc
        )

        # Calculate EMAs
        candles.ta.ema(length=self.fast_ema, append=True)
        candles.ta.ema(length=self.slow_ema, append=True)
        candles.rename(
            **{f"EMA_{self.fast_ema}": "fast_ema",
               f"EMA_{self.slow_ema}": "slow_ema"},
            inplace=True,
        )

        # Check for crossover signals
        fast_above_slow = candles.ta_lib.above(
            candles.fast_ema, candles.slow_ema
        )
        fast_below_slow = candles.ta_lib.below(
            candles.fast_ema, candles.slow_ema
        )

        if fast_above_slow.iloc[-1]:
            self.tracker.update(
                order_type=OrderType.BUY,
                snooze=self.timeout
            )
        elif fast_below_slow.iloc[-1]:
            self.tracker.update(
                order_type=OrderType.SELL,
                snooze=self.timeout
            )
        else:
            self.tracker.update(
                order_type=None,
                snooze=self.interval.seconds
            )

    async def trade(self):
        await self.find_entry()

        if self.tracker.order_type is None:
            # No signal — sleep until next bar
            await self.sleep(secs=self.tracker.snooze)
        else:
            # Signal found — place trade and cooldown
            await self.trader.place_trade(
                order_type=self.tracker.order_type,
                parameters=self.parameters,
            )
            await self.delay(secs=self.tracker.snooze)
Enter fullscreen mode Exit fullscreen mode

bot.py

import logging
from aiomql import Bot, ForexSymbol, OpenPositionsTracker
from strategies.ema_crossover import EMAXOver

logging.basicConfig(level=logging.INFO)


def main():
    # Define symbols to trade
    symbols = [
        ForexSymbol(name=s)
        for s in ["EURUSD", "GBPUSD", "USDJPY"]
    ]

    # Create a strategy instance for each symbol
    strategies = [EMAXOver(symbol=sym) for sym in symbols]

    # Create the bot
    bot = Bot()
    bot.add_strategies(strategies)

    # Track open positions on a separate thread
    bot.add_coroutine(
        coroutine=OpenPositionsTracker(autocommit=True).track,
        on_separate_thread=True
    )

    # Start trading
    bot.execute()


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Architecture Overview

┌─────────────────────────────────────────────────────┐
│                     Bot (Orchestrator)               │
│  ┌──────────────────────────────────────────────┐   │
│  │               Executor                        │   │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────────────┐│   │
│  │  │Strategy 1│ │Strategy 2│ │  Coroutines/    ││   │
│  │  │(EURUSD) │ │(GBPUSD) │ │  Functions       ││   │
│  │  └────┬────┘ └────┬────┘ └────────┬────────┘│   │
│  └───────┼───────────┼───────────────┼──────────┘   │
│          │           │               │               │
│  ┌───────▼───────────▼───────────────▼──────────┐   │
│  │                Shared Services                 │   │
│  │  ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐│   │
│  │  │ Config │ │MetaTrader│ │  RAM   │ │ Result ││   │
│  │  │(single)│ │(single) │ │        │ │        ││   │
│  │  └────────┘ └────────┘ └────────┘ └────────┘│   │
│  └──────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘
         │
         ▼
┌─────────────────────┐
│  MetaTrader 5       │
│  Terminal           │
│  (asyncio.to_thread)│
└─────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Design Principles

  1. SingletonsConfig and MetaTrader in some cases are singletons, ensuring consistent state across all components.
  2. Async-first — Every MT5 call is wrapped with asyncio.to_thread, keeping the event loop responsive.
  3. Composition — Components are designed to be composed. A Strategy uses a Symbol, Trader, RAM, and Sessions.
  4. Separation of concerns — Market data (Symbol, Candles, Ticks), trade execution (Order, Trader), risk management (RAM), and orchestration (Bot, Executor) are all separate modules.

Summary

Feature Module Description
MT5 Connection MetaTrader Async MT5 interface with retry
Configuration Config Singleton JSON/programmatic config
Market Data Symbol, Candles, Ticks Symbols, OHLC bars, tick data
Technical Analysis ta_libs pandas-ta + optional TA-Lib
Orders Order Check, send, margin/profit calc
Trade Execution Trader, SimpleTrader, ScalpTrader High-level trade placement
Risk Management RAM Position sizing, trade limits
Strategy Strategy Base class for trading logic
Sessions Session, Sessions Time-window trading restrictions
Orchestration Bot, Executor Multi-strategy concurrent execution
Position Tracking OpenPositionsTracker, OpenPosition Trailing stops, hedging, stacking
Trade Recording Result CSV, JSON, SQLite persistence
History History Deal and order history queries
Forex Helpers ForexSymbol Pip calc, volume from SL
Positions Positions Open position management
Multi-Process Bot.process_pool() Run bots in parallel processes
Sync API core.sync, lib.sync Synchronous mirrors

aiomql takes care of the infrastructure, the connection handling, the execution
loops, and the bookkeeping — so you can focus on what matters: your trading
strategy
.


For the full API reference, see the documentation.

aiomql is MIT-licensed and maintained by Ichinga Samuel.


Support This Project

If you found this tutorial helpful, consider supporting the development of aiomql:

Top comments (0)