Skip to content

Commit

Permalink
First stab at harmonizing Strategy + Trader 2
Browse files Browse the repository at this point in the history
  • Loading branch information
jbaron committed Jun 30, 2024
1 parent 5a65c23 commit 4111dcd
Show file tree
Hide file tree
Showing 31 changed files with 212 additions and 335 deletions.
21 changes: 11 additions & 10 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,29 @@
= Changelog

== Major changes in 3.0
Roboquant version 3.0 has many changes, several of them are breaking changes. The key reason was to clean-up and simplify areas that were a hard to maintain and/or extend. More often than not things where generic enough to add a lot of complexity while still not covering many the use cases.
Roboquant version 3.0 has many changes, several of them are breaking changes. The key reason was to clean-up and simplify areas that were a hard to maintain and/or extend. This makes it easier for people to contiue to try new trading ideas and hack their way _roboquant_ if required.

A non-extensive list of the major changes:

- Signals are simplified and now use a double value for the rating rather than an enum. This is better suited for more advanced ML type of algos.
- `Policy` is removed. For similar functionality, look at a `SignalStrategy` and `SignalConverter`.

- Signals themselves are also simplified and now use a double value for the rating rather than an enum. This is better suited for more advanced ML type of algos.

- Removed the `Lifecycle` interface including `reset` for all components (Stratyegy, Policy, Broker).

- Order handling is simplified. For example, order-ids are handout by the broker (during the place order method). So they are no longer assigned when creating the order. This is more inline how most brokers work.

- Moved `Roboquant.run` to standalone function `run`. For now `Roboquant` is still there for backwards compatibility (but deprecated)
- Removed `Roboquant` class. You can now use directly the standalone functions `run` and `runAsync`.

- SimBroker order execution is simplified. Every order type has its own executor that is responsible for every aspect of the execution. A bit less configurable, but much easier to roll your own implementation.

- Improved the `Broker` interface. Only the `broker.sync()` call now provides access to the `account` object.

