roboquant avatar

Strategy & Signals

Introduction

A strategy 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 task of a Policy.

The main method that needs to be implemented in the Strategy interface 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 open orders, positions 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 = EMAStrategy.PERIODS_5_15
val strategy2 = EMAStrategy.PERIODS_12_26
val strategy3 = EMAStrategy.PERIODS_50_200

// Use an EMA crossover strategy with custom periods
val strategy4 = 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.

AssetFilterStrategy

If you don’t want to receive all price actions, you can wrap your strategy in a AssetFilterStrategy that allows to filter out certain price actions based on asset and/or time.

// Only receive price actions denoted in USD
val strategy1 = AssetFilterStrategy(EMAStrategy(), AssetFilter.includeCurrencies(Currency.USD))

// use the filter extension function to exclude price actions for Tesla
val strategy2 = EMAStrategy().filter { asset, _ ->
    asset.symbol != "TSLA"
}

// use CombinedStrategy and filter to create more complex strategies
val t = Instant.parse("2010-01-01T00:00:00Z")
val strategy = CombinedStrategy(
    EMAStrategy.PERIODS_12_26.filter { _, time -> time < t },
    EMAStrategy.PERIODS_5_15.filter { _, time -> time >= t }
)

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 your strategy is only interested in a single asset. If an event doesn’t contain a price-action for that particular asset, the generate method will not be invoked.

class MyStrategy(asset: Asset) : SingleAssetStrategy(asset) {

    override fun generate(priceAction: PriceAction, time: Instant): Signal? {
        TODO("Your implementation goes here")
    }

}

val apple = Asset("AAPL")
val strategy = MyStrategy(apple)

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. 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 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 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 to which the signal applies, like a single stock

  2. A Rating for that asset. A 5-point scale (a Kotlin enumeration type) is 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 an order, a higher probability implying a larger order size.