Strategy & Signals

Introduction

A Strategy is likely the place where you do most software development. It is responsible for generating signals based on the events it receives.

Since a Strategy only has access to an Event and not the Account, it has no knowledge of concepts 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 highlight some of them, and using them is a great way to get started.

EMACrossover

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

// Use a EMACrossover with predefined look-back periods
val strategy1 = EMACrossover.EMA_12_26

// Use a EMACrossover with custom look-back periods
val strategy2 = 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))
}

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 raise 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

Combining Strategies

The Roboquant constructor takes only a single strategy as its parameter. However, it is easy to combine multiple strategies in your run using the composition pattern.

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

Extending Strategies

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

A useful base strategies 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.

/**
 * Trend following strategy
 */
class MyStrategy1(lookBack:Int= 10) : HistoricPriceStrategy(lookBack) {

    /**
     * This will only be invoked once there is enough historic data available as specified
     * by the lookBack parameter
     */
    override fun generate(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
        }
    }

}

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, the 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 and not strategy. 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 policy documentation for more details.

Signals

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

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

  2. A rating for that same asset. There is 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 market conform. If your strategy gives no clear indication about the future performance of an asset, don’t generate a signal at all.

Simple Signals

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

Advanced Signals

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

val type: SignalType = SignalType.BOTH
val takeProfit: Double = Double.NaN
val stopLoss: Double = Double.NaN
val probability: Double = Double.NaN
val source: String = ""
Signal(asset, rating, type, takeProfit, stopLoss, probability, source)

It is up to the policy how/if to use these attributes when generating the orders. But for example the probability could be used to determine the sizing of the order, a high probability means a larger size.