roboquant avatar

Strategy & Signals

Introduction

A strategy in general is a systematic approach to buying and selling assets, such as stocks, bonds, currencies, or commodities, in order to achieve a specific financial goal. A strategy typically involves the use rules, indicators, and other technical or fundamental analysis tools to identify potential trades.

A Strategy in roboquant is the component responsible for generating signals based on the events it receives and is likely the place where you do most of your software development and differentiate from other algo-traders.

a Strategy in roboquant doesn’t generate orders, that is the responsibility of a Trader.

The main method that needs to be implemented in the Strategy interface is the generate method:

override fun createSignals(event: Event): List<Signal> {
    TODO("Not yet implemented")
}

Since a Strategy only has access to an Event and not the Account, it has no knowledge of things like open orders, positions, trades or buying power. As a consequence a Strategy is a generic component that can be easily shared or re-used.

Out-of-the-box Strategies

There are several commonly used strategies included with roboquant. This section highlights some of them, and using them is a great way to get started.

EMAStrategy

An EMA (Exponential Moving Average) crossover strategy is a popular technical analysis strategy that involves the use of moving averages of different periods to identify trends and potential buy or sell signals.

In this strategy, traders typically use two EMAs: a shorter-term EMA and a longer-term EMA. The shorter-term EMA reacts more quickly to price changes, while the longer-term EMA smoothes out the price movements over a longer period.

When the shorter-term EMA crosses above the longer-term EMA, it is seen as a bullish signal, indicating that the trend is turning upward and traders may want to consider buying the asset. Conversely, when the shorter-term EMA crosses below the longer-term EMA, it is seen as a bearish signal, indicating that the trend is turning downward and traders may want to consider selling the asset.

Traders can adjust the periods of the EMAs depending on their trading style and the time frame of the chart they are analyzing. For example, a popular combination is the 50-day EMA and the 200-day EMA, which can be used to identify long-term trends.

// Use an EMA-crossover strategy with predefined periods
val strategy1 = EMACrossover.PERIODS_5_15
val strategy2 = EMACrossover.PERIODS_12_26
val strategy3 = EMACrossover.PERIODS_50_200

// Use an EMA crossover strategy with custom periods
val strategy4 = EMACrossover(fastPeriod = 20, slowPeriod = 50)

TaLibStrategy

TaLibStrategy is short for Technical Analysis Strategy and is a convenient way of using the ta-lib indicators in a strategy by defining the BUY and/or SELL condition.

The ta-lib library comes with over 150 indicators, and these can be combined anyway you like in a TaLibStrategy.

val shortTerm = 30
val longTerm = 50

// Make sure the strategy collects enough data
// for all the used indicators to work correctly
val strategy = TaLibStrategy(longTerm)

// When to generate a BUY signal
strategy.buy { series ->
    cdlMorningStar(series) || cdl3WhiteSoldiers(series)
}

// When to generate a SELL signal
strategy.sell { series ->
    cdl3BlackCrows(series) || (cdl2Crows(series) &&
            sma(series.close, shortTerm) < sma(series.close, longTerm))
}

NoSignalStrategy

This strategy doesn’t perform any action and will never generate a signal. If you develop all your logic in a Trader and don’t require a strategy, use this strategy when you create a Roboquant instance. See also Trader based Strategies.

CombinedStrategy

The Roboquant constructor takes only a single strategy as its argument. However, it is possible to combine multiple strategies in your run using the CombinedStrategy class.

This class will invoke each strategy in sequence and return the combined signals in the same order as the strategies are provided. There is no additional signal processing (like removing duplicate signals) performed.

val strategy = CombinedStrategy(strategy1, strategy2, strategy3)

ParallelStrategy

This is similar to the CombinedStrategy, but this time the strategies are run in parallel and not sequential. The order of returned signals follows the same logic as with a CombinedStrategy.

The main benefit is that this strategy can speed up the processing, assuming the hardware has sufficient CPU resources available.

val strategy = ParallelStrategy(strategy1, strategy2, strategy3)

Extending Strategies

Next to the standard strategies, there are also strategies included that can be the foundation for your own strategy. You extend these classes and add your own logic to them. These base strategies take care of some repetitive tasks and make it quicker to develop your own strategies.

HistoricPriceStrategy

