Quickstart
Tutorial for PoseiTrader a high-performance algorithmic trading platform and event driven backtester.
Overview
This quickstart tutorial steps through how to get up and running with PoseiTrader backtesting using FX data. To support this, some pre-loaded test data is available using the standard Posei persistence format (Parquet).
Prerequisites
- Python 3.11+ installed
-
PoseiTrader
latest release installed (
pip install -U posei_trader
) -
JupyterLab
or similar installed (
pip install -U jupyterlab
)
1. Get sample data
To save time, we have prepared a script to load sample data into the Posei format for use with this example. First, download and load the data by running the next cell (this should take ~ 1-2 mins):
!apt-get update && apt-get install curl -y
!curl https://raw.githubusercontent.com/poseissystems/posei_data/main/posei_data/hist_data_to_catalog.py | python -
For further details on how to load data into Posei, see Loading External Data guide.
from posei_trader.backtest.node import BacktestDataConfig
from posei_trader.backtest.node import BacktestEngineConfig
from posei_trader.backtest.node import BacktestNode
from posei_trader.backtest.node import BacktestRunConfig
from posei_trader.backtest.node import BacktestVenueConfig
from posei_trader.config import ImportableStrategyConfig
from posei_trader.config import LoggingConfig
from posei_trader.model import Quantity
from posei_trader.model import QuoteTick
from posei_trader.persistence.catalog import ParquetDataCatalog
2. Set up a Parquet data catalog
If everything worked correctly, you should be able to see a single EUR/USD instrument in the catalog.
# You can also use a relative path such as `ParquetDataCatalog("./catalog")`,
# for example if you're running this notebook after the data setup from the docs.
# catalog = ParquetDataCatalog("./catalog")
catalog = ParquetDataCatalog.from_env()
catalog.instruments()
3. Write a trading strategy
PoseiTrader includes many indicators built-in, in this example we will use the MACD indicator to build a simple trading strategy.
You can read more about
MACD here, this indicator merely serves as an example
without any expected alpha. There is also a way of
registering indicators to receive certain data
types, however in this example we manually pass
the received QuoteTick
to the
indicator in the
on_quote_tick
method.
from posei_trader.core.message import Event
from posei_trader.indicators.macd import MovingAverageConvergenceDivergence
from posei_trader.model import InstrumentId
from posei_trader.model import Position
from posei_trader.model.enums import OrderSide
from posei_trader.model.enums import PositionSide
from posei_trader.model.enums import PriceType
from posei_trader.model.events import PositionOpened
from posei_trader.trading.strategy import Strategy
from posei_trader.trading.strategy import StrategyConfig
class MACDConfig(StrategyConfig):
instrument_id: InstrumentId
fast_period: int = 12
slow_period: int = 26
trade_size: int = 1_000_000
entry_threshold: float = 0.00010
class MACDStrategy(Strategy):
def __init__(self, config: MACDConfig):
super().__init__(config=config)
# Our "trading signal"
self.macd = MovingAverageConvergenceDivergence(
fast_period=config.fast_period, slow_period=config.slow_period, price_type=PriceType.MID
)
self.trade_size = Quantity.from_int(config.trade_size)
# Convenience
self.position: Position | None = None
def on_start(self):
self.subscribe_quote_ticks(instrument_id=self.config.instrument_id)
def on_stop(self):
self.close_all_positions(self.config.instrument_id)
self.unsubscribe_quote_ticks(instrument_id=self.config.instrument_id)
def on_quote_tick(self, tick: QuoteTick):
# You can register indicators to receive quote tick updates automatically,
# here we manually update the indicator to demonstrate the flexibility available.
self.macd.handle_quote_tick(tick)
if not self.macd.initialized:
return # Wait for indicator to warm up
# self._log.info(f"{self.macd.value=}:%5d")
self.check_for_entry()
self.check_for_exit()
def on_event(self, event: Event):
if isinstance(event, PositionOpened):
self.position = self.cache.position(event.position_id)
def check_for_entry(self):
# If MACD line is above our entry threshold, we should be LONG
if self.macd.value > self.config.entry_threshold:
if self.position and self.position.side == PositionSide.LONG:
return # Already LONG
order = self.order_factory.market(
instrument_id=self.config.instrument_id,
order_side=OrderSide.BUY,
quantity=self.trade_size,
)
self.submit_order(order)
# If MACD line is below our entry threshold, we should be SHORT
elif self.macd.value < -self.config.entry_threshold:
if self.position and self.position.side == PositionSide.SHORT:
return # Already SHORT
order = self.order_factory.market(
instrument_id=self.config.instrument_id,
order_side=OrderSide.SELL,
quantity=self.trade_size,
)
self.submit_order(order)
def check_for_exit(self):
# If MACD line is above zero then exit if we are SHORT
if self.macd.value >= 0.0:
if self.position and self.position.side == PositionSide.SHORT:
self.close_position(self.position)
# If MACD line is below zero then exit if we are LONG
else:
if self.position and self.position.side == PositionSide.LONG:
self.close_position(self.position)
def on_dispose(self):
pass # Do nothing else
Configuring backtests
Now that we have a trading strategy and data, we
can begin to configure a backtest run. Posei uses
a BacktestNode
to orchestrate
backtest runs, which requires some setup. This may
seem a little complex at first, however this is
necessary for the capabilities that Posei strives
for.
To configure a BacktestNode
, we first
need to create an instance of a
BacktestRunConfig
, configuring the
following (minimal) aspects of the backtest:
-
engine
: The engine for the backtest representing our core system, which will also contain our strategies -
venues
: The simulated venues (exchanges or brokers) available in the backtest -
data
: The input data we would like to perform the backtest on
There are many more configurable features which will be described later in the docs, for now this will get us up and running.
4. Configure venue
First, we create a venue configuration. For this
example we will create a simulated FX ECN. A venue
needs a name which acts as an ID (in this case
SIM
), as well as some basic
configuration, e.g. the account type (CASH
vs MARGIN
), an optional base
currency, and starting balance(s).
FX trading is typically done on margin with Non-Deliverable Forward, Swap or CFD type instruments.
venue = BacktestVenueConfig(
name="SIM",
oms_type="NETTING",
account_type="MARGIN",
base_currency="USD",
starting_balances=["1_000_000 USD"]
)
5. Configure data
We need to know about the instruments that we
would like to load data for, we can use the
ParquetDataCatalog
for this.
instruments = catalog.instruments()
instruments
Next, we need to configure the data for the backtest. Posei is built to be very flexible when it comes to loading data for backtests, however this also means some configuration is required.
For each tick type (and instrument), we add a
BacktestDataConfig
. In this instance
we are simply adding the QuoteTick
(s)
for our EUR/USD instrument:
from posei_trader.model import QuoteTick
data = BacktestDataConfig(
catalog_path=str(catalog.path),
data_cls=QuoteTick,
instrument_id=instruments[0].id,
end_time="2020-01-10",
)
6. Configure engine
Then, we need a
BacktestEngineConfig
which represents
the configuration of our core trading system. Here
we need to pass our trading strategies, we can
also adjust the log level and configure many other
components (however, it's also fine to use
the defaults):
Strategies are added via the
ImportableStrategyConfig
, which
enables importing strategies from arbitrary files
or user packages. In this instance, our
MACDStrategy
is defined in the
current module, which python refers to as
__main__
.
# PoseiTrader currently exceeds the rate limit for Jupyter notebook logging (stdout output),
# this is why the `log_level` is set to "ERROR". If you lower this level to see
# more logging then the notebook will hang during cell execution. A fix is currently
# being investigated which involves either raising the configured rate limits for
# Jupyter, or throttling the log flushing from Posei.
# https://github.com/jupyterlab/jupyterlab/issues/12845
# https://github.com/deshaw/jupyterlab-limit-output
engine = BacktestEngineConfig(
strategies=[
ImportableStrategyConfig(
strategy_path="__main__:MACDStrategy",
config_path="__main__:MACDConfig",
config={
"instrument_id": instruments[0].id,
"fast_period": 12,
"slow_period": 26,
},
)
],
logging=LoggingConfig(log_level="ERROR"),
)
7. Run backtest
We can now pass our various config pieces to the
BacktestRunConfig
. This object now
contains the full configuration for our backtest.
config = BacktestRunConfig(
engine=engine,
venues=[venue],
data=[data],
)
The BacktestNode
class will
orchestrate the backtest run. The reason for this
separation between configuration and execution is
the BacktestNode
, which enables
running multiple configurations (different
parameters or batches of data). We are now ready
to run some backtests.
from posei_trader.backtest.results import BacktestResult
node = BacktestNode(configs=[config])
# Runs one or many configs synchronously
results: list[BacktestResult] = node.run()
8. Analyze results
Now that the run is complete, we can also directly
query for the BacktestEngine
(s) used
internally by the BacktestNode
by
using the run configs ID.
The engine(s) can provide additional reports and information.
from posei_trader.backtest.engine import BacktestEngine
from posei_trader.model import Venue
engine: BacktestEngine = node.get_engine(config.id)
engine.trader.generate_order_fills_report()
engine.trader.generate_positions_report()
engine.trader.generate_account_report(Venue("SIM"))