Skip to content

Custom Strategies

Alex Lopatin edited this page Feb 8, 2019 · 4 revisions

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

  1. Basics
    1. Params
    2. Initialization
    3. Resolving Market Data
    4. Handling Market Data
    5. Time Series
    6. Market Orders
    7. Limit Orders
    8. Events
  2. Trading Sessions
    1. Bots
    2. Backtests
  3. Guarantees
    1. Thread Safety
    2. Order Execution
    3. Market Data Ordering

After reading this guide, check out Custom Strategy: Market Making tutorial for a step-by-step strategy building guide with code examples.


Basics

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

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.

Initialization

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 DataPaths 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.

Resolving Market Data

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.

Handling Market Data

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.

Time Series

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:

  1. Builds up an in-memory TA4J time series, which can be used to calculate indicators
  2. Saves the time series to the trading session Report so that it can analyzed and plotted

Market Orders

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

Limit Orders

limitOrder

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 given market.
  • postOnly - whether to allow any portion of this order to execute immediately as a taker.
  • ctx - the trading session instance.

limitOrderOnce

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.

Events

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.

Trading Sessions

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:

Bots

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.

Backtests

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.

Guarantees

Thread Safety

The handleData and handleEvent methods will never be called concurrently. It is safe to use non-thread safe data structures in your strategy.

Order Execution

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.

Market Data Ordering

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.