Skip to content

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.

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

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)

Contains new market data. Your signal engine receives one on every new bar for each registered timeframe.

Fields:

FieldTypeDescription
symbolstrInstrument symbol (e.g., 'EURUSD')
datetimedatetimeTimestamp of the bar
timeframestrTimeframe that produced this bar
dataBarOHLCV data (see below)

The data field contains a Bar object with integer-stored prices for performance. Use the _f properties for float values:

PropertyTypeDescription
data.open_ffloatOpen price
data.high_ffloatHigh price
data.low_ffloatLow price
data.close_ffloatClose price
data.volumeintVolume
data.spread_ffloatSpread
@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()

A trading signal generated by your strategy. This is the main event you create.

Fields:

FieldTypeRequiredDescription
symbolstrYesInstrument to trade
time_generateddatetimeYesWhen the signal should execute (see Signal Timing)
strategy_idstrYesYour strategy’s identifier
signal_typeSignalTypeYesSignalType.BUY or SignalType.SELL
order_typeOrderTypeYesOrderType.MARKET, OrderType.LIMIT, or OrderType.STOP
order_priceDecimalNoTarget price for LIMIT/STOP orders. Default 0.0
slDecimalNoStop-loss price. Default 0.0 (no stop-loss)
tpDecimalNoTake-profit price. Default 0.0 (no take-profit)

Imports:

from pyeventbt import SignalEvent
from pyeventbt.events.events import SignalType, OrderType
from decimal import Decimal

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'),
)

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)
@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

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:

FieldTypeDescription
symbolstrInstrument
time_generateddatetimeSignal timestamp
strategy_idstrStrategy identifier
volumeDecimalPosition size (calculated by Sizing Engine)
signal_typeSignalTypeBUY or SELL
order_typeOrderTypeMARKET, LIMIT, or STOP
order_priceDecimalTarget price
slDecimalStop-loss
tpDecimalTake-profit

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:

FieldTypeDescription
dealDealTypeDealType.IN (open) or DealType.OUT (close)
symbolstrInstrument
time_generateddatetimeExecution timestamp
position_idintPosition identifier
strategy_idstrStrategy identifier
volumeDecimalExecuted volume
priceDecimalExecution price
signal_typeSignalTypeBUY or SELL
commissionDecimalBroker commission
swapDecimalSwap/rollover cost
feeDecimalAdditional fees
gross_profitDecimalGross P&L (for closing trades)
ccystrCurrency of costs/profits

Timer-based event for periodic actions (e.g., daily rebalancing, scheduled reports).

Created by the @strategy.run_every() decorator.

Key fields:

FieldTypeDescription
schedule_timeframeStrategyTimeframesHow often this fires
symbolstrInstrument
timestamppd.TimestampCurrent 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}")

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 open
if modules.TRADING_CONTEXT == "BACKTEST":
time_generated = event.datetime + signal_timeframe.to_timedelta()
else:
# In live trading: execute now
time_generated = datetime.now()
  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