# PyEventBT — Complete Documentation > PyEventBT is an open-source Python framework for event-driven backtesting and live trading on MetaTrader 5 (MT5). Install with `pip install pyeventbt`. Documentation at https://pyeventbt.com. Source at https://github.com/marticastany/pyeventbt. License: Apache 2.0. --- # About PyEventBT **PyEventBT** is an institutional-grade event-driven backtesting and live trading framework built with Python for the MetaTrader 5 platform. It provides a complete mock of the MT5 API for an easy transition between backtesting and live trading, allowing traders to easily develop multi-rule, multi-timeframe and multi-instrument strategies. Whether you're building simple moving average crossovers or complex multi-rule and multi-timeframe strategies, PyEventBT provides the tools you need to develop, test, and deploy with confidence. Its modular architecture allows you to design your own signal sources, position sizing logic and risk management overlay as independent and interchangeable blocks. ## Why PyEventBT? **PyEventBT** helps traders develop, test and deploy algorithmic trading strategies with ease. It provides a unified ecosystem where you can backtest and live trade your strategies with the same code base, minimizing the impact of errors when moving from backtesting to live trading. It also means you **won't need to write a single line of MQL5 code** or depend on the MetaTrader Backtester anymore, as PyEventBT allows you to completely bypass it and only use the MetaTrader 5 platform to send the live trades once your strategy is ready. Even this event-driven approach is generally slower than vectorized backtesting, it ensures that your backtest results accurately reflect how your strategy would have performed in production. ## Key Benefits - **Easy to Use Modules**: Simple API with decorator-based strategy definition and pluggable components for risk management, position sizing and execution let you customize every aspect of your trading system. - **Python, not MQL5**: You can use the full power of Python to develop your strategies, without the need to write a single line of MQL5 code. - **Multi Timeframe-Instrument-Rule**: Handle multiple timeframes, instruments and rules in a single strategy. - **Write Once, Run Anywhere**: The same strategy code runs in both backtesting and live trading. No need to rewrite or adapt your logic when moving from research to production. - **Production-Ready**: Built-in integration with MetaTrader 5 means you can deploy your strategies to live markets with minimal configuration. - **Comprehensive Analysis**: Built-in performance metrics and visualization tools let you analyze your backtest results and make informed decisions. - **Technical Indicators**: Built-in library of common indicators (ATR, SMA, EMA, RSI and more). ## The Problem PyEventBT Solves Most backtesting frameworks suffer from one or more of these issues: 1. **Look-Ahead Bias**: Vectorized backtesting can inadvertently use future data 2. **Unrealistic Execution**: Assumptions about fills, slippage, and timing don't match reality 3. **Backtest/Live Divergence**: Code that works in backtests fails in live trading 4. **Limited Flexibility**: Hard to customize risk management, sizing, or execution logic 5. **Complex Setup**: Difficult to integrate with live trading platforms PyEventBT solves these by: - **Event-Driven Processing**: Eliminates look-ahead bias by processing events in chronological order - **Realistic Simulation**: Simulates actual broker behavior, including slippage and execution delays - **Unified Codebase**: Same strategy code for backtest and live trading - **Modular Architecture**: Easy to customize any component without affecting others - **MT5 Integration**: Direct connection to MetaTrader 5 for seamless live trading --- # Installation PyEventBT is published on PyPI and can be easily installed with a single command. ## Quick Install ```bash pip install pyeventbt ``` ## Prerequisites PyEventBT requires **Python 3.12** or higher. **Platform Recommendation**: While development and backtesting can be done on any platform (macOS, Linux, Windows), **Windows is recommended for live trading**. This is because MetaTrader 5 (MT5) has native support on Windows. On macOS and Linux, MT5 requires virtualization (e.g., Wine), which may introduce instability or IPC problems. ## Recommended Setup 1. Create a virtual environment: ```bash # Using Conda conda create -n pyeventbt python=3.12 # Or using venv python3.12 -m venv venv ``` 2. Activate the environment: ```bash # Conda conda activate pyeventbt # venv (Mac/Linux) source venv/bin/activate # venv (Windows) .\venv\Scripts\activate ``` 3. Install PyEventBT from PyPI: ```bash pip install pyeventbt ``` ## Verify Installation ```python import pyeventbt print(pyeventbt.__version__) ``` --- # How It Works PyEventBT is built around an **event-driven architecture**. It simulates real-world trading conditions by processing market events sequentially, just as they would occur in live trading. This eliminates look-ahead bias by design and ensures your backtest results accurately reflect live performance. ## Event Flow 1. **Market Data** arrives as `BarEvent` objects (OHLCV bars) 2. **Your Strategy** processes the data and generates `SignalEvent` objects 3. **Position Sizing** calculates how much to trade 4. **Risk Engine** validates the trade against your risk rules 5. **Execution Engine** places the order (simulated or real) 6. **Portfolio** tracks positions, P&L, and account state This flow ensures that your strategy only sees data that would have been available at that moment, just like in real trading. ## Core Components ### Strategy The main entry point where you define your trading logic using simple decorators. ### Data Provider Access historical data for backtesting or live data from MT5. Supports multiple timeframes and symbols. ### Signal Engine Your strategy logic runs here, generating trading signals based on market conditions. ### Sizing Engine Calculates position sizes based on your account equity and risk parameters. ### Risk Engine Validates trades against your risk management rules before execution. ### Execution Engine Handles order execution, working with simulated brokers in backtests and real MT5 in live trading. ### Portfolio Tracks your account state, positions, orders, and P&L throughout the trading session. --- # Core Architecture Reference PyEventBT follows a **layered, event-driven architecture**. Every component communicates through events flowing through a central queue. This design ensures your strategy code works identically in backtesting and live trading — with zero changes. ## Two Layers ### Your Code - **Strategy**: The entry point. You create a `Strategy` instance, define your signal logic with decorators, configure sizing and risk, then call `backtest()` or `run_live()`. - **Signal Engine**: Your trading logic. Receives `BarEvent`s, accesses market data and portfolio state via `modules`, and returns `SignalEvent`s when conditions are met. ### Framework Internals - **Trading Director**: The orchestrator. Manages the event loop, routes events to the correct handlers, and coordinates all components. You never interact with it directly. - **Data Provider**: Loads market data from CSV files (backtest) or MetaTrader 5 (live). Provides `get_latest_bars()`, `get_latest_tick()`, and other data access methods via `modules.DATA_PROVIDER`. - **Portfolio Handler**: Bridges signal generation and order execution. Receives your `SignalEvent`, runs it through the Sizing Engine and Risk Engine, and produces an `OrderEvent` if approved. - **Sizing Engine**: Calculates position size for each signal. Predefined options: `MinSizingConfig`, `FixedSizingConfig`, `RiskPctSizingConfig`. Or write your own with `@strategy.custom_sizing_engine`. - **Risk Engine**: Validates orders against risk rules before execution. Use `PassthroughRiskConfig` (allow all) or write custom rules with `@strategy.custom_risk_engine`. - **Execution Engine**: Sends orders to the broker. In backtesting, simulates fills using the MT5 execution model. In live trading, routes orders to MetaTrader 5. Accessible via `modules.EXECUTION_ENGINE`. - **Portfolio**: Tracks all positions, pending orders, balance, equity, and P&L. Updated automatically after each fill. Accessible via `modules.PORTFOLIO`. ## Event Pipeline Every bar of market data triggers this pipeline. You only create **Step 2** — everything else is automatic. 1. **BarEvent** — Data Provider emits a new bar. The Trading Director routes it to your Signal Engine. 2. **SignalEvent** — Your strategy logic runs. If conditions are met, you return one or more `SignalEvent`s. *(This is your code.)* 3. **Sizing** — The Portfolio Handler passes your signal to the Sizing Engine, which calculates position volume. 4. **Risk Validation** — The Risk Engine checks the sized order against your risk rules. If rejected, the signal is discarded. 5. **OrderEvent** — An approved order is created and sent to the Execution Engine. 6. **FillEvent** — The broker (simulated or real) executes the order and returns fill details: price, commission, swap. 7. **Portfolio Update** — The Portfolio records the new position and updates balance, equity, and P&L. Events are processed in strict chronological order. Your strategy never sees future data — the same guarantee holds in both backtesting and live trading. ## Backtest vs Live The architecture is designed so your strategy code doesn't change between modes. The framework swaps the internal implementations: | Component | Backtest Mode | Live Mode | | :--- | :--- | :--- | | **Data Provider** | Reads from CSV files | Connects to MT5 terminal | | **Execution Engine** | Simulates fills using MT5 model | Routes orders to MT5 broker | | **Portfolio** | Tracks simulated positions | Tracks real positions | | **Your Strategy** | No changes | No changes | ```python # Backtest — runs on historical CSV data backtest = strategy.backtest( strategy_id=strategy_id, initial_capital=100000, symbols_to_trade=['EURUSD'], csv_dir='./data', start_date=datetime(2020, 1, 1), end_date=datetime(2023, 12, 1), ) # Live — same strategy, same signal logic, just a different call strategy.run_live( mt5_configuration=mt5_config, strategy_id=strategy_id, initial_capital=100000, symbols_to_trade=['EURUSD'], heartbeat=0.1, ) ``` ## The `modules` Object Your signal engine receives a `modules` parameter that provides access to framework components at runtime: | Module | What it gives you | | :--- | :--- | | `modules.DATA_PROVIDER` | Market data: `get_latest_bars()`, `get_latest_tick()`, `get_latest_bid()`, `get_latest_ask()` | | `modules.PORTFOLIO` | Account state: `get_positions()`, `get_account_balance()`, `get_number_of_strategy_open_positions_by_symbol()` | | `modules.EXECUTION_ENGINE` | Order management: `close_all_strategy_positions()`, `cancel_all_strategy_pending_orders()` | | `modules.TRADING_CONTEXT` | Mode detection: `"BACKTEST"` or `"LIVE"` | ```python @strategy.custom_signal_engine(...) def my_strategy(event: BarEvent, modules: Modules): # Data bars = modules.DATA_PROVIDER.get_latest_bars(event.symbol, tf, 50) # Portfolio state positions = modules.PORTFOLIO.get_number_of_strategy_open_positions_by_symbol(event.symbol) # Order management if positions['LONG'] > 0: modules.EXECUTION_ENGINE.close_strategy_long_positions_by_symbol(event.symbol) # Context if modules.TRADING_CONTEXT == "BACKTEST": time_generated = event.datetime + tf.to_timedelta() ``` ## Extensibility Every major component has an interface that you can implement: - **Custom Signal Engines**: Define your own signal generation logic - **Custom Sizing Engines**: Implement custom position sizing algorithms - **Custom Risk Engines**: Create your own risk management rules - **Custom Execution Engines**: Integrate with other brokers or execution systems - **Hooks**: Add custom callbacks at various points in the event flow ## Integration Points - **MetaTrader 5**: Full integration for backtesting and live trading - **CSV Data**: Import historical data from CSV files - **Quantdle**: Integration with Quantdle data services - **Custom Brokers**: Extensible architecture for other broker integrations --- # Events Reference PyEventBT is built on an **event-driven architecture**. All communication between components happens through events that flow through a central queue. Understanding events is essential because the `SignalEvent` is the primary object you create in your strategies. ## Why Events? Events ensure that: - Your strategy only sees data that would have been available at that moment (no look-ahead bias) - Everything happens in the correct chronological order - Backtests accurately simulate real trading conditions - The same code works for both backtesting and live trading ## Event Flow Every bar of market data triggers this pipeline: ``` BarEvent -> Your Strategy -> SignalEvent -> Sizing Engine -> Risk Engine -> OrderEvent -> Execution Engine -> FillEvent -> Portfolio ``` 1. **BarEvent** — New market data arrives (automatically generated by the Data Provider) 2. **SignalEvent** — Your strategy generates a trading signal (this is what you create) 3. **OrderEvent** — Sizing and risk engines validate the signal and produce an order (automatic) 4. **FillEvent** — The order is executed by the broker, simulated or real (automatic) 5. **Portfolio Update** — Account balance, positions, and equity are updated (automatic) You only create **SignalEvents**. Everything else — BarEvents, OrderEvents, FillEvents — is handled automatically by the framework. ## Event Types ### BarEvent Contains new market data. Your signal engine receives one on every new bar for each registered timeframe. **Fields:** | Field | Type | Description | | :--- | :--- | :--- | | `symbol` | `str` | Instrument symbol (e.g., `'EURUSD'`) | | `datetime` | `datetime` | Timestamp of the bar | | `timeframe` | `str` | Timeframe that produced this bar | | `data` | `Bar` | OHLCV data (see below) | The `data` field contains a `Bar` object with integer-stored prices for performance. Use the `_f` properties for float values: | Property | Type | Description | | :--- | :--- | :--- | | `data.open_f` | `float` | Open price | | `data.high_f` | `float` | High price | | `data.low_f` | `float` | Low price | | `data.close_f` | `float` | Close price | | `data.volume` | `int` | Volume | | `data.spread_f` | `float` | Spread | ```python @strategy.custom_signal_engine( strategy_id=strategy_id, strategy_timeframes=[StrategyTimeframes.ONE_HOUR] ) def my_strategy(event: BarEvent, modules: Modules): # Metadata from the event symbol = event.symbol # 'EURUSD' timestamp = event.datetime # When this bar closed tf = event.timeframe # Which timeframe produced this bar # Single bar data (from the event directly) current_close = event.data.close_f # Historical bars (from the Data Provider — this is the common pattern) bars = modules.DATA_PROVIDER.get_latest_bars(symbol, tf, 50) close_prices = bars.select('close').to_numpy().flatten() ``` In practice, you'll rarely access `event.data` directly. Most strategies use `modules.DATA_PROVIDER.get_latest_bars()` to get a Polars DataFrame of historical bars for indicator calculations. ### SignalEvent A trading signal generated by your strategy. **This is the main event you create.** **Fields:** | Field | Type | Required | Description | | :--- | :--- | :--- | :--- | | `symbol` | `str` | Yes | Instrument to trade | | `time_generated` | `datetime` | Yes | When the signal should execute | | `strategy_id` | `str` | Yes | Your strategy's identifier | | `signal_type` | `SignalType` | Yes | `SignalType.BUY` or `SignalType.SELL` | | `order_type` | `OrderType` | Yes | `OrderType.MARKET`, `OrderType.LIMIT`, or `OrderType.STOP` | | `order_price` | `Decimal` | No | Target price for LIMIT/STOP orders. Default `0.0` | | `sl` | `Decimal` | No | Stop-loss price. Default `0.0` (no stop-loss) | | `tp` | `Decimal` | No | Take-profit price. Default `0.0` (no take-profit) | **Imports:** ```python from pyeventbt import SignalEvent from pyeventbt.events.events import SignalType, OrderType from decimal import Decimal ``` #### Creating a Market Order signal ```python last_tick = modules.DATA_PROVIDER.get_latest_tick(symbol) signal = SignalEvent( symbol=symbol, time_generated=time_generated, strategy_id=strategy_id, signal_type=SignalType.BUY, order_type=OrderType.MARKET, order_price=last_tick['ask'], sl=Decimal('0.0'), tp=Decimal('0.0'), ) ``` #### Creating a Limit Order signal ```python signal = SignalEvent( symbol=symbol, time_generated=time_generated, strategy_id=strategy_id, signal_type=SignalType.BUY, order_type=OrderType.LIMIT, order_price=Decimal('1.1000'), sl=Decimal('1.0950'), tp=Decimal('1.1100'), ) ``` #### Creating a Stop Order signal (for breakout strategies) ```python signal = SignalEvent( symbol=symbol, time_generated=time_generated, strategy_id=strategy_id, signal_type=SignalType.BUY, order_type=OrderType.STOP, order_price=Decimal('1.1100'), sl=Decimal('1.1050'), tp=Decimal('1.1200'), ) ``` #### Returning signals from your strategy Your signal engine can return: - **`None`** or **no return** — no signal this bar - **A single `SignalEvent`** — one signal - **A list of `SignalEvent`** — multiple signals (e.g., both a BUY STOP and SELL STOP for a breakout strategy) ```python @strategy.custom_signal_engine(...) def my_strategy(event: BarEvent, modules: Modules): signal_events = [] if buy_condition: signal_events.append(SignalEvent(...)) if sell_condition: signal_events.append(SignalEvent(...)) return signal_events # Empty list = no signals ``` ### OrderEvent An order ready to be sent to the broker. Created automatically after your SignalEvent passes through the Sizing Engine and Risk Engine. **You don't create these directly.** The system converts your `SignalEvent` into an `OrderEvent` by: 1. Calculating position size (via the Sizing Engine) 2. Validating the trade (via the Risk Engine) **Key fields:** | Field | Type | Description | | :--- | :--- | :--- | | `symbol` | `str` | Instrument | | `time_generated` | `datetime` | Signal timestamp | | `strategy_id` | `str` | Strategy identifier | | `volume` | `Decimal` | Position size (calculated by Sizing Engine) | | `signal_type` | `SignalType` | BUY or SELL | | `order_type` | `OrderType` | MARKET, LIMIT, or STOP | | `order_price` | `Decimal` | Target price | | `sl` | `Decimal` | Stop-loss | | `tp` | `Decimal` | Take-profit | ### FillEvent Confirmation that an order was executed by the broker (simulated or real). **You don't create these directly.** They're generated by the Execution Engine after processing an OrderEvent. **Key fields:** | Field | Type | Description | | :--- | :--- | :--- | | `deal` | `DealType` | `DealType.IN` (open) or `DealType.OUT` (close) | | `symbol` | `str` | Instrument | | `time_generated` | `datetime` | Execution timestamp | | `position_id` | `int` | Position identifier | | `strategy_id` | `str` | Strategy identifier | | `volume` | `Decimal` | Executed volume | | `price` | `Decimal` | Execution price | | `signal_type` | `SignalType` | BUY or SELL | | `commission` | `Decimal` | Broker commission | | `swap` | `Decimal` | Swap/rollover cost | | `fee` | `Decimal` | Additional fees | | `gross_profit` | `Decimal` | Gross P&L (for closing trades) | | `ccy` | `str` | Currency of costs/profits | ### ScheduledEvent Timer-based event for periodic actions (e.g., daily rebalancing, scheduled reports). Created by the `@strategy.run_every()` decorator. **Key fields:** | Field | Type | Description | | :--- | :--- | :--- | | `schedule_timeframe` | `StrategyTimeframes` | How often this fires | | `symbol` | `str` | Instrument | | `timestamp` | `pd.Timestamp` | Current timestamp | ```python @strategy.run_every(StrategyTimeframes.ONE_DAY) def daily_task(event: ScheduledEvent, modules: Modules): # Runs once per day per symbol print(f"Daily check for {event.symbol} at {event.timestamp}") ``` ## Signal Timing The `time_generated` field on a SignalEvent controls **when** the signal executes. This is critical for avoiding look-ahead bias in backtesting. ```python # In backtesting: execute at the NEXT bar's open if modules.TRADING_CONTEXT == "BACKTEST": time_generated = event.datetime + signal_timeframe.to_timedelta() else: # In live trading: execute now time_generated = datetime.now() ``` **Important**: If you set `time_generated = event.datetime` in a backtest, the signal would execute at the same bar it was generated — meaning you'd be trading on information you couldn't have acted on yet. Always offset by one bar using `+ signal_timeframe.to_timedelta()`. ## What Happens After You Return a Signal 1. **Sizing Engine** calculates position size based on your configuration (`MinSizingConfig`, `FixedSizingConfig`, or `RiskPctSizingConfig`) 2. **Risk Engine** validates the trade against your risk rules (`PassthroughRiskConfig` or custom) 3. **OrderEvent** is created if approved (signal is discarded if rejected) 4. **Execution Engine** places the order — simulated in backtesting, real via MT5 in live trading 5. **FillEvent** is generated with execution details (price, commission, etc.) 6. **Portfolio** updates positions, balance, and equity --- # Data Provider Module The **Data Provider** is the heart of market data access in PyEventBT. It abstracts the complexity of data sourcing, seamlessly switching between historical CSV files for backtesting and real-time MetaTrader 5 feeds for live trading. Write your strategy **once**. The Data Provider ensures your logic works identically whether you are running a backtest or trading live. ## Key Features - **Unified API**: A single API for both historical and live data. No code changes required to go live. - **Multi-Timeframe**: Access data from multiple timeframes simultaneously (e.g., trade on H1 while checking trends on D1). - **Auto-Management**: Handles data loading, caching, and updates automatically, so you can focus on strategy logic. - **Indicator Ready**: Returns data structures perfect for computing technical indicators like ATR, RSI, or Moving Averages. ## Accessing Data In your strategy, the data provider is accessible via the `modules` object. ### Get Latest Bars Retrieve a sequence of historical bars, useful for calculating indicators or analyzing trends. ```python @strategy.custom_signal_engine(...) def my_strategy(event: BarEvent, modules: Modules): # Get the last 50 bars for H1 timeframe bars = modules.DATA_PROVIDER.get_latest_bars( symbol=event.symbol, timeframe=StrategyTimeframes.ONE_HOUR, N=50 ) if bars is None or bars.height < 50: return [] # Not enough data yet # Access data columns closes = bars.select('close').to_numpy() highs = bars.select('high').to_numpy() ``` ### Get Single Bar Retrieve just the most recent completed bar. ```python bar = modules.DATA_PROVIDER.get_latest_bar( symbol='EURUSD', timeframe=StrategyTimeframes.ONE_HOUR ) if bar: print(f"Last close: {bar.close}") ``` ### Current Prices Get the absolute latest Bid/Ask prices (Live trading context). ```python # Useful for precise entry/exit in live execution bid = modules.DATA_PROVIDER.get_latest_bid(symbol) ask = modules.DATA_PROVIDER.get_latest_ask(symbol) spread = ask - bid ``` ## Common Use Cases ### Moving Average Crossover ```python def ma_crossover_logic(symbol, modules): bars = modules.DATA_PROVIDER.get_latest_bars( symbol, StrategyTimeframes.ONE_HOUR, 50 ) if bars is None or bars.height < 30: return closes = bars.select('close').to_numpy().flatten() sma_fast = closes[-10:].mean() # 10-period sma_slow = closes[-30:].mean() # 30-period if sma_fast > sma_slow: # Bullish condition pass ``` ### Multi-Timeframe Analysis ```python # Entry Timeframe: 1 Hour hourly_bars = modules.DATA_PROVIDER.get_latest_bars( symbol, StrategyTimeframes.ONE_HOUR, 20 ) # Trend Timeframe: 1 Day daily_bars = modules.DATA_PROVIDER.get_latest_bars( symbol, StrategyTimeframes.ONE_DAY, 20 ) # Check if daily trend is UP before taking hourly buy signals daily_closes = daily_bars.select('close').to_numpy() is_uptrend = daily_closes[-1] > daily_closes[-5] ``` ## Data Sources & Configuration ### Backtesting Mode Reads from CSV files in your configured `csv_dir`. File structure: ``` data/ EURUSD.csv GBPUSD.csv SPX500.csv ``` **CSV Format**: `datetime,open,high,low,close,volume` ### Live Mode Connects directly to the MetaTrader 5 terminal. - **Real-time ticks**: Updates bid/ask instantly. - **Bar generation**: Aggregates ticks into bars automatically. ## Best Practices - Always check if the returned data is `None` or if the list is empty before performing calculations. At the start of a backtest, data might not be available yet. - **Performance**: Request only the `N` bars you need for your calculation. - **Safety**: Use `get_latest_bid`/`ask` for execution logic to ensure you are trading at the most current price. ## Methods Reference ### get_latest_bars(symbol, timeframe, N) -> Bars Returns the most recent `N` bars for a given symbol and timeframe. ### get_latest_bar(symbol, timeframe) -> Bar Returns the single latest completed bar. ### get_latest_tick(symbol) -> dict Returns the most recent tick data (bid, ask, last, volume, time). ### get_latest_bid(symbol) -> Decimal Returns the absolute latest bid price. ### get_latest_ask(symbol) -> Decimal Returns the absolute latest ask price. ### get_latest_datetime(symbol, timeframe) -> Timestamp Returns the timestamp of the last data point. --- # Execution Engine Module The **Execution Engine** is responsible for managing the lifecycle of your orders. It acts as the bridge between your strategy's logic and the market, handling everything from order placement and modifications to position closures and account monitoring. The Execution Engine automatically adapts its behavior: - **Backtesting**: It simulates order fills, slippage, and latencies. - **Live Trading**: It routes orders directly to the MetaTrader 5 terminal. ## Key Capabilities - **Order Connectivity**: Seamlessly routes both Market and Pending (Limit/Stop) orders to the simulated or real broker. - **Position Control**: Granular control to close specific positions, or bulk commands to flatten your entire book. - **Risk Management**: Native support for updating Stop Loss (SL) and Take Profit (TP) levels on active positions. - **Account monitoring**: Access real-time account metrics like Balance, Equity, and Margin directly from your strategy. ## Processing Orders The most common way to execute trades is by returning `SignalEvent` objects from your strategy. The engine handles the rest: 1. **Strategy generates Signal**: You return a `SignalEvent` (Buy/Sell). 2. **Sizing Engine**: The signal is sized (volume calculation) based on your risk rules. 3. **Risk Check**: The sized signal passes through the Risk Manager for final validation. 4. **Order Creation**: If approved, an `OrderEvent` is created. 5. **Execution**: The Execution Engine places the trade. You can also manually cancel orders or close positions using the `modules.EXECUTION_ENGINE` instance: ```python @strategy.custom_signal_engine(...) def my_strategy(event, modules): # Cancel all pending orders before placing new ones modules.EXECUTION_ENGINE.cancel_all_strategy_pending_orders() # ... generate new signals ... return new_signals ``` ## Common Use Cases ### Closing All Positions (end of day) ```python def check_eod_closure(event, modules): current_time = event.datetime.time() if current_time >= time(15, 55): modules.EXECUTION_ENGINE.close_all_strategy_positions() modules.EXECUTION_ENGINE.cancel_all_strategy_pending_orders() return [] ``` ### Trailing Stop Loss ```python def update_trailing_stop(event, modules): positions = modules.EXECUTION_ENGINE._get_strategy_positions() for pos in positions: if pos.type == 'BUY' and event.close > pos.open_price + 20 * pip: new_sl = event.close - 10 * pip if new_sl > pos.sl: modules.EXECUTION_ENGINE.update_position_sl_tp( position_ticket=pos.ticket, new_sl=new_sl, new_tp=pos.tp ) ``` ### Selective Cancellations ```python # Remove all Buy Limit orders for EURUSD modules.EXECUTION_ENGINE.cancel_all_strategy_pending_orders_by_type_and_symbol( order_type='ORDER_TYPE_BUY_LIMIT', symbol='EURUSD' ) ``` ## Account Information For account metrics, use the Portfolio module: ```python # CORRECT way to access account info in your strategy: equity = modules.PORTFOLIO.get_total_equity() ``` ## Methods Reference ### close_all_strategy_positions() -> None Closes every open position associated with the strategy instance. ### close_strategy_long_positions_by_symbol(symbol: str) -> None Closes all BUY positions for a specific symbol. ### close_strategy_short_positions_by_symbol(symbol: str) -> None Closes all SELL positions for a specific symbol. ### close_position(position_ticket: int) -> OrderSendResult Closes a single specific position by its ticket. ### cancel_all_strategy_pending_orders() -> None Cancels all pending orders (Limits/Stops) for the strategy. ### cancel_all_strategy_pending_orders_by_type_and_symbol(order_type: str, symbol: str) -> None Cancels pending orders matching a specific type and symbol. Order types: `ORDER_TYPE_BUY_LIMIT`, `ORDER_TYPE_SELL_LIMIT`, `ORDER_TYPE_BUY_STOP`, `ORDER_TYPE_SELL_STOP`. ### cancel_pending_order(order_ticket: int) -> OrderSendResult Cancels a single specific pending order by its ticket. ### update_position_sl_tp(position_ticket: int, new_sl: float = 0.0, new_tp: float = 0.0) -> None Modifies the Stop Loss (SL) and Take Profit (TP) levels of an existing position. ### enable_trading() / disable_trading() -> None Master switches to allow or block new order execution for the strategy. --- # Portfolio Module The **Portfolio** module is your central hub for account information. It tracks your current positions, active orders, and financial metrics (Balance, Equity, PnL) in real-time. Think of the **Portfolio** as your *read-only* view of the account state (Positions, PnL, Balance). To *change* the state (e.g., place orders, close positions), you use the **Execution Engine**. ## Key Capabilities - **Position Tracking**: Monitor all open positions, including specific counts per symbol (Long/Short/Total). - **Order Management**: Track pending orders to avoid duplicate entries or manage grid strategies. - **Account Financials**: Real-time access to Account Balance, Equity, Unrealized PnL, and Realized PnL. - **Risk Controls**: Use current equity or position counts to enforce strategy-level risk limits. ## Accessing Portfolio Data ### Check Open Positions ```python @strategy.custom_signal_engine(...) def my_strategy(event: BarEvent, modules: Modules): # returns dict: {'BUY': int, 'SELL': int, 'TOTAL': int} positions = modules.PORTFOLIO.get_number_of_strategy_open_positions_by_symbol(event.symbol) if positions['TOTAL'] == 0: return [SignalEvent(...)] return [] ``` ### Monitor Account Health ```python balance = modules.PORTFOLIO.get_account_balance() equity = modules.PORTFOLIO.get_account_equity() if equity < balance * 0.95: print("Drawdown > 5%, halting trading") return [] ``` ### Pending Orders ```python orders = modules.PORTFOLIO.get_number_of_strategy_pending_orders_by_symbol(event.symbol) if orders['TOTAL'] > 0: return [] ``` ## Common Use Cases ### Max Positions Per Symbol ```python def check_max_positions(event, modules, max_positions: int) -> bool: counts = modules.PORTFOLIO.get_number_of_strategy_open_positions_by_symbol(event.symbol) return counts['TOTAL'] >= max_positions ``` ### Strategy-Wide Position Limit ```python def check_portfolio_positions(event, modules, max_positions: int) -> bool: total_positions = 0 universe = ['EURUSD', 'GBPUSD', 'SPX500', 'XAUUSD'] for instrument in universe: p = modules.PORTFOLIO.get_number_of_strategy_open_positions_by_symbol(instrument) total_positions += p['TOTAL'] return total_positions >= max_positions ``` ### Dynamic Position Sizing (Equity Based) ```python current_equity = modules.PORTFOLIO.get_account_equity() risk_amount = current_equity * 0.01 # Risk 1% of current equity ``` ## Methods Reference ### get_positions(symbol='', ticket=None) -> tuple[OpenPosition] Returns detailed objects for specific open positions, optionally filtering by symbol or ticket. ### get_pending_orders(symbol='', ticket=None) -> tuple[PendingOrder] Returns detailed objects for pending orders (Limit/Stop), optionally filtered. ### get_number_of_strategy_open_positions_by_symbol(symbol: str) -> dict[str, int] Returns dict with keys `'BUY'`, `'SELL'`, and `'TOTAL'`. ### get_number_of_strategy_pending_orders_by_symbol(symbol: str) -> dict[str, int] Returns dict with keys `'BUY'`, `'SELL'`, and `'TOTAL'`. ### get_account_balance() -> Decimal Returns the current cash balance (realized PnL only). ### get_account_equity() -> Decimal Returns the floating equity (Balance + Unrealized PnL). ### get_account_unrealised_pnl() -> Decimal Returns the current floating profit/loss across all open positions. ### get_account_realised_pnl() -> Decimal Returns the total profit/loss from closed trades. --- # Signal Engine Module The signal engine is where trading strategy logic lives. It receives bar events and generates trading signals (BUY/SELL) based on strategy rules. ## Main Purpose This module encapsulates the core trading logic, analyzing market data and deciding when to enter or exit positions. It's the "brain" of your trading strategy. ## Key Features - **Event-driven**: Reacts to `BarEvent` objects - **Modular**: Swap signal engines without changing other components - **Custom Strategies**: Implement custom logic via decorators or by extending the interface - **Multi-signal Support**: Can generate multiple signals per bar ## Signal Generation Flow ``` BarEvent -> Signal Engine -> Strategy Logic -> SignalEvent(s) -> Event Queue ``` ## Creating Custom Strategies ### Option 1: Use Decorator ```python @strategy.custom_signal_engine(strategy_id=..., strategy_timeframes=...) def my_strategy(event: BarEvent, modules: Modules): if condition_met: return SignalEvent(symbol=event.symbol, signal_type=SignalType.BUY, ...) ``` ### Option 2: Implement ISignalEngine ```python class MyCustomSignal(ISignalEngine): def generate_signal(self, bar_event: BarEvent, modules: Modules): # Your strategy logic pass ``` ## Pre-built Signal Engines - **MACrossoverConfig**: Moving average crossover strategy - **signal_passthrough**: Passthrough engine for custom decorated strategies ## Usage The signal engine is called automatically by the trading director when bar events arrive. It has access to: - Current bar data - Historical data via data provider - Portfolio state - Indicators module - All other system components through the `modules` object --- # Strategy Module The strategy module is the main entry point for creating and running trading strategies. It provides a high-level API that brings together all system components. ## Key Features - **Simplified API**: High-level methods for common tasks - **Decorator Support**: Use `@strategy.custom_signal_engine` to define strategy logic - **Backtest Execution**: Run historical backtests with one method call - **Live Trading**: Deploy strategies to live markets - **Walk-forward Analysis**: Advanced backtesting with rolling windows - **Optimization**: Built-in support for strategy parameter optimization - **Multi-timeframe**: Handle multiple timeframes in a single strategy ## StrategyTimeframes Available timeframes: `ONE_MIN`, `FIVE_MIN`, `FIFTEEN_MIN`, `THIRTY_MIN`, `ONE_HOUR`, `FOUR_HOUR`, `ONE_DAY`, `ONE_WEEK`, `ONE_MONTH` ## Usage Example ```python from pyeventbt import Strategy strategy = Strategy(logging_level=logging.INFO) @strategy.custom_signal_engine(strategy_id="my_strat", strategy_timeframes=[StrategyTimeframes.ONE_DAY]) def my_strategy(event: BarEvent, modules: Modules): # Your logic here if buy_condition: return SignalEvent(...) # Configure engines strategy.configure_predefined_sizing_engine(MinSizingConfig()) strategy.configure_predefined_risk_engine(PassthroughRiskConfig()) # Run backtest results = strategy.backtest( strategy_id="my_strat", initial_capital=100000, symbols_to_trade=['EURUSD'], csv_dir=None, start_date=datetime(2020, 1, 1), end_date=datetime(2023, 12, 1), account_currency='USD' ) results.plot() ``` --- # Strategy Example: MA Crossover A classic trend-following strategy that goes **long when a fast SMA crosses above a slow SMA**, and **short when it crosses below**. This is the simplest complete strategy you can build with PyEventBT. - **Difficulty**: Beginner - **Timeframe**: Daily (D1) - **Order Type**: Market Orders - **Concepts**: Signal generation, position management, indicator calculation, backtesting ## Strategy Logic 1. Calculate a **fast SMA** (10-period) and a **slow SMA** (30-period) on daily closes 2. If fast SMA > slow SMA -> we want to be **long** 3. If fast SMA < slow SMA -> we want to be **short** 4. Before opening a new position, close any opposite position first 5. Only hold **one position at a time** ## Full Code ```python from pyeventbt import ( Strategy, BarEvent, SignalEvent, Modules, StrategyTimeframes, PassthroughRiskConfig, MinSizingConfig ) from pyeventbt.events.events import OrderType, SignalType from pyeventbt.indicators import SMA from datetime import datetime from decimal import Decimal import logging logger = logging.getLogger("pyeventbt") strategy_id = "ma_crossover" strategy = Strategy(logging_level=logging.INFO) signal_timeframe = StrategyTimeframes.ONE_DAY strategy_timeframes = [signal_timeframe] symbols_to_trade = ['EURUSD'] starting_capital = 100000 fast_ma_period = 10 slow_ma_period = 30 @strategy.custom_signal_engine( strategy_id=strategy_id, strategy_timeframes=strategy_timeframes ) def ma_crossover_strategy(event: BarEvent, modules: Modules): """ Stay long while fast MA > slow MA. Stay short while fast MA < slow MA. Always maintain at most one open position. """ if event.timeframe != signal_timeframe: return symbol = event.symbol signal_events = [] bars_needed = slow_ma_period + 10 bars = modules.DATA_PROVIDER.get_latest_bars( symbol, signal_timeframe, bars_needed ) if bars is None or bars.height < bars_needed: return close_prices = bars.select('close').to_numpy().flatten() fast_ma = SMA.compute(close_prices, fast_ma_period) slow_ma = SMA.compute(close_prices, slow_ma_period) current_fast = fast_ma[-1] current_slow = slow_ma[-1] if current_fast > current_slow: desired_position = "LONG" elif current_fast < current_slow: desired_position = "SHORT" else: return open_positions = modules.PORTFOLIO \ .get_number_of_strategy_open_positions_by_symbol(symbol) signal_type = None if open_positions['LONG'] == 0 and desired_position == "LONG": if open_positions['SHORT'] > 0: modules.EXECUTION_ENGINE \ .close_strategy_short_positions_by_symbol(symbol) signal_type = SignalType.BUY if open_positions['SHORT'] == 0 and desired_position == "SHORT": if open_positions['LONG'] > 0: modules.EXECUTION_ENGINE \ .close_strategy_long_positions_by_symbol(symbol) signal_type = SignalType.SELL if signal_type is None: return if modules.TRADING_CONTEXT == "BACKTEST": time_generated = event.datetime + signal_timeframe.to_timedelta() else: time_generated = datetime.now() last_tick = modules.DATA_PROVIDER.get_latest_tick(symbol) signal_events.append(SignalEvent( symbol=symbol, time_generated=time_generated, strategy_id=strategy_id, signal_type=signal_type, order_type=OrderType.MARKET, order_price=( last_tick['ask'] if signal_type == SignalType.BUY else last_tick['bid'] ), sl=Decimal('0.0'), tp=Decimal('0.0'), )) return signal_events strategy.configure_predefined_sizing_engine(MinSizingConfig()) strategy.configure_predefined_risk_engine(PassthroughRiskConfig()) backtest = strategy.backtest( strategy_id=strategy_id, initial_capital=starting_capital, symbols_to_trade=symbols_to_trade, csv_dir=None, backtest_name=strategy_id, start_date=datetime(2020, 1, 1), end_date=datetime(2023, 12, 1), export_backtest_parquet=False, account_currency='USD' ) backtest.plot() ``` ### Adapt This Strategy | Variation | What to change | | :--- | :--- | | **Different MA periods** | Change `fast_ma_period` and `slow_ma_period` | | **Use EMA instead of SMA** | Replace `SMA` import with `EMA` from `pyeventbt.indicators` | | **Add a stop-loss** | Set the `sl` parameter in `SignalEvent` to a non-zero `Decimal` | | **Trade multiple symbols** | Add symbols to `symbols_to_trade` list | | **Shorter timeframe** | Change `signal_timeframe` to `StrategyTimeframes.ONE_HOUR` | ### Going Live The same strategy code works for live trading — just replace the `strategy.backtest(...)` call: ```python from pyeventbt import Mt5PlatformConfig mt5_config = Mt5PlatformConfig( path="C:\\Program Files\\MetaTrader 5\\terminal64.exe", login=12345, password="your_password", server="YourBroker-Demo", timeout=60000, portable=False ) strategy.run_live( mt5_configuration=mt5_config, strategy_id=strategy_id, initial_capital=100000, symbols_to_trade=symbols_to_trade, heartbeat=0.1 ) ``` Live trading requires MetaTrader 5 installed and running on Windows. Always test on a **demo account** first. --- # Strategy Example: Bollinger Bands Breakout An intraday strategy that places **BUY STOP and SELL STOP orders at the Bollinger Bands** each morning, catching breakouts in either direction. All positions are closed at 21:00. - **Difficulty**: Intermediate - **Timeframe**: Hourly (H1) + Daily (D1) - **Order Type**: Pending STOP Orders - **Concepts**: Multi-timeframe, pending orders, time-based exits, daily state tracking ## Strategy Logic 1. **Each morning at 08:00**, calculate the Bollinger Bands (20-period, 2.5 std dev) on H1 bars 2. Place a **BUY STOP** at the upper band and a **SELL STOP** at the lower band 3. If price breaks out in either direction, the pending order fills automatically 4. **At 21:00**, close all open positions and cancel any unfilled pending orders 5. Reset the daily state and repeat the next morning ## Full Code ```python from pyeventbt import ( Strategy, BarEvent, SignalEvent, Modules, StrategyTimeframes, PassthroughRiskConfig, MinSizingConfig ) from pyeventbt.events.events import OrderType, SignalType from pyeventbt.indicators.indicators import BollingerBands from datetime import datetime, time from decimal import Decimal import logging import numpy as np logger = logging.getLogger("pyeventbt") strategy_id = "bbands_breakout" strategy = Strategy(logging_level=logging.INFO) signal_timeframe = StrategyTimeframes.ONE_HOUR daily_timeframe = StrategyTimeframes.ONE_DAY strategy_timeframes = [signal_timeframe, daily_timeframe] symbols_to_trade = ['EURUSD'] starting_capital = 100000 bb_period = 20 bb_std_dev = 2.5 close_hour = 21 close_minute = 0 order_placement_hour = 8 order_placement_minute = 0 orders_placed_today: dict[str, bool] = { symbol: False for symbol in symbols_to_trade } current_trading_date: dict[str, datetime | None] = { symbol: None for symbol in symbols_to_trade } @strategy.custom_signal_engine( strategy_id=strategy_id, strategy_timeframes=strategy_timeframes ) def bbands_breakout(event: BarEvent, modules: Modules): symbol = event.symbol signal_events = [] current_time = event.datetime.time() current_date = event.datetime.date() if current_trading_date[symbol] != current_date: current_trading_date[symbol] = current_date orders_placed_today[symbol] = False open_positions = modules.PORTFOLIO \ .get_number_of_strategy_open_positions_by_symbol(symbol) pending_orders = modules.PORTFOLIO \ .get_number_of_strategy_pending_orders_by_symbol(symbol) if current_time >= time(close_hour, close_minute): if open_positions['TOTAL'] > 0: modules.EXECUTION_ENGINE.close_all_strategy_positions() if pending_orders['TOTAL'] > 0: modules.EXECUTION_ENGINE.cancel_all_strategy_pending_orders() return if (current_time >= time(order_placement_hour, order_placement_minute) and not orders_placed_today[symbol] and pending_orders['TOTAL'] == 0 and event.timeframe == signal_timeframe): bars_needed = bb_period + 10 indicator_bars = modules.DATA_PROVIDER.get_latest_bars( symbol, signal_timeframe, bars_needed ) if indicator_bars is None or indicator_bars.height < bars_needed: return close = indicator_bars.select('close').to_numpy().flatten() upper, middle, lower = BollingerBands.compute( close, bb_period, bb_std_dev ) current_upper = upper[-1] current_lower = lower[-1] if np.isnan(current_upper) or np.isnan(current_lower): return upper_breakout = Decimal(str(current_upper)) lower_breakout = Decimal(str(current_lower)) if modules.TRADING_CONTEXT == "BACKTEST": time_generated = event.datetime + signal_timeframe.to_timedelta() else: time_generated = datetime.now() signal_events.append(SignalEvent( symbol=symbol, time_generated=time_generated, strategy_id=strategy_id, signal_type=SignalType.BUY, order_type=OrderType.STOP, order_price=upper_breakout, sl=Decimal('0.0'), tp=Decimal('0.0'), )) signal_events.append(SignalEvent( symbol=symbol, time_generated=time_generated, strategy_id=strategy_id, signal_type=SignalType.SELL, order_type=OrderType.STOP, order_price=lower_breakout, sl=Decimal('0.0'), tp=Decimal('0.0'), )) orders_placed_today[symbol] = True return signal_events strategy.configure_predefined_sizing_engine(MinSizingConfig()) strategy.configure_predefined_risk_engine(PassthroughRiskConfig()) backtest = strategy.backtest( strategy_id=strategy_id, initial_capital=starting_capital, symbols_to_trade=symbols_to_trade, csv_dir=None, backtest_name=strategy_id, start_date=datetime(2020, 1, 1), end_date=datetime(2023, 12, 1), export_backtest_parquet=False, account_currency='USD' ) backtest.plot() ``` --- # Using Quantdle Data This example shows how to use Quantdle as your data source — downloading historical data, caching it locally as CSV, and running a backtest on it. PyEventBT backtests run on CSV files. The **`QuantdleDataUpdater`** automates the entire workflow: - **First run**: Downloads all data from Quantdle and saves as CSV - **Subsequent runs**: Uses cached CSV files — no API calls needed - **Date range extension**: Only downloads the missing gap, not the full range - **Format conversion**: Automatically converts Quantdle data to PyEventBT's expected CSV format You can still run any PyEventBT strategy by setting `csv_dir=None` to use the built-in sample dataset. Quantdle is one option, not a requirement. ## Setup 1. Install the Quantdle client: `pip install quantdle` 2. Get your API credentials from quantdle.com ## Usage ```python from pyeventbt import QuantdleDataUpdater from datetime import datetime updater = QuantdleDataUpdater( api_key="your_api_key_here", api_key_id="your_api_key_id_here" ) updater.update_data( csv_dir='./data', symbols=['EURUSD'], start_date=datetime(2020, 1, 1), end_date=datetime(2023, 12, 1), timeframe="1min" ) ``` Then use `csv_dir='./data'` in your `strategy.backtest(...)` call instead of `csv_dir=None`. ## QuantdleDataUpdater Reference ### Constructor ```python QuantdleDataUpdater(api_key: str, api_key_id: str) ``` ### update_data() ```python updater.update_data( csv_dir: str, # Directory for CSV cache symbols: list[str], # e.g., ['EURUSD', 'GBPUSD'] start_date: datetime, end_date: datetime, timeframe: str = "1min" # Also accepts "5min", "1h", "1d" ) ``` --- # Built-in Technical Indicators PyEventBT includes optimized technical indicators using NumPy and Numba: - **SMA** (Simple Moving Average): `SMA.compute(data, period)` - **EMA** (Exponential Moving Average): `EMA.compute(data, period)` - **ATR** (Average True Range): `ATR.compute(high, low, close, period)` - **KAMA** (Kaufman's Adaptive Moving Average): `KAMA.compute(data, period)` - **TRIX** (Triple Exponential Moving Average): `TRIX.compute(data, period)` - **DeMarker**: `DeMarker.compute(high, low, period)` - **RVI** (Relative Vigor Index): `RVI.compute(open, high, low, close, period)` - **BollingerBands**: `BollingerBands.compute(data, period, std_dev)` — returns (upper, middle, lower) ### Import patterns ```python from pyeventbt.indicators import SMA, EMA, ATR, KAMA, TRIX, DeMarker, RVI from pyeventbt.indicators.indicators import BollingerBands ``` ### Usage pattern in strategies ```python bars = modules.DATA_PROVIDER.get_latest_bars(symbol, timeframe, 50) close = bars.select('close').to_numpy().flatten() sma_values = SMA.compute(close, 20) current_sma = sma_values[-1] ``` --- # Configuration Classes ## Sizing Engine Configurations - **MinSizingConfig()**: Uses the broker's minimum lot size for the symbol - **FixedSizingConfig(volume=Decimal('0.1'))**: Fixed lot size for every trade - **RiskPctSizingConfig(risk_pct=Decimal('0.01'))**: Risk a percentage of equity per trade ```python strategy.configure_predefined_sizing_engine(MinSizingConfig()) # or strategy.configure_predefined_sizing_engine(FixedSizingConfig(volume=Decimal('0.1'))) # or strategy.configure_predefined_sizing_engine(RiskPctSizingConfig(risk_pct=Decimal('0.01'))) ``` ## Risk Engine Configurations - **PassthroughRiskConfig()**: Allows all orders (no risk filtering) - Custom risk engine via `@strategy.custom_risk_engine` decorator ```python strategy.configure_predefined_risk_engine(PassthroughRiskConfig()) ``` ## MT5 Platform Configuration (for live trading) ```python from pyeventbt import Mt5PlatformConfig mt5_config = Mt5PlatformConfig( path="C:\\Program Files\\MetaTrader 5\\terminal64.exe", login=12345, password="your_password", server="YourBroker-Demo", timeout=60000, portable=False ) ``` --- # Common Import Patterns ```python # Core imports (used in every strategy) from pyeventbt import ( Strategy, BarEvent, SignalEvent, Modules, StrategyTimeframes, PassthroughRiskConfig, MinSizingConfig, ) # Event type constants from pyeventbt.events.events import OrderType, SignalType # Indicators from pyeventbt.indicators import SMA, EMA, ATR from pyeventbt.indicators.indicators import BollingerBands # Additional sizing configs from pyeventbt import FixedSizingConfig, RiskPctSizingConfig # Live trading from pyeventbt import Mt5PlatformConfig # Data sourcing from pyeventbt import QuantdleDataUpdater # Standard library (commonly needed) from datetime import datetime, time from decimal import Decimal import logging import numpy as np ``` --- # PyEventBT vs Alternatives | Feature | PyEventBT | Backtrader | Zipline | VectorBT | |---|---|---|---|---| | Event-driven | Yes | Yes | Yes | No (vectorized) | | MetaTrader 5 integration | Native | No | No | No | | Live trading | Yes (MT5) | Limited | No | No | | Same code backtest/live | Yes | No | No | N/A | | Python-only (no MQL5) | Yes | Yes | Yes | Yes | | Look-ahead bias prevention | By design | Manual | By design | Manual | | Built-in indicators | Yes | Yes | No | Yes | | Multi-timeframe | Yes | Yes | Limited | Yes | | Pending orders (Limit/Stop) | Yes | Yes | Limited | No | | Position sizing engines | Pluggable | Built-in | Built-in | Manual | | Risk management engines | Pluggable | Limited | Limited | Manual | --- # FAQ **Q: What instruments can I trade?** A: Any instrument available on your MetaTrader 5 broker — Forex pairs (EURUSD, GBPUSD), indices (SPX500, DAX40), commodities (XAUUSD), and more. **Q: Can I use my own data?** A: Yes. Place CSV files in a directory and pass `csv_dir='/path/to/data'` to `strategy.backtest()`. CSV format: `datetime,open,high,low,close,volume`. You can also use Quantdle for automated data downloads. **Q: Does it work on Mac/Linux?** A: Yes for development and backtesting. Live trading requires Windows (MetaTrader 5 dependency). **Q: How do I go from backtest to live?** A: Replace `strategy.backtest(...)` with `strategy.run_live(...)` using an `Mt5PlatformConfig`. The strategy logic stays exactly the same. **Q: Can I trade multiple symbols?** A: Yes. Pass a list of symbols to `symbols_to_trade` (e.g., `['EURUSD', 'GBPUSD', 'XAUUSD']`). Your signal engine receives bars for each symbol. **Q: Can I use multiple timeframes?** A: Yes. Pass multiple timeframes to `strategy_timeframes` (e.g., `[StrategyTimeframes.ONE_HOUR, StrategyTimeframes.ONE_DAY]`). Filter on `event.timeframe` in your signal engine.