override fun generate(event: Event): List<Signal>
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:
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 theSignal
that is created. -
the
generateRating
method if the default Signal is fine, and you only want to provide theRating
.
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:
-
The
Asset
to which the signal applies, like a single stock -
A
Rating
for that asset. A 5-point scale (a Kotlin enumeration type) is used for theRating
of aSignal
:-
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.