Strategy & Signals

Introduction

A Strategy is responsible for generating signals based on the events it receives and is likely the place where you do most software development. The main method that needs to be implemented is the generate method:

override fun generate(event: Event): List<Signal> {

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

Standard 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

This strategy uses two exponential moving averages and whenever they cross over, a BUY or SELL signal is generated.

// Use a EMA Crossover Strategy with predefined look-back periods
val strategy1 = EMAStrategy.PERIODS_12_26

// Use an EMA Crossover Strategy custom look-back periods
val strategy2 = EMAStrategy(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))
}

RSIStrategy

Strategy using the Relative Strength Index of an asset to generate signals. RSI measures the magnitude of recent price changes to evaluate overbought or oversold conditions in the price of a stock or other asset.

If the RSI raises above the configured high threshold (default 70), a sell signal will be generated. And if the RSI falls below the configured low threshold (default 30), a buy signal will be generated.

// Default thresholds values
val strategy1 = RSIStrategy()

// Own defined thresholds
val strategy2 = RSIStrategy(lowThreshold = 25.0, highThreshold = 75.0)

NoSignalStrategy

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

CombinedStrategy

The Roboquant constructor takes only a single strategy as its argument. However, it is easy 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 for example remove duplicate signals).

val strategy = CombinedStrategy(strategy1, strategy2, strategy3)
val roboquant = Roboquant(strategy)

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)
val roboquant = Roboquant(strategy)

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.

SingleAssetStrategy

This is an ideal base class if you only care about a single asset.

HistoricPriceStrategy

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

You can:

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

  • Override the generateRating method if the default Signal is fine, and you only need to provide the rating.

The following code shows an example of each implementation:

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

    /**
     * this method should return a Signal or null. 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.max() -> Signal(asset, Rating.BUY)
            data.last() == data.min() -> Signal(asset, Rating.SELL)
            else -> null
        }
    }

}

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

    /**
     * this method should return a Rating or null.
     */
    override fun generateRating(data: DoubleArray): Rating? {
        return when {
            data.last() == data.max() -> Rating.BUY
            data.last() == data.min() -> Rating.SELL
            else -> null
        }
    }

}

Custom Strategies

When you develop your own Strategy, 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 generate(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 generate(event: Event): List<Signal> {
        val signals = mutableListOf<Signal>()
        for ((asset, priceAction) in event.prices) {

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

            if (currentPrice > 1.05 * previousPrice)
                signals.add(Signal(asset, Rating.BUY))

            if (currentPrice < 0.95 * previousPrice)
                signals.add(Signal(asset, Rating.SELL))

            previousPrices[asset] = currentPrice
        }
        return signals
    }

    // Make sure we clear the previous prices when reset
    override fun reset() {
        previousPrices.clear()
    }

}

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.

Policy based Strategies

There are use cases that require a strategy not only to have access to an Event but also to the Account. These type 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

See the advanced policies for more details.

Signals

A Signal is the outcome of a strategy and is modelled closely after the ratings given by rating agencies. The two mandatory attributes of a Signal are:

  1. The asset for which the signal applies, like a single stock

  2. A rating for that same asset. A 5-point scale used for the Rating of a Signal:

    • BUY: Also known as strong buy and "on the recommended list". Buy is a recommendation to purchase a specific security.

    • SELL: Also known as strong sell, it’s a recommendation to sell a security or to liquidate an asset.

    • HOLD: In general terms, a company with a hold recommendation is expected to perform at the same pace as comparable companies or in-line with the market.

    • UNDERPERFORM: A recommendation that means a stock is expected to do slightly worse than the overall stock market return. Underperform can also be expressed as "moderate sell," "weak hold" and "underweight."

    • OUTPERFORM: Also known as "moderate buy," "accumulate" and "overweight." Outperform is an analyst recommendation meaning a stock is expected to do slightly better than the market return.

      A HOLD rating is not the same as no signal. Only generate hold signals if you think the asset will perform like the market. If your strategy gives no clear indication about the future performance of an asset, don’t generate a signal at all.

Simple Signals

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

val apple = Asset("AAPL")
val signal = Signal(apple, Rating.BUY)

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

// The price when to exit when profitable
val takeProfit: Double = Double.NaN

// The price when to exit when unprofitable
val stopLoss: Double = Double.NaN

// The probability of this signal being correct
val probability: Double = Double.NaN

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

Signal(asset, rating, type, takeProfit, stopLoss, probability, tag)

It is up to the policy if/how these attributes are used when generating the orders. For example, the probability could be used to determine the sizing of the order, a higher probability implying a larger order size.