A useful base strategy is HistoricPriceStrategy. This strategy collects historic prices for a predefined look-back period before invoking your method. Only once enough historic data is collected, your method will be invoked. That way, you don’t have to deal with some of the complexity that comes with an event-based trading engine.

You can override one of two methods:

  • the generateSignal method if you want full control on the Signal that is created.

  • the generateRating method if the default Signal is fine, and you only want to provide the Rating.

The following code shows an example of each approach:

class MyStrategy1(lookBack: Int = 10) : HistoricPriceStrategy(lookBack) {

    /**
     * This method should return a [Signal] or null. A signal can contain
     * additional attributes to the ones shown in this example.
     */
    override fun generateSignal(asset: Asset, data: DoubleArray): Signal? {
        return when {
            data.last() == data.maxOrNull() -> Signal.buy(asset)
            data.last() == data.minOrNull() -> Signal.sell(asset)
            else -> null
        }
    }

}

class MyStrategy2(lookBack: Int = 10) : HistoricPriceStrategy(lookBack) {

    /**
     * This method should return a rating value or null.
     */
    override fun generateRating(data: DoubleArray): Double? {
        return when {
            data.last() == data.maxOrNull() -> 1.0
            data.last() == data.minOrNull() -> -1.0
            else -> null
        }
    }

}

Custom Strategies

When you develop your own Strategy from scratch, you will need to implement at least the generate method that is defined in the Strategy interface. This method should return zero or more signals.

class MyStrategy : Strategy {

    override fun createSignals(event: Event):List<Signal> {
        TODO("Not yet implemented")
    }

}

Now let’s see how a trend-following Strategy would look like that would:

  • generate a BUY-signal every time the current price is more than 5% higher than the previous price for a given Asset

  • and a SELL-signal if it is more than 5% lower

class MyStrategy : Strategy {

    private val previousPrices = mutableMapOf<Asset, Double>()

    override fun createSignals(event: Event): List<Signal> {
        val signals = mutableListOf<Signal>()
        for ((asset, priceItem) in event.prices) {

            val currentPrice = priceItem.getPrice()
            val previousPrice = previousPrices.getOrDefault(asset, currentPrice)

            if (currentPrice > 1.05 * previousPrice)
                signals.add(Signal.buy(asset))

            if (currentPrice < 0.95 * previousPrice)
                signals.add(Signal.sell(asset))

            previousPrices[asset] = currentPrice
        }
        return signals
    }

}

As you can see, a Strategy can maintain state, like previousPrices in the above example. You have to ensure you reset the state when a Lifecycle event is called, like the reset method in the above example.

Trader based Strategies

There are use cases that require a strategy not only to have access to an Event but also to the Account. These types of strategies can be implemented in roboquant, but they should be implemented as a policy instead.

Some reasons you might need to use a policy:

  • Access to the Account instance is required, for example, to re-balance a portfolio

  • Directly generate orders instead of signals

Signals

A Signal is the outcome of a strategy and contain the following properties:

  1. The Asset to which the signal applies, like a single stock

  2. A Rating for that asset, a float typically between -1 (Sell) and 1 (Buy).

  3. Type of Signal (exit, entry or both)

Simple Signals

When instantiating a Signal, only the Asset and Rating are required parameters and in many cases sufficient:

val apple = Stock("AAPL")
val signal = Signal.buy(apple)

In the above example, the asset is instantiated. But typically they are re-used from the actions found in the event.

Advanced Signals

Besides the mandatory Rating and Asset attributes of a signal instance, a signal can optionally contain the following attributes (with their default values):

// Type of signal: ENTRY, EXIT or BOTH
val type: SignalType = SignalType.BOTH

// An arbitrary string, for example, to trace the strategy that generated this signal
val tag: String = ""

// A double value
val rating = 1.0

Signal(asset, rating, type, tag)

Warmup

Sometimes before going live, you want to ensure your strategy already has enough historical data to start generating signals from the start.

A separate run can achieve this you start live trading.

val strategy = EMACrossover()

// Run a warmup over the past 30 days with the default SimBroker, so no live orders are generated
val past = Timeframe.past(30.days)
run(historicFeed, strategy, timeframe = past)

// Start the live trading with a real broker over the next 8 hours, now the strategy is warmed-up
val next = Timeframe.next(8.hours)
run(liveFeed, strategy, timeframe = next)
Make sure to set reset to false when starting the live run. Otherwise, the historic data will be deleted before the run is even started.