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().
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.
The architecture separates what you control from what the framework handles automatically.
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 BarEvents, accesses market data and portfolio state via modules, and returns SignalEvents when conditions are met.
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.
Every bar of market data triggers this pipeline. You only create Step 2 — everything else is automatic.
BarEvent — Data Provider emits a new bar. The Trading Director routes it to your Signal Engine.
SignalEvent — Your strategy logic runs. If conditions are met, you return one or more SignalEvents. (This is your code.)
Sizing — The Portfolio Handler passes your signal to the Sizing Engine, which calculates position volume.
Risk Validation — The Risk Engine checks the sized order against your risk rules. If rejected, the signal is discarded.
OrderEvent — An approved order is created and sent to the Execution Engine.
FillEvent — The broker (simulated or real) executes the order and returns fill details: price, commission, swap.
Portfolio Update — The Portfolio records the new position and updates balance, equity, and P&L.
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 |
# Backtest — runs on historical CSV databacktest = 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 callstrategy.run_live( mt5_configuration=mt5_config, strategy_id=strategy_id, initial_capital=100000, symbols_to_trade=['EURUSD'], heartbeat=0.1,)modules ObjectYour 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" |
@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()