Domain Model

Approach

Besides core components like Strategy and Policy, roboquant also comes with a number of domain specific classes that make it easier to implement robust trading strategies. The two main focus areas are:

  1. monetary related functionality

  2. time related functionality

This approach makes it very easy to deal with these entities without much code. This is also where many of the Kotlin language features come in handy like the support for operator overloading and extension functions.

Monetary

In order to make it easier to develop solutions in a multi-currency setup, roboquant introduces a number of money-related domain classes: Currency, Amount, Wallet and ExchangeRates that will help to do so.

Currency

The roboquant Currency class represents any type of currency. It follows closely the interface that comes with the standard Java Currency class, but is independent of it. The main reason is that the Java implementation doesn’t support cryptocurrencies while the roboquant implementation supports any type of currency.

Also, several common currencies are predefined, to make it easier to work with and avoid typing mistakes.

Currency.USD
Currency.BTC
Currency.getInstance("MyCryptoCoin")

Amount

An amount can hold the value for a single currency. You can create a new Amount using the constructor. But you can also use the extension functions that are defined for commonly used currencies (e.g. 120.EUR). Amounts, just like numbers, are immutable.

val amount1 = Amount(Currency.getInstance("USD"), 100.0) // the most code
val amount2 = Amount(Currency.USD, 100.0) // less code
val amount3 = 100.USD // the least code

val amount4 = 50.USD * 2 // simple calculation
require(amount4 == amount3)

// display using the common number of decimals for that currency
println(amount1.formatValue())
Amounts store their values internally as a Double and not a BigDecimal. For (simulated) trading the potential loss of precision is not an issue and the benefits are improved performance and lower memory consumption.

Wallet

A Wallet can hold amounts of multiple currencies at the same time. You can create a new Wallet by using its constructor, or by adding multiple amounts together. You can also deposit and withdraw amounts in a wallet. A Wallet instance is mutable.

There is an important difference between deposit method and using the + operator. The deposit method will modify the existing instance while the + operator will return a new wallet instance.

val wallet1 = Wallet(100.EUR, 10.USD)
wallet1.deposit(200.GBP)
wallet1.withdraw(1000.JPY)

val wallet2 = 20.EUR - 1000.JPY + 100.GBP
val wallet3 = 0.02.BTC + 100.USDT + 0.1.ETH
If you add two amounts together you’ll always get a new Wallet instance back, even if the two amounts have the same currency.

ExchangeRates

ExchangeRates is an interface that supports the conversion of an amount from one currency to another one. Although you can invoke the API of an implementation directly, typically you would use the convert method of an Amount or Wallet instance.

The Config.exchangeRates property is used by the components to perform the actual conversion. So you can change this property and all code from now on will use this new implementation.

The ExchangeRates interface also supports different rates at different moments in time, but it depends on the actual implementation if the provided time parameter is taken into consideration. One of the implementations that comes out of the box with roboquant is the ECBExchangeRates, which contains daily exchange rates for most fiat currencies from the year 2000 onwards.

// Load the exchange rates as published by the ECB (European Central Bank)
Config.exchangeRates = ECBExchangeRates.fromWeb()
val amountEUR = 20.EUR
val amountUSD = amountEUR.convert(Currency.USD)

val wallet = 20.USD + 1000.JPY

// Convert wallet based on today's exchange rates
val amountGBP1 = wallet.convert(Currency.GBP)

// Convert wallet based on two years ago exchange rates
val time = Instant.now() - 2.years
val amountGBP2 = wallet.convert(Currency.GBP, time)
require(amountGBP1 != amountGBP2)

Time

In order to make it easier to trade in multiple regions, roboquant introduces a number of time related domain classes: Timeline, Timeframe, TradingPeriod.

Internally all time in stored as an Instant type and so can always be easily compared, regardless of from which timezone the information originated. Event, for example, exposes its time attribute as an Instant.

Timeline

