If there is one thing that prevents algo-traders from going live, it is that there is not a robust policy in place that handles all the possible edge-cases. The logic required for a robust policy is anything but trivial and should always incorporate extensive testing. |
Policy
What is a policy?
The Policy
is responsible for creating the orders that will be placed at the broker. The most common use case is that a Policy
does this based on the signals it receives from a Strategy
(but there are other use cases).
A good Policy
implementation is key in making your solution as robust as possible. Important things to consider when implementing a policy:
-
How to manage overall risk and exposure when market conditions change
-
What is a good allocation strategy, so how much of your buying power do you allocate to a certain asset
-
What order types to create
-
How to handle new orders when there are still open orders (for the same asset)
-
How to limit the maximum number of orders sent to a broker (circuit breaker)
-
How to deal with conflicting signals from strategies
-
How to handle yo-yo signals (buy-sell-buy-sell) that occur in a short timeframe
-
How to ensure there is still enough buying power left to avoid margin calls
Out-of-the-box policies
FlexPolicy
This is the default policy that will be used if no other policy is specified when creating an instance of the Roboquant
class. It provides many constructor parameters that influence its behavior.
val policy = FlexPolicy {
orderPercentage = 0.01
shorting = true
priceType = "OPEN"
fractions = 4
oneOrderOnly = true
safetyMargin = 0.1
minPrice = 1000.USD
}
You can also extend the FlexPolicy and overwrite some of its methods. For example, to create different order types, you would implement something like this:
class MyFlexPolicy : FlexPolicy() {
override fun createOrder(signal: Signal, size: Size, priceAction: PriceAction): Order? {
// We don't short in this example, and exit orders are already covered by the bracket order
if (size < 0) return null
val asset = signal.asset
val price = priceAction.getPrice(config.priceType)
// Create a bracket order with an additional take-profit and stop-loss defined
return BracketOrder(
LimitOrder(asset, size, price), // limit order at current price for entry
TrailOrder(asset, -size, 0.05), // 5% trail order for take profit
StopOrder(asset, -size, price * 0.98) // stop loss order 2% under current price
)
}
}
Chaining
There are several extension methods available that can add functionality to any type of policy by creating a chain of policies in which each policy has a particular tasks. Typically, the extension methods perform one of two tasks:
-
Remove signals before the next policy is invoked
-
Remove orders before they are handed to the broker
val policy = MyPolicy()
.resolve(SignalResolution.NO_CONFLICTS) // remove all conflicting signals
.circuitBreaker(10, 1.days) // stop orders if there are too many created
Fractional Order sizes
Roboquant doesn’t make assumptions on the minimal order and position sizes. They are not limited to integers only, and so there is no restriction on using a broker that supports fractional trading.
Since it is a Policy
instance that creates the orders, here you can also put any type of order size logic you require.
The FlexPolicy allows you to specify the number of decimals for sizing calculations, allowing to easily enable fractional orders.
|
Custom Policies
In case the out-of-the-box policies will not do, you can implement your own Policy
. You only have to implement a single method named act
. This method has access to the generated signals and the Account
and Event
object.
class MyPolicy : Policy {
override fun act(signals: List<Signal>, account: Account, event: Event): List<Order> {
val orders = mutableListOf<Order>()
// Your code goes here
return orders
}
}
So a very naive and not robust implementation could look something like this:
class MyNaivePolicy : Policy {
override fun act(signals: List<Signal>, account: Account, event: Event): List<Order> {
val orders = mutableListOf<Order>()
for (signal in signals) {
val size = if (signal.rating.isPositive) 100 else -100
val order = MarketOrder(signal.asset, size)
orders.add(order)
}
return orders
}
}
Or using the more concise Kotlin way of doing things:
class MyNaivePolicy : Policy {
override fun act(signals: List<Signal>, account: Account, event: Event): List<Order> {
return signals.map {
val size = if (it.rating.isPositive) 100 else -100
MarketOrder(it.asset, size)
}
}
}
The following example is more realistic and shows an implementation that calculates the ATR (Average True Range). The ATR is then used to set the limit amount in a Limit Order.
This example uses the FlexPolicy
as its base class that will take care of much of the logic like sizing and dealing with concurrent orders.
/**
* Custom Policy that extends the FlexPolicy and uses the ATR (Average True Range)
* to set the limit amount of a LimitOrder.
*/
class SmartLimitPolicy(val atrPercentage: Double = 0.02, val windowSize: Int = 5) : FlexPolicy() {
// Keep track of historic prices per asset
private var prices = AssetPriceBarSeries(windowSize + 1)
// Use TaLib for calculation of the ATR
private val taLib = TaLib()
override fun act(signals: List<Signal>, account: Account, event: Event): List<Order> {
// Update prices, so we have them available when the createOrder is invoked.
prices.addAll(event)
// Call the regular signal processing
return super.act(signals, account, event)
}
/**
* Override the default behavior of creating a simple MarkerOrder. Create limit BUY and
* SELL orders with the actual limit based on the ATR of the underlying asset.
*/
override fun createOrder(signal: Signal, size: Size, priceAction: PriceAction): Order? {
val asset = signal.asset
val price = priceAction.getPrice(config.priceType)
// We set a limit based on the ATR. The higher the ATR, the more the limit price
// will be distanced from the current price.
val priceBarSerie = prices.getValue(asset)
if (!priceBarSerie.isFull()) return null
val atr = taLib.atr(priceBarSerie, windowSize)
val limit = price - size.sign * atr * atrPercentage
return LimitOrder(asset, size, limit)
}
override fun reset() {
prices.clear()
super.reset()
}
}
When developing custom policies, they should not only be robust and deal with corner-cases, but they should also be explainable. Back-testing over large amounts of data is already challenging enough without having a Policy
in place whose behavior is very difficult to wrap your head around. So the KISS design principle (Keep It Simple, Stupid
) applies to policies.
Things you might want to check before creating new orders in your policy and take the appropriate action:
-
Are there pending open orders for that same asset?
-
What are the total numbers of open orders?
-
How many orders have been created in the last x minutes?
-
How many open positions are there right now?
-
How much buying power is left?
-
What is the unrealized PNL of my open positions?
-
What is the realized PNL in the last x minutes?
See also how to use the account to help answer some of the above questions/
Advanced Policies
Most commonly, a Policy
is used to transform signals it receives from the Strategy
into orders. But there are cases where the policy is not using Signals to create orders. Common use cases are:
-
Re-balancing your portfolio at regular intervals (like monthly) based on some criteria. Since a
Strategy
doesn’t have access to anAccount
and therefore also not to the positions in theAccount
, this needs to be done in aPolicy
. -
Advanced machine learning techniques like reinforcement learning that train the algorithm to directly create orders based on events (without creating intermediate signals).
In these cases you don’t require a Strategy
and you can implement all the logic in the Policy
instead. You can use the NoSignalStrategy
as strategy that doesn’t perform any action.
See also the Account description on how to use information available in the account object to make decisions.
The following example shows the boilerplate code for a policy that at a regular interval (20 days) re-balances the portfolio:
class MyPolicy : Policy {
private var rebalanceDate = Instant.MIN
private val holdingPeriod = 20.days
/**
* Based on some logic determine the target portfolio
*/
fun getTargetPortfolio(): List<Position> {
TODO("your logic goes here")
}
override fun act(signals: List<Signal>, account: Account, event: Event): List<Order> {
if (event.time < rebalanceDate) return emptyList()
rebalanceDate = event.time + holdingPeriod
val targetPortfolio = getTargetPortfolio()
// Get the difference of target portfolio and the current portfolio
val diff = account.positions.diff(targetPortfolio)
// Transform the difference into MarketOrders
return diff.map { MarketOrder(it.key, it.value) }
}
override fun reset() {
rebalanceDate = Instant.MIN
}
}
val roboquant = Roboquant(
NoSignalStrategy(), // will always return an empty list of signals
policy = MyPolicy()
)
The following example shows an approach to trading a single asset. This would be applicable if, for example, you only trade a single currency pair.
The advantage over using a strategy is that you have full control over all the logic in a single place.
class SingleAssetPolicy(private val asset: Asset) : Policy {
private val history = PriceSeries(10)
/**
* Open a position
*/
private fun openPosition(account: Account, priceAction: PriceAction): Order? {
val available = account.buyingPower.value * 0.9 // Let's not use 100%
val price = priceAction.getPrice()
val size = asset.contractSize(available, price, fractions = 0)
return if (history.add(price)) {
// Your logic to decide to open a position
TODO()
} else {
null
}
}
/**
* Close a position
*/
private fun closePosition(position: Position, priceAction: PriceAction): Order? {
val price = priceAction.getPrice()
val profit = price / position.avgPrice - 1.0
return when {
profit >= 0.01 -> MarketOrder(position.asset, -position.size) // take profit
profit <= 0.01 -> MarketOrder(position.asset, -position.size) // stop loss
else -> null
}
}
override fun act(signals: List<Signal>, account: Account, event: Event): List<Order> {
// If there is already an open order, don't do anything
if (account.openOrders.assets.contains(asset)) return emptyList()
// If there is no price, don't do anything
val priceAction = event.prices[asset] ?: return emptyList()
val position = account.positions.firstOrNull { it.asset == asset }
val order = if (position != null) {
closePosition(position, priceAction)
} else {
openPosition(account, priceAction)
}
return if (order == null) emptyList() else listOf(order)
}
override fun reset() {
history.clear()
}
}
val roboquant = Roboquant(
NoSignalStrategy(), // will always return an empty list of signals
policy = SingleAssetPolicy(feed.assets.first())
)
roboquant.run(feed)