- `MetricsLogger` is replaced by `Journal` and `MetricsJournal`. Journal has access to more data (also signals and new orders).
- `MetricsLogger` is replaced by `Journal` and `MetricsJournal`. Journal has access to more data (also new orders).
+
[source,kotlin]
----
fun track(event: Event, account: Account, signals: List<Signal>, orders: List<Order>) {
fun track(event: Event, account: Account, orders: List<Order>) {
TODO()
}
----
Expand All @@ -37,9 +39,8 @@ Also more clear separation of concerns since multi-run logging logic is now hand

- For the time being removed the machine learning module. Rethinking what frameworks to use for future ML algo trading.

- Removed `roboquant-binance` module. It was using an old unsupported API. And since they no longer operate in my country, difficult to test when moving to the newer API.
- Removed the `roboquant-binance` module. It was using an old unsupported Binance API. And since Binance no longer operate in my country, it is difficult to moving to the newer API without be able to test it.

- You can now start a run using the `org.roboquant.run` function. So no need to instantiate a Roboquant class anymore. The `run` also returns the latest `account` object. The run method also displays a progress bar if desired.
+
[source,kotlin]
----
Expand All @@ -56,8 +57,8 @@ val account = run(feed, strategy)

- Use the new Alpaca library that is based on published OpenAPI specification.

- Simplified AvroFeed (have to see if this is not too simple). We dropped the `feed.assets` method to make this possible
- Simplified AvroFeed (have to see if this is not too simple). We had to drop the `feed.assets` method to make this possible.

- Updated almost all dependencies. Only not yet moved to Kotlin 2.0 since Jupyter Notebooks are not yet supported.
- Updated most of the dependencies. We only not yet moved to Kotlin 2.0 since Jupyter Notebooks are not yet supported.

- Removed most of `Summary` functionality. This required relative a lot of maintenance and now replaced with a much simpler toString() implementation.
- Removed the `Summary` functionality. This required relative a lot of code and maintenance and now replaced by a simpler toString() implementation.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import org.roboquant.common.*
import org.roboquant.feeds.csv.CSVConfig
import org.roboquant.feeds.csv.CSVFeed
import org.roboquant.feeds.csv.TimeParser
import org.roboquant.strategies.FlexTrader
import org.roboquant.strategies.FlexConverter
import org.roboquant.run
import org.roboquant.strategies.EMACrossover
import java.time.Instant
Expand Down Expand Up @@ -94,12 +94,12 @@ internal class AvroSamples {

// Since we only trade in one asset and have leverage, we allow up to 1000% of our equity (10k) allocated to
// one order
val policy = FlexTrader.capitalBased {
val policy = FlexConverter.capitalBased {
shorting = true
orderPercentage = 90.percent
safetyMargin = 10.percent
}
strategy.signal2Order = policy
strategy.signalConverter = policy

val account = run(feed, strategy, broker = broker)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import com.ib.client.OrderStatus as IBOrderStatus
/**
* Use your Interactive Brokers account for trading. Can be used with live trading or paper trading accounts of
* Interactive Brokers. It is highly recommended to start with a paper trading account and validate your strategy and
* signal2Order extensively before moving to live trading.
* signalConverter extensively before moving to live trading.
*
* ## Use at your own risk, since there are no guarantees about the correct functioning of the roboquant software.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ internal class JupyterCore(
"org.roboquant.feeds.random.*",
"org.roboquant.brokers.sim.*",
"org.roboquant.brokers.*",
"org.roboquant.traders.*",
"org.roboquant.jupyter.*",
"org.roboquant.charts.*",
"java.time.Instant",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import org.roboquant.brokers.sim.SimBroker
import org.roboquant.common.*
import org.roboquant.feeds.*
import org.roboquant.feeds.random.RandomWalk
import org.roboquant.strategies.FlexTrader
import org.roboquant.strategies.FlexConverter
import org.roboquant.strategies.CombinedStrategy
import org.roboquant.strategies.EMACrossover
import org.roboquant.strategies.Signal
Expand Down Expand Up @@ -156,10 +156,10 @@ private object Performance {
)

val broker = SimBroker(accountModel = MarginAccount())
val policy = FlexTrader {
val policy = FlexConverter {
shorting = true
}
strategy.signal2Order = policy
strategy.signalConverter = policy

run(feed, strategy, broker = broker)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import org.roboquant.strategies.Strategy
import java.time.Instant

/**
* Signal2Order that can be paused and also captures a number of metrics
* SignalConverter that can be paused and also captures a number of metrics
*/
internal class PausableStrategy(private val trader: Strategy, var pause: Boolean = false) : Strategy {

Expand All @@ -35,9 +35,9 @@ internal class PausableStrategy(private val trader: Strategy, var pause: Boolean

internal var lastUpdate: Instant = Instant.MIN

override fun create(account: Account, event: Event): List<Instruction> {
override fun create(event: Event, account: Account): List<Instruction> {
// Still invoke the strategy so any state can be updated if required.
val orders = trader.create(account, event)
val orders = trader.create(event, account)

totalEvents++
if (event.items.isEmpty()) emptyEvents++
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ import org.roboquant.common.Size
import org.roboquant.feeds.Event
import org.roboquant.feeds.PriceItem
import org.roboquant.orders.*
import org.roboquant.strategies.FlexTrader
import org.roboquant.strategies.FlexConverter
import org.roboquant.strategies.FlexPolicyConfig
import org.roboquant.strategies.Signal


/**
* This signal2Order is a subclass of [FlexTrader] and uses ATR (Average True Range) to:
* This signalConverter is a subclass of [FlexConverter] and uses ATR (Average True Range) to:
*
* - Create a [BracketOrder] with ATR based take profit and stop loss values
* - Optionally reduce the sizing based on the ATR.
Expand All @@ -49,13 +49,13 @@ import org.roboquant.strategies.Signal
* @property atRisk max percentage of the order value that can be at risk.
* If null, no risk-based sizing will be applied.
*/
open class AtrSignal2Order(
open class AtrSignalConverter(
private val atrPeriod: Int = 20,
private val atrProfit: Double = 4.0,
private val atrLoss: Double = 2.0,
private val atRisk: Double? = null,
configure: FlexPolicyConfig.() -> Unit = {}
) : FlexTrader(configure = configure) {
) : FlexConverter(configure = configure) {


init {
Expand Down Expand Up @@ -83,11 +83,11 @@ open class AtrSignal2Order(
}

/**
* @see FlexTrader.transform
* @see FlexConverter.convert
*/
override fun transform(signals: List<Signal>, account: Account, event: Event): List<Instruction> {
override fun convert(signals: List<Signal>, account: Account, event: Event): List<Instruction> {
data.addAll(event)
return super.transform(signals, account, event)
return super.convert(signals, account, event)
}

/**
Expand Down Expand Up @@ -125,7 +125,7 @@ open class AtrSignal2Order(
}

/**
* @see FlexTrader.calcSize
* @see FlexConverter.calcSize
*
* This implementation adds functionality that if the value at risk is larger than the defined [atRisk] percentage
* of the total order amount, the size will be reduced accordingly.
Expand All @@ -139,7 +139,7 @@ open class AtrSignal2Order(
}

/**
* @see FlexTrader.createOrder
* @see FlexConverter.createOrder
*/
override fun createOrder(signal: Signal, size: Size, priceItem: PriceItem): Instruction? {
val asset = signal.asset
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import kotlin.math.min
* It will then hold these positions for a number of days before re-evaluating the strategy. After re-evaluation, the
* strategy will then generate the market orders required to achieve the desired new portfolio composition.
*
* Since this strategy controls the complete portfolio and not just generates signals, it is implemented as a signal2Order
* Since this strategy controls the complete portfolio and not just generates signals, it is implemented as a signalConverter
* and not a strategy. It doesn't use leverage or buying power, when re-balancing it just re-balances the total equity
* of the account across the long and short positions.
*
Expand Down Expand Up @@ -150,7 +150,7 @@ open class BettingAgainstBetaTrader(
* @param event the market data
* @return
*/
override fun create(account: Account, event: Event): List<Instruction> {
override fun create(event: Event, account: Account): List<Instruction> {

// First, we update the buffers
data.addAll(event, windowSize, priceType)
Expand Down
20 changes: 10 additions & 10 deletions roboquant-ta/src/test/kotlin/org/roboquant/samples/TaSamples.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import org.roboquant.feeds.PriceItem
import org.roboquant.feeds.random.RandomWalk
import org.roboquant.orders.LimitOrder
import org.roboquant.orders.Instruction
import org.roboquant.strategies.FlexTrader
import org.roboquant.strategies.FlexConverter
import org.roboquant.strategies.EMACrossover
import org.roboquant.strategies.Signal
import org.roboquant.ta.*
Expand Down Expand Up @@ -87,23 +87,23 @@ internal class TaSamples {
internal fun customPolicy() {

/**
* Custom Signal2Order that extends the FlexTrader and captures the ATR (Average True Range) using the TaLibMetric. It
* Custom SignalConverter that extends the FlexConverter and captures the ATR (Average True Range) using the TaLibMetric. It
* then uses the ATR to set the limit amount of a LimitOrder.
*/
class SmartLimitSignal2Order(private val atrPercentage: Double = 200.percent, private val atrPeriod: Int) :
FlexTrader() {
class SmartLimitSignalConverter(private val atrPercentage: Double = 200.percent, private val atrPeriod: Int) :
FlexConverter() {

// use TaLibMetric to calculate the ATR values
private val atr = TaLibMetric { mapOf("atr" to atr(it, atrPeriod)) }
private var atrMetrics = emptyMap<String, Double>()

override fun transform(signals: List<Signal>, account: Account, event: Event): List<Instruction> {
override fun convert(signals: List<Signal>, account: Account, event: Event): List<Instruction> {
// Update the metrics and store the results, so we have them available when the
// createOrder is invoked.
atrMetrics = atr.calculate(account, event)

// Call the regular FlexTrader processing
return super.transform(signals, account, event)
// Call the regular FlexConverter processing
return super.convert(signals, account, event)
}

/**
Expand All @@ -127,7 +127,7 @@ internal class TaSamples {

val feed = RandomWalk.lastYears(5)
val strategy = EMACrossover.PERIODS_12_26
strategy.signal2Order = SmartLimitSignal2Order(atrPeriod = 5)
strategy.signalConverter = SmartLimitSignalConverter(atrPeriod = 5)
val account = run(feed, EMACrossover.PERIODS_12_26)
println(account)
}
Expand All @@ -136,11 +136,11 @@ internal class TaSamples {
@Ignore
internal fun atrPolicy() {
val strategy = EMACrossover.PERIODS_5_15
val policy = AtrSignal2Order(10, 6.0, 3.0, null) {
val policy = AtrSignalConverter(10, 6.0, 3.0, null) {
orderPercentage = 0.02
shorting = true
}
strategy.signal2Order = policy
strategy.signalConverter = policy
val broker = SimBroker(accountModel = MarginAccount(minimumEquity = 50_000.0))

val feed = RandomWalk.lastYears(5)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ import org.roboquant.orders.BracketOrder
import org.roboquant.orders.LimitOrder
import org.roboquant.orders.Instruction
import org.roboquant.orders.StopOrder
import org.roboquant.strategies.FlexTrader
import org.roboquant.strategies.FlexConverter
import org.roboquant.strategies.Signal
import java.time.Instant
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

internal class AtrSignal2OrderTest {
internal class AtrSignalConverterTest {

private fun usAccount(amount: Amount = 100_000.USD): Account {
val account = InternalAccount(amount.currency)
Expand All @@ -43,7 +43,7 @@ internal class AtrSignal2OrderTest {
return account.toAccount()
}

private fun run(policy: FlexTrader): List<Instruction> {
private fun run(policy: FlexConverter): List<Instruction> {
val asset = Asset("TEST")
val signals = listOf(Signal.buy(asset))
val now = Instant.now()
Expand All @@ -55,15 +55,15 @@ internal class AtrSignal2OrderTest {
val p = 5.0
val priceBar = PriceBar(asset, p + it, p + it, p + it, p + it)
val event = Event(now + it.millis, listOf(priceBar))
val o = policy.transform(signals, account, event)
val o = policy.convert(signals, account, event)
instructions.addAll(o)
}
return instructions
}

@Test
fun bracketATR() {
val p = AtrSignal2Order(10, 4.0, 2.0, null) {
val p = AtrSignalConverter(10, 4.0, 2.0, null) {
orderPercentage = 0.02
}
val orders = run(p)
Expand All @@ -84,7 +84,7 @@ internal class AtrSignal2OrderTest {

@Test
fun bracketSizingAtr() {
val p = AtrSignal2Order(10, 4.0, 2.0, 0.1) {
val p = AtrSignalConverter(10, 4.0, 2.0, 0.1) {
orderPercentage = 0.02
}
val orders = run(p)
Expand All @@ -98,7 +98,7 @@ internal class AtrSignal2OrderTest {
@Test
fun bracketSizingAtrValidation() {
assertThrows<IllegalArgumentException> {
AtrSignal2Order(10, 4.0, 2.0, 1.3) {
AtrSignalConverter(10, 4.0, 2.0, 1.3) {
orderPercentage = 0.02
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import org.roboquant.run
import kotlin.test.Test
import kotlin.test.assertTrue

internal class BettingAgainstBetaSignal2OrderTest {
internal class BettingAgainstBetaSignalConverterTest {

@Test
fun test() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import java.time.Instant

/**
* Internal Account is meant to be used by broker implementations, like the SimBroker. The broker is the only one with
* a reference to the InternalAccount and will communicate the state to the outside world (Signal2Order and Metrics) using
* a reference to the InternalAccount and will communicate the state to the outside world (SignalConverter and Metrics) using
* the [Account] object.
*
* @property baseCurrency The base currency to use for things like reporting
Expand Down Expand Up @@ -136,7 +136,7 @@ class InternalAccount(override var baseCurrency: Currency) : Account {
}

/**
* Create an immutable [Account] instance that can be shared with other components (Signal2Order and Metric) and is
* Create an immutable [Account] instance that can be shared with other components (SignalConverter and Metric) and is
* guaranteed not to change after it has been created.
*/
@Synchronized
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ abstract class SingleOrderExecutor<T : SingleOrder>(final override var order: T)
}

/**
* Validate TiF signal2Order and return true if the order has expired according to the defined TIF.
* Validate TiF signalConverter and return true if the order has expired according to the defined TIF.
*/
open fun expired(time: Instant): Boolean {
return when (val tif = order.tif) {
Expand All @@ -74,7 +74,7 @@ abstract class SingleOrderExecutor<T : SingleOrder>(final override var order: T)
is FOK -> remaining.nonzero
is GTD -> time > tif.date
is IOC -> time > openedAt
else -> throw UnsupportedException("unsupported time-in-force signal2Order tif=$tif")
else -> throw UnsupportedException("unsupported time-in-force signalConverter tif=$tif")
}
}

Expand Down
Loading

0 comments on commit 4111dcd

Please sign in to comment.