roboquant avatar

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.

Currency

The roboquant Currency class represents any type of currency. It follows closely the interface tof 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. 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. 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 during a run. You can set this property and all code from now on will use this new implementation. The default is that no conversion at all will be performed, and an exception will be thrown if a conversion is required. As long as you trade in a single currency (like USD), this default will be fine.

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, TimeSpan.

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,
)

// Run a back test against these periods
timeFrames.forEach {
    roboquant.run(feed, it.extend(5.days))
}

TimeSpan

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

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

  2. Duration which you use for hours and shorter and is timezone independent

The TimeSpan class in roboquant unifies this into a single implementation, and make it easier to use them in combination with Timeframe, Instant and ZonedDateTime instances.

You can perform operations like:

// Use the TimePeriod constructor directly
val period = TimeSpan(years = 1, months = 6, seconds = 30)

// Use extension methods
val oneDay = 1.days
val oneHour = 1.hours
val myTimePeriod = 1.years + 6.months
val myHighFrequencyPeriod = 1.seconds + 500.millis

// Calculate using TimePeriods (using UTC if required)
val now = Instant.now()
val tomorrow = now + 1.days
val yesterday = now - 1.days
val nextHourAndHalf = now + 1.hours + 30.minutes
val nextYearAndHalf = now + myTimePeriod

// Using an explicit timezone
val zone = ZoneId.of("Europe/Amsterdam")
val localTomorrow = now.plus(1.months, zone)

// Using timezone aware ZonedDateTime
val localNow = ZonedDateTime.now() // timezone aware datetime
val localTomorrow2 = localNow + 1.days

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

// Extend a timeframe
val timeframe2 = Timeframe.blackMonday1987.extend(before = 2.months, after = 1.months)
Whenever a timezone is required for an operation and none is supplied, UTC will be used. This provides consistent behavior across machines in different time zones.

Exchanges and TradingCalendars

An asset is linked to a single exchange, and that exchange has a time-zone associated with it. This is mostly relevant during back-testing when certain time-in-force policies need to be evaluated (like good for today).

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

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

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

// LocalDate features
val localDate = exchange.getLocalDate(now)
val dayOfWeek = localDate.dayOfWeek
val dayOfMonth = localDate.dayOfMonth

// using the configured TradingCalendar
exchange.isTrading(now)
exchange.getOpeningTime(localDate)
exchange.getClosingTime(localDate)

Size

Order sizes and position sizes are expressed as an instance of the Size class. For the internal working of roboquant, there is no differentiation between fractional and non-fractional orders. All the order and position quantities are expressed using the same Size class.

The Size class is precise till 8 decimal digits and ensures that there are no rounding errors when, for example, when closing a position. It is an immutable class, and you can perform simple arithmetic on it.

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

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