# aiomql — The Complete Guide to Building Algorithmic Trading Bots with Python & MetaTrader 5 > **An in-depth tutorial covering every feature of the aiomql framework — from your first async connection to production-grade, multi-instrument trading bots.** ## Table of Contents 1. [What is aiomql?](#what-is-aiomql) 2. [Installation & Requirements](#installation--requirements) 3. [Configuration](#configuration) 4. [Connecting to MetaTrader 5](#connecting-to-metatrader-5) 5. [Working with Symbols](#working-with-symbols) 6. [Market Data: Candles & Ticks](#market-data-candles--ticks) 7. [Technical Analysis](#technical-analysis) 8. [Placing Orders](#placing-orders) 9. [The Trader Abstraction](#the-trader-abstraction) 10. [Risk & Money Management (RAM)](#risk--money-management-ram) 11. [Building a Strategy](#building-a-strategy) 12. [Trading Sessions](#trading-sessions) 13. [The Bot Orchestrator](#the-bot-orchestrator) 14. [Position Tracking](#position-tracking) 15. [Trade Result Recording](#trade-result-recording) 16. [Trade History](#trade-history) 17. [Contributed Extensions](#contributed-extensions) 18. [Multi-Process Execution](#multi-process-execution) 19. [Synchronous API](#synchronous-api) 20. [Full Example: EMA Crossover Bot](#full-example-ema-crossover-bot) 21. [Architecture Overview](#architecture-overview) 22. [Summary](#summary) --- ## What is aiomql? **aiomql** is a Python framework for building algorithmic trading bots on top of [MetaTrader 5](https://www.metatrader5.com/). 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 ```bash pip install aiomql ``` ### Optional Extras ```bash # TA-Lib technical indicators pip install aiomql[talib] # Optional accelerators (Cython, Numba, tqdm) pip install aiomql[optional] # Everything pip install aiomql[all] ``` --- ## 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: ```json { "login": 12345678, "password": "your_password", "server": "YourBroker-Demo", "trade_record_mode": "csv" } ``` 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 ```python from aiomql import Config config = Config( login=12345678, password="your_password", server="YourBroker-Demo", trade_record_mode="csv" ) ``` ### 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 ```python 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()) ``` ### 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. ```python # 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 ``` ### 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 ```python 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}") ``` ### Getting Price Data ```python # 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 ) ``` ### Volume Utilities The `Symbol` class includes helpers for working with lot sizes: ```python # 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) ``` ### Currency Conversion ```python # Convert between currencies using live rates amount_in_usd = await symbol.convert_currency( amount=100.0, from_currency="EUR", to_currency="USD" ) ``` ### ForexSymbol — Specialized for Forex The `ForexSymbol` class extends `Symbol` with forex-specific calculations: ```python 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) ``` --- ## 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: ```python 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 ``` ### Renaming Columns ```python # Rename DataFrame columns (useful after adding indicators) candles.rename( EMA_34="fast_ema", EMA_55="slow_ema", inplace=True ) ``` ### Ticks The `Tick` class represents a single price tick, and `Ticks` is a collection: ```python # 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) ``` --- ## 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 ```python 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()) ``` ### The ta_lib Helper The `Candles` object also includes a `ta_lib` helper with convenience functions: ```python # 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) ``` ### 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 ```python 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}") ``` ### Modifying an Order ```python order.modify( sl=1.0960, tp=1.1120 ) ``` ### Margin and Profit Calculations ```python # 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}") ``` ### Pending Order Management ```python # 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") ``` ### 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 ```python 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 ) ``` ### Checking and Sending ```python # Check if the order passes broker validation can_trade = await trader.check_order() if can_trade: result = await trader.send_order() ``` ### Recording Trades After sending, trades are automatically recorded via the `record_trade` method: ```python await trader.record_trade( result=result, parameters={"strategy": "EMA Crossover"}, name="EMAXOver", expected_profit=25.50 ) ``` ### 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. ```python 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"} ) ``` #### ScalpTrader Places trades using minimum volume and no stop-loss/take-profit levels. Ideal for scalping strategies where positions are managed manually. ```python 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"} ) ``` --- ## 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 ```python 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 ) ``` ### Calculating Risk Amount ```python # 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 ``` ### Enforcing Position Limits ```python # 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!") ``` ### 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 ```python 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) ``` ### 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: ```python strategy = MyStrategy( symbol=ForexSymbol(name="EURUSD"), params={"fast_period": 10, "slow_period": 30} ) ``` Parameters are also accessible as attributes thanks to `__getattr__`: ```python print(strategy.fast_period) # 10 ``` #### 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: ```python 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" ``` #### 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: ```python async def initialize(self): """Called once before the strategy starts trading.""" result = await self.symbol.initialize() # Load historical data, train models, etc. return result ``` #### Stopping a Strategy Raise `StopTrading` from within `trade()` to gracefully stop the strategy: ```python from aiomql.core.exceptions import StopTrading async def trade(self): if some_condition: raise StopTrading("Reached daily profit target") ``` --- ## 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 ```python 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) ) ``` ### Session Actions Sessions can automatically execute actions at their start and end: ```python # 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 ) ``` ### 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) ```python sessions = Sessions(sessions=[london, new_york]) strategy = MyStrategy( symbol=ForexSymbol(name="EURUSD"), sessions=sessions ) ``` ### Session Utilities ```python # 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") ``` --- ## 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 ```python 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() ``` ### Adding Coroutines You can add background coroutines that run alongside your strategies: ```python 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 ) ``` ### Adding Functions For synchronous functions that should run in a thread pool: ```python import time def heartbeat(interval=30): while True: print("Bot is alive...") time.sleep(interval) bot.add_function(function=heartbeat, interval=30) ``` ### Async Entry Point If you're already inside an async context, use `start()` instead of `execute()`: ```python async def main(): bot = Bot() bot.add_strategies(strategies) await bot.start() asyncio.run(main()) ``` ### 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: ```python 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") ``` ### 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: ```python 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() ``` 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 ```python # In your Config or aiomql.json config = Config(trade_record_mode="csv") # Trades are saved to: /trade_records/.csv ``` ### JSON ```python config = Config(trade_record_mode="json") # Trades are saved to: /trade_records/.json ``` ### SQLite ```python config = Config(trade_record_mode="sql") # Trades are saved to a SQLite database in your project root ``` ### 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 ```python 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") ``` --- ## Trade History The `History` class lets you retrieve completed deals and orders from your account history: ```python 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*" ) ``` --- ## 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: ```python from aiomql import Chaos, ForexSymbol strategy = Chaos(symbol=ForexSymbol(name="EURUSD")) ``` ### Symbols #### ForexSymbol Specialized `Symbol` subclass with pip calculations and volume computation based on stop-loss distance. [See the Symbols section above for details.](#forexsymbol--specialized-for-forex) ### Traders #### SimpleTrader Trades with a stop-loss, calculating volume from risk. [See above.](#simpletrader) #### ScalpTrader Trades with minimum volume and no stops. [See above.](#scalptrader) ### Trackers #### Tracker (Strategy Tracker) A lightweight state holder for tracking strategy signals and snooze timers. [See the Strategy section.](#the-tracker) #### PositionTracker Wraps a tracking function to execute on an open position. #### OpenPositionsTracker Automatically discovers and manages all open positions. [See the Position Tracking section.](#openpositionstracker) --- ## Multi-Process Execution For running completely independent bots in parallel, use `Bot.process_pool()`: ```python 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 ) ``` 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 ```python 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() ``` ### Synchronous Strategy Initialization ```python # The Bot class handles sync initialization internally bot = Bot() bot.execute() # Uses sync initialization under the hood ``` ### Synchronous RAM ```python ram = RAM(risk=2) amount = ram.get_amount_sync() can_trade = ram.check_open_positions_sync() ``` --- ## 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 ``` ### aiomql.json ```json { "login": 12345678, "password": "your_password", "server": "YourBroker-Demo", "trade_record_mode": "csv" } ``` ### strategies/ema_crossover.py ```python 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) ``` ### bot.py ```python 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() ``` --- ## 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)│ └─────────────────────┘ ``` ### Design Principles 1. **Singletons** — `Config` 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](docs/toc.md).* *aiomql is MIT-licensed and maintained by [Ichinga Samuel](https://github.com/Ichinga-Samuel/aiomql).* --- ### Support This Project If you found this tutorial helpful, consider supporting the development of aiomql: - [❤️ Sponsor on GitHub](https://github.com/sponsors/Ichinga-Samuel) - [☕ Buy Me a Coffee](https://buymeacoffee.com/ichingasamuel)