Events
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?
Section titled “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
Section titled “Event Flow”Every bar of market data triggers this pipeline:
BarEvent → Your Strategy → SignalEvent → Sizing Engine → Risk Engine → OrderEvent → Execution Engine → FillEvent → Portfolio- BarEvent — New market data arrives (automatically generated by the Data Provider)
- SignalEvent — Your strategy generates a trading signal (this is what you create)
- OrderEvent — Sizing and risk engines validate the signal and produce an order (automatic)
- FillEvent — The order is executed by the broker, simulated or real (automatic)
- Portfolio Update — Account balance, positions, and equity are updated (automatic)
Event Types
Section titled “Event Types”BarEvent
Section titled “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 |
@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()SignalEvent
Section titled “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 (see Signal Timing) |
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:
from pyeventbt import SignalEventfrom pyeventbt.events.events import SignalType, OrderTypefrom decimal import DecimalCreating signals by order type
Section titled “Creating signals by order type”Execute immediately at the current bid/ask price.
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'),)Execute when price reaches a better level (buy below current, sell above current).
# Buy limit: triggers if price drops to 1.1000signal = 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'),)Execute when price breaks through a level (buy above current, sell below current). Used for breakout strategies.
# Buy stop: triggers if price breaks above 1.1100signal = 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
Section titled “Returning signals from your strategy”Your signal engine can return:
Noneor 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)
@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 signalsOrderEvent
Section titled “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:
- Calculating position size (via the Sizing Engine)
- 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
Section titled “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
Section titled “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 |
@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
Section titled “Signal Timing”The time_generated field on a SignalEvent controls when the signal executes. This is critical for avoiding look-ahead bias in backtesting.
# In backtesting: execute at the NEXT bar's openif modules.TRADING_CONTEXT == "BACKTEST": time_generated = event.datetime + signal_timeframe.to_timedelta()else: # In live trading: execute now time_generated = datetime.now()What Happens After You Return a Signal
Section titled “What Happens After You Return a Signal”- Sizing Engine calculates position size based on your configuration (
MinSizingConfig,FixedSizingConfig, orRiskPctSizingConfig) - Risk Engine validates the trade against your risk rules (
PassthroughRiskConfigor custom) - OrderEvent is created if approved (signal is discarded if rejected)
- Execution Engine places the order — simulated in backtesting, real via MT5 in live trading
- FillEvent is generated with execution details (price, commission, etc.)
- Portfolio updates positions, balance, and equity