Skip to content

Core Architecture

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.

  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 SignalEvents. (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.

The architecture is designed so your strategy code doesn’t change between modes. The framework swaps the internal implementations:

ComponentBacktest ModeLive Mode
Data ProviderReads from CSV filesConnects to MT5 terminal
Execution EngineSimulates fills using MT5 modelRoutes orders to MT5 broker
PortfolioTracks simulated positionsTracks real positions
Your StrategyNo changesNo changes
# 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,
)

Your signal engine receives a modules parameter that provides access to framework components at runtime:

ModuleWhat it gives you
modules.DATA_PROVIDERMarket data: get_latest_bars(), get_latest_tick(), get_latest_bid(), get_latest_ask()
modules.PORTFOLIOAccount state: get_positions(), get_account_balance(), get_number_of_strategy_open_positions_by_symbol()
modules.EXECUTION_ENGINEOrder management: close_all_strategy_positions(), cancel_all_strategy_pending_orders()
modules.TRADING_CONTEXTMode 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()