Skip to content

Commit

Permalink
Further enhancements back test
Browse files Browse the repository at this point in the history
  • Loading branch information
jbaron committed Jul 23, 2023
1 parent 278baae commit cd19e78
Show file tree
Hide file tree
Showing 12 changed files with 153 additions and 166 deletions.
25 changes: 13 additions & 12 deletions roboquant/src/main/kotlin/org/roboquant/Roboquant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ import org.roboquant.brokers.Broker
import org.roboquant.brokers.closeSizes
import org.roboquant.brokers.sim.SimBroker
import org.roboquant.common.Logging
import org.roboquant.common.TimeSpan
import org.roboquant.common.Timeframe
import org.roboquant.common.plus
import org.roboquant.feeds.Event
import org.roboquant.feeds.EventChannel
import org.roboquant.feeds.Feed
import org.roboquant.feeds.TradePrice
import org.roboquant.loggers.MemoryLogger
import org.roboquant.loggers.MetricsLogger
import org.roboquant.loggers.SilentLogger
import org.roboquant.metrics.Metric
import org.roboquant.orders.MarketOrder
import org.roboquant.orders.createCancelOrders
Expand Down Expand Up @@ -110,26 +111,23 @@ data class Roboquant(
}
}

/**
* Returns a copy of this instance but with the [SilentLogger] enabled.
*
* This is typically used during warmup and training phases when metrics output are not of interest.
*/
fun silent() = copy(logger = SilentLogger())

/**
* Start a new run using the provided [feed] as data. If no [timeframe] is provided all the events in the feed
* will be processed. You can provide a custom [name] that will help to later identify this run. If none is
* provided, a name will be generated with the format "run-<counter>"
* Additionally you can provide a [warmup] period in which no metrics will be logged or orders placed.
*
* This is the synchronous (blocking) method of run that is convenient to use. However, if you want to execute runs
* in parallel have a look at [runAsync]
*/
fun run(
feed: Feed,
timeframe: Timeframe = feed.timeframe,
name: String = "run",
warmup: TimeSpan = TimeSpan.ZERO,
name: String = "run"
) = runBlocking {
runAsync(feed, timeframe, name)
runAsync(feed, timeframe, warmup, name)
}

/**
Expand All @@ -142,7 +140,8 @@ data class Roboquant(
suspend fun runAsync(
feed: Feed,
timeframe: Timeframe = feed.timeframe,
name: String = "run",
warmup: TimeSpan = TimeSpan.ZERO,
name: String = "run"
) {
val channel = EventChannel(channelCapacity, timeframe)
val scope = CoroutineScope(Dispatchers.Default + Job())
Expand All @@ -152,6 +151,8 @@ data class Roboquant(
}

start(name, timeframe)
val warmupEnd = timeframe.start + warmup

try {
while (true) {
val event = channel.receive()
Expand All @@ -161,12 +162,12 @@ data class Roboquant(
broker.sync(event)
val account = broker.account
val metricResult = getMetrics(account, event)
logger.log(metricResult, time, name)
if (time >= warmupEnd) logger.log(metricResult, time, name)

// Generate signals and place orders
val signals = strategy.generate(event)
val orders = policy.act(signals, account, event)
broker.place(orders, time)
if (time >= warmupEnd) broker.place(orders, time)

kotlinLogger.trace {
"time=$${event.time} actions=${event.actions.size} signals=${signals.size} orders=${orders.size}"
Expand Down
40 changes: 12 additions & 28 deletions roboquant/src/main/kotlin/org/roboquant/backtest/Backtest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,44 +20,30 @@ import org.roboquant.Roboquant
import org.roboquant.brokers.sim.SimBroker
import org.roboquant.common.TimeSpan
import org.roboquant.common.Timeframe
import org.roboquant.common.plus
import org.roboquant.feeds.Feed


/**
* Run back test without parameter optimizations. This is useful to get insights into the performance over different
* timeframes.
* Run back tests without parameter optimizations.
* The back tests in this class will be run sequentially.
*
* These types of back tests are especially useful to get insights into the performance over different timeframes.
*
* @property feed The feed to use during the back tests
* @property roboquant The roboquant instance to use during the back tests
*/
class Backtest(val feed: Feed, val roboquant: Roboquant) {

init {
require(roboquant.broker is SimBroker) { "Only a SimBroker can be used for back testing"}
}


private val broker
get() = roboquant.broker as SimBroker


/**
* Run a warmup and return the remaining timeframe
*/
private fun warmup(timeframe: Timeframe, period: TimeSpan) : Timeframe {
require(timeframe.isFinite())
val end = timeframe.start + period
val tf = Timeframe(timeframe.start, end)
roboquant.silent().run(feed, tf) // Run without logging
broker.reset() // Reset the broker after a warmup
return timeframe.copy(start = end)
}

/**
* Perform a single run over the provided [timeframe] using the provided [warmup] period. The timeframe is
* including the warmup period.
*/
fun singleRun(timeframe: Timeframe = feed.timeframe, warmup: TimeSpan = TimeSpan.ZERO) {
val tf = if (! warmup.isZero) warmup(timeframe, warmup) else timeframe
roboquant.run(feed, tf, name = "run-$tf")
roboquant.run(feed, timeframe, warmup, name = "run-$timeframe")
}

/**
Expand All @@ -70,8 +56,7 @@ class Backtest(val feed: Feed, val roboquant: Roboquant) {
) {
require(feed.timeframe.isFinite()) { "feed needs a finite timeframe" }
feed.timeframe.split(period, warmup).forEach {
val tf = if (! warmup.isZero) warmup(it, warmup) else it
roboquant.run(feed, tf, name = "run-$tf")
roboquant.run(feed, it, warmup, name = "run-$it")
roboquant.reset(false)
}
}
Expand All @@ -80,17 +65,16 @@ class Backtest(val feed: Feed, val roboquant: Roboquant) {
* Run a Monte Carlo simulation of a number of [samples] to find out how metrics of interest are distributed
* given the provided [period].
*
* Optional each period can be preceded by a [warmup] time-span.
* Optional each period can include a [warmup] time-span.
*/
fun monteCarlo(
period: TimeSpan,
samples: Int,
warmup: TimeSpan = TimeSpan.ZERO,
) {
require(feed.timeframe.isFinite()) { "feed needs a finite timeframe" }
feed.timeframe.sample(period + warmup, samples).forEach {
val tf = if (! warmup.isZero) warmup(it, warmup) else it
roboquant.run(feed, tf, name = "run-$tf")
feed.timeframe.sample(period, samples).forEach {
roboquant.run(feed, it, warmup, name = "run-$it")
roboquant.reset(false)
}
}
Expand Down
Loading

0 comments on commit cd19e78

Please sign in to comment.