roboquant avatar

Roboquant

Run

The run and runAsync functions are the engine of the platform. They orchestrate the interaction between the components and perform the actual run.

The only required parameters are a feed and a strategy. So, the bare minimum to create an instance would look something like this:

val strategy = EMACrossover()
run(feed, strategy)

The following default values will be used if you don’t provide additional parameters:

  • The SimBroker will be used as the broker with its default settings

  • The FlexTrader will be the trader used

  • No trading Journal will be used or metrics will be tracked

Each of these defaults can be overwritten with a different implementation when you instantiate a Roboquant, as the following code snippet demonstrates:

run(
    feed,
    EMACrossover(), // the strategy to use
    broker = AlpacaBroker(), // using the Alpaca broker
    journal = BasicJournal(),
    timeframe = Timeframe.blackMonday1987,
    timeOutMillis = 2000,
    showProgressBar = true
)

As you can see, you can provide multiple metrics, but only a single instance of the other components. But that doesn’t mean you cannot use multiple strategies or policies. You can, for example, combine multiple strategies into one and pass that instance to the Roboquant constructor:

val strategy1 = EMACrossover.PERIODS_12_26
val strategy2 = EMACrossover.PERIODS_50_200
val strategy = CombinedStrategy(strategy1, strategy2)

See also the strategy page for more details.

The same run method is used for all the different stages, from back testing to live trading. See also the 4 stages for more details about these stages.

In the most basic form, you provide the Feed as a single the argument to the run method. In that case, all the events available in the feed will be used in the run. So if your feed contains 100 stocks with each stock having 20 years of end-of-day candlesticks, all that data will be used in a single run.

run(feed, EMACrossover())

However, you can restrict a run to a certain timeframe. This is especially useful for live feeds that would otherwise run forever. But it also comes in handy when using historic data, and you want to perform multiple runs over different timeframes.

// Historical feed run example
val timeframe = Timeframe.fromYears(2015, 2020)
run(feed1, EMACrossover(), timeframe = timeframe)

// Live feed run example
val timeframe2 = Timeframe.next(120.minutes)
run(feed2, EMACrossover(), timeframe = timeframe2)

You can invoke a run multiple times, for example, with different timeframes. The following code shows how to use this to perform a walk-forward back test of two-year periods:

val timeframe = feed.timeframe
timeframe.split(2.years).forEach {
    val account = run(feed, EMACrossover(), timeframe = it)
    println(account.equity())
}

Or run even more back tests, like in this Monte Carlo simulation where we draw 100 random timeframes of 2 years each:

val timeframe = feed.timeframe
timeframe.sample(2.years, 100).forEach {
    val account = run(feed, EMACrossover(), timeframe = it)
    println(account.equity())
}

From a metric logging perspective, the metrics have a different RunPhase in the RunInfo object associated with them. So you can always find out in which RunPhase a metric was generated.

Running in Parallel

If you want to run many back-tests, you can run them in parallel and leverage the available cores on your computer to expedite the process. The roboquant engine scales almost linearly with the number of available cores, and it only requires a few extra lines of code.

Performance and scalability are determined by many other factors besides the engine itself. For example, the amount of memory used and allocated at each step, the CPU usage of your strategy, or the files read from disc.

The following example shows how to run a walk forward in parallel. By reusing the same logger instance, all results will be stored in a single logger instance and can be easily compared afterwards. Also, the feed can be shared across jobs, resulting in lower memory consumption.

val timeframe = feed.timeframe
val mrj = MultiRunJournal { MemoryJournal(AccountMetric()) }
val jobs = ParallelJobs()

for (period in timeframe.split(2.years)) {
    jobs.add {
        // Give the run a unique identifiable name
        runAsync(feed, EMACrossover(), mrj.getJournal(), timeframe = period)
    }
}

// Wait until all the jobs are finished
jobs.joinAllBlocking()

// If you are in Jupyter Notebook, you can easily plot a metric for all the runs
val equity = mrj.getMetric("account.equity")
TimeSeriesChart(equity)

Pinpointing issues

It isn’t always easy to find out why a run isn’t behaving as expected. One way to pinpoint where the actual issue is located is to enable extra metrics recording.

The Trader is a place where made decisions can sometime not be that obvious, and additional metrics can help:

val strategy = EMACrossover()
run(feed, EMACrossover())

This will tell you for each step, how many actions, signals and orders there were and enables you to draw some conclusions:

Metric Observation Possible problem

policy.actions

always 0

The feed isn’t generating actions. For example, when using a LiveFeed outside trading hours.

policy.signals

always 0

The strategy isn’t generating signals.

policy.orders

always 0

The policy isn’t generating orders. For example, you don’t have enough buying power to buy a single BitCoin and didn’t enable fractional orders.