-
Notifications
You must be signed in to change notification settings - Fork 10
Custom Strategies
To implement a custom Flashbot strategy, it's important to first understand how they're run by the engine, what guarantees are provided, and what behaviors to expect in all three execution modes: backtest, live, and paper. This page steps through the lifecycle of a strategy and covers all major considerations when writing one.
Contents
After reading this guide, check out Custom Strategy: Market Making tutorial for a step-by-step strategy building guide with code examples.
To implement a strategy, a class needs to extends the
flashbot.engine.Strategy
abstract class. Take a look at the methods in that class to see what can be overridden. A strategy
must implement the Params
inner class and following methods: title
, decodeParams
,
initialize
, and handleData
. Typically strategies also implement the handleEvent
method for
reacting to fills, order cancellations, and errors.
As well as the info
method for generating the dashboard.
Params
is an inner class that every strategy must declare, along with the paramsDecoder
method,
which provides a way to decode JSON into an instance of Params
.
When the strategy is run, either for a backtest or a bot, it is supplied with a JSON params object,
which the engine then decodes using paramsDecoder
and sets as the this.params
instance variable
on the strategy. This happens before initialize
method is invoked, meaning that your initialization
logic (such as what data the strategy subscribes to) can depend on the input params.
This method is called once before a strategy starts running. It's a non-blocking method that
returns the market data subscriptions of this strategy as a Future[Seq[DataPath[Any]]]
.
A DataPath[T]
identifies a stream of MarketData[T]
.
The engine will wait for this Future to complete, and then will resolve the resulting DataPath
s
to an Akka streams source of type Source[MarketData[T], NotUsed]
.
This method is also commonly used to initialize any internal strategy state that might depend on
this.params
, as params are not available at the time the strategy's constructor is called.
Market data resolution is the process of finding an actual data stream for a data path.
The engine resolves market data by calling the resolveMarketData
method on the strategy.
This method has access to the engine's DataServer
and the default implementation in the Strategy
super class is to query the data server for the data stream. This can be overridden if you'd like
to use some custom data stream for this strategy without setting up an actual DataServer
and
corresponding DataSource
.
After data paths are resolved to market data streams, the streams are merged and the items
of the combined stream are sent to the strategy's handleData
method one by one. The way that
the streams are merged depends on which mode the strategy is running in. Read more on this in the
Market Data Ordering section.
The handleData
method is the main function of a strategy. It's purpose is to use incoming data
to place/cancel orders. It has access to a TradingSession
which provides various contextual
information such as the current portfolio, currently open orders, prices, and instruments.
Typically strategies use the market data to build time series indicators, which are used to
determine if an order should be placed. The main methods to place orders are limitOrder
,
limitOrderOnce
, and marketOrder
. The semantics of each of these is explained in the sections
below.
When a strategy receives market data, it usually builds a time series of the price associated
with that data. Market data has a price if it implements the Priced
interface.
Flashbot uses TA4J for time series and indicators. Various
time series helper functions can be used from a strategy by making it extend the TimeSeriesMixin
trait. This makes the record
method available, which does two things:
- Builds up an in-memory TA4J time series, which can be used to calculate indicators
- Saves the time series to the trading session
Report
so that it can analyzed and plotted
A strategy can call the marketOrder
method to buy or sell a certain amount of an asset. It takes
the following arguments:
-
market
- the market (exchange and instrument symbol) to route the order to. -
size
- the size of the order. May be denominated in any asset whose price can be implicitly converted to the market's base asset. Use a positive value for buy orders and negative for sell. -
ctx
- the trading session
This is the primary method for placing limit orders.
It is used to declare the target state limit orders on the exchange.
In other words, this method is idempotent. Calling this method a second time, in the same handleData
call, with the same parameters will have no effect.
Flashbot manages the process of creating and cancelling
actual limit orders on the exchange so that they conform to the limit order targets
declared by this method. Each limit order target is logically identified by the key
parameter and always corresponds to at-most one actual order on the exchange.
It takes the following arguments:
-
market
- the market (exchange and instrument symbol) of the order. -
size
- the size of the order. May be denominated in any asset whose price can be implicitly converted to the market's base asset. Use a positive value for buy orders and negative for sell. -
price
- the price level of the limit order. -
key
- the logical identifier of this limit order target within the givenmarket
. -
postOnly
- whether to allow any portion of this order to execute immediately as a taker. -
ctx
- the trading session instance.
Submits a new limit order to the exchange.
Note that unlike the declarative limitOrder
method, limitOrderOnce
is
not idempotent! This makes it considerably harder to write most strategies, as you'll
have to do your own bookkeeping. It only exists in case lower level control is required.
In general, limitOrder
is the recommended method.
This takes the same arguments that limitOrder
does, with the exception of key
, which will
be randomly generated.
A strategy can react to events that occur during the trading session. A common use of events is to listen for fills and immediately place a hedge order when the initial order is filled.
Check out the StrategyEvent file to see a list of available events.
Since a strategy is just a container for trading logic based on market conditions, it's designed
to not know if it's in backtesting, live, or paper trading mode. Everything that a strategy needs
to make decisions comes from incoming market data, and the TradingSession
variable (ctx
).
The TradingSession
gives information about the context in which the strategy is running.
This includes the current portfolio, all orders that were placed by the strategy, market prices,
and the loaded instruments. It also contains a report of the strategy's activity, and serves as
persistent storage for strategies running as bots.
The configuration and behavior of the TradingSession
depends on it's mode:
A bot is a set of configurations for starting a trading session. When a bot is enabled, it begins a new trading session in either "live" or "paper" mode. If the strategy crashes, or the bot is disabled, that trading session's report is saved to disk. Once the bot is once again enabled, it looks at the report of the previous session to restore it's state.
In "paper" mode, each bot's session will have an isolated portfolio. If a paper bot crashes, it's portfolio is part of the state that gets restored on re-start.
In "live" mode, where the session places orders on real exchanges, the portfolio is not isolated. There is a thread-safe portfolio instance that is shared between all bots throughout the engine. It represents your actual balances on the exchange.
Like bots, backtests are also thin wrappers around a trading session. However, unlike bots, the trading session used by backtests is never persisted. The accumulated report is entirely in memory. In fact, the act of running a backtest never writes anything to disk. Like "paper" bots, backtests run with an isolated portfolio, which is configured by the backtest request itself.
The handleData
and handleEvent
methods will never be called concurrently. It is safe to use
non-thread safe data structures in your strategy.
The trading session maintains an internal order action queue for each exchange. If two market orders are submitted to the same exchange, then the second order will not be sent to the exchange until the first one completes successfully.
This allows the strategy to submit orders in a logical sequence, without having to worry about timing one order to be submitted if/when the previous one completes. This is only the case if all orders in question are on the same exchange. Orders on separate exchanges will be placed into separate queues, which will submit both orders instantly without one waiting on the other.
Strategies can subscribe to multiple market data streams from the initialize
method.
Since handleData
only takes one market data item at a time, these streams must be merged into
one combined market data stream first. The merging algorithm follows these rules, based on the
mode of the trading session:
If it's in backtest mode, then the items are sorted by time, relative to each other, as they are merged.
If it's in either live or paper trading mode (i.e. a bot), then the items are not explicitly sorted
by time as they are merged. This usually doesn't matter, since live data is inherently ordered
by time, but it's important to know, especially when working with sub-second strategies, that
data arriving into the handleData
method is not guaranteed to be strictly increasing by timestamp
when a strategy is live.