Timeline is a list of Instant instances ordered in chronological order. For example, a CSVFeed has a timeline, that represents all the moments in time for which the feed has data available. A timeline has zero or more elements, but is always finite.

val feed = CSVFeed("somepath")

// Split the timeline in chunks of 250
// and run back test over them
feed.timeline.split(250).forEach {
    roboquant.run(feed, it)
}

Timeframe

Timeframe represents a period of time with a start-time (always inclusive) and an end-time (inclusive or exclusive).

It is used throughout roboquant, but one particular use case is that you can restrict a run to a particular timeframe. So instead of one large run over all the events in a Feed, you can have multiple smaller runs. The Timeframe has several helper methods that make it easier to split a timeframe into smaller parts.

// Parse a string
val tf1 = Timeframe.parse("2019-01-01", "2020-01-01")

// Add to years to the timeframe
val tf2 = tf1 + 2.years

// Split timeframe in 31 days periods
val tf3 = tf2.split(31.days) // result is List<Timeframe>

// Create 100 random timeframes, each of 3 months duration
val tf4 = tf2.sample(3.months, 100) // result is List<Timeframe>

// Predefined timeframes
val tf5 = Timeframe.blackMonday1987

There is also a set of predefined timeframes for significant periods in the history of trading. This comes in handy if you want to validate the performance of your strategy in a certain type of regime.

val timeFrames = listOf(
    Timeframe.blackMonday1987,
    Timeframe.financialCrisis2008,
    Timeframe.coronaCrash2020,
    Timeframe.flashCrash2010,
    Timeframe.tenYearBullMarket2009,
)

timeFrames.forEach {
    roboquant.run(feed, it)
}

// Create a timeframe from 2 months before till 1 month after Black Monday
val tf = Timeframe.blackMonday1987.extend(before = 2.months, after = 1.months)

TradingPeriod

When dealing with time duration, there are actual two types of duration in Java/Kotlin:

  1. Period which you use for days and longer and is timezone dependent

  2. Duration which you use for days and shorter and is not timezone dependent

The TradingPeriod in roboquant unifies this into a single implementation, allowing you to do things like:

// Use the TradingPeriod constructor directly
val period = TradingPeriod(Duration.ofDays(10L))

// Use extension methods
val oneDay = 1.days
val oneHour = 1.hours

// Calculate using TradingPeriods
val now = Instant.now()
val tomorrow = now + oneDay
val yesterday = now - oneDay
val nextHour = now + oneHour

// Run a forward test for the next 8 hours
val timeframe = Timeframe.next(8.hours)
roboquant.run(feed, timeframe)

Whenever a timezone is required for a time operation, the one configured at Config.defaultZoneId will be used.

Assets & Exchanges

An asset is linked to an exchange and that exchange has a time-zone associated with it.

val start = Instant.parse("2023-01-04-T10:00:00Z")
val end = Instant.parse("2023-01-04-T16:00:00Z")

// Sydney
val asset1 = Asset("XYZ", exchange = Exchange.SSX)
asset1.exchange.sameDay(start, end) // False

// London
val asset2 = Asset("XYZ", exchange = Exchange.LSE)
asset2.exchange.sameDay(start, end) // True

// Access local date
val time = Instant.now()
val localDate = asset2.exchange.getLocalDate(time)
val dayOfWeek = localDate.dayOfWeek
val dayOfMonth = localDate.dayOfMonth

The exchange also has a TradingCalendar associated with it, that determines what are the valid trading hours for a particular exchange.

Size

Order sizes and position sizes are expressed as an instance of the Size class. This class is precise till 8 decimal digits and ensures that there are no rounding errors when for example closing a position.

It is an immutable class, but you can perform simple arithmetic on it. It also caters for fractional trading if desired.

val size = Size(100) + Size(200)
val size2 = Size("100.12")

val order = MarketOrder(bitcoin, Size("0.01"))
val positionSize = account.positions.first().size