roboquant avatar

Roboquant

Create an Instance

The Roboquant class (org.roboquant.Roboquant) is the engine of the platform, not to be confused with roboquant (all lower case) which is the name of the platform itself. An instance of the Roboquant class orchestrates the interaction between the components and performs the actual runs.

The only required parameter when creating a new instance of Roboquant is the strategy. So the bare minimum to create an instance would look something like this:

val strategy = EMAStrategy()
val roboquant = Roboquant(strategy)

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

  • No Metric will be captured

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

  • The FlexPolicy will be the policy used

  • The MemoryLogger for logging the metrics (although without any metrics to capture, there is not much to log)

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

val roboquant = Roboquant(
    EMAStrategy(),
    AccountMetric(), PNLMetric(),
    policy = FlexPolicy {
        shorting = true
    },
    broker = AlpacaBroker(),
    logger = InfoLogger()
)

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 = EMAStrategy.PERIODS_12_26
val strategy2 = EMAStrategy.PERIODS_50_200
val strategy = CombinedStrategy(strategy1, strategy2)
val roboquant = Roboquant(strategy)

See also the strategy page for more details.

Run

After you have created an instance of the Roboquant class and have a data feed, you can start the run.

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.

roboquant.run(feed)

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)
roboquant.run(feed1, timeframe)

// Live feed run example
val timeframe2 = Timeframe.next(120.minutes)
roboquant.run(feed2, 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 {
    roboquant.run(feed, it)
    println(roboquant.broker.account.equityAmount)
}

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 {
    roboquant.run(feed, it)
    println(roboquant.broker.account.equityAmount)
}

Validation run

There are strategies that are self-learning. For example, many machine-learning-based strategies fall under this category. These strategies are trained on one timeframe and then validated on another (typically sequential) timeframe.

Also, this type of strategy can be easily back-tested with roboquant. The run method accepts an additional timeframe parameter to trigger a second validation phase.

// Single run example
val (main, validation) = feed.timeframe.splitTrainTest(0.2) // 20% for validation phase
roboquant.run(feed, main, "run-main")
println(roboquant.broker.account.equityAmount)
roboquant.broker.reset()
roboquant.run(feed, validation, "run-validation")

// Walk forward example
feed.timeframe.split(2.years).forEach {
    val (main2, validation2) = it.splitTrainTest(0.25) // 25% for validation phase
    roboquant.run(feed, main2)
    roboquant.broker.reset()
    roboquant.run(feed, validation, "run-validation")
    println(roboquant.broker.account.equityAmount)
}

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 logger = MemoryLogger(showProgress = false)
val jobs = ParallelJobs()

for (period in timeframe.split(2.years)) {
    jobs.add {
        // Create a new roboquant instance for each job
        val roboquant = Roboquant(EMAStrategy(), AccountMetric(), logger = logger)

        // Give the run a unique identifiable name
        roboquant.runAsync(feed, period, name = "run-$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 = logger.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:

val strategy = EMAStrategy()
val policy = FlexPolicy()
policy.enableMetrics = true
val roboquant = Roboquant(strategy, policy = policy, logger = ConsoleLogger())
roboquant.run(feed)

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.