diff --git a/pom.xml b/pom.xml index 1ced3501..84472601 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,7 @@ ~ limitations under the License. --> + @@ -69,7 +70,7 @@ official 17 false - 1.9.24 + 1.9.25 1.9 1.9 2.0.13 @@ -467,6 +468,7 @@ ${slf4j.version} test + @@ -484,6 +486,13 @@ jackson-databind 2.15.2 + + roboquant + org.roboquant + ${project.version} + test-jar + test + diff --git a/roboquant-charts/pom.xml b/roboquant-charts/pom.xml index 44c518f9..e8cca53a 100644 --- a/roboquant-charts/pom.xml +++ b/roboquant-charts/pom.xml @@ -60,6 +60,12 @@ + + roboquant + org.roboquant + test-jar + test + diff --git a/roboquant-ibkr/src/main/kotlin/org/roboquant/ibkr/IBKRBroker.kt b/roboquant-ibkr/src/main/kotlin/org/roboquant/ibkr/IBKRBroker.kt index af796232..4b3a15f0 100644 --- a/roboquant-ibkr/src/main/kotlin/org/roboquant/ibkr/IBKRBroker.kt +++ b/roboquant-ibkr/src/main/kotlin/org/roboquant/ibkr/IBKRBroker.kt @@ -230,11 +230,6 @@ class IBKRBroker( */ private inner class Wrapper(logger: Logging.Logger) : BaseWrapper(logger) { - val accountTags = mutableMapOf( - AccountSummaryTag.TotalCashValue to Amount(Currency.USD, 0), - AccountSummaryTag.BuyingPower to Amount(Currency.USD, 0), - ) - /** * Convert an IBOrder to a roboquant Instruction. * This is only used during initial connection when retrieving any open orders linked to the account. diff --git a/roboquant-questdb/pom.xml b/roboquant-questdb/pom.xml index d438ca8b..12e68651 100644 --- a/roboquant-questdb/pom.xml +++ b/roboquant-questdb/pom.xml @@ -45,6 +45,12 @@ roboquant org.roboquant + + roboquant + org.roboquant + test-jar + test + org.questdb @@ -52,14 +58,6 @@ 8.1.0 - - org.roboquant - roboquant - 3.0.0-SNAPSHOT - test-jar - test - - diff --git a/roboquant-ta/pom.xml b/roboquant-ta/pom.xml index e049c7c8..cfb49675 100644 --- a/roboquant-ta/pom.xml +++ b/roboquant-ta/pom.xml @@ -54,6 +54,12 @@ ta-lib 0.4.0 + + roboquant + org.roboquant + test-jar + test + diff --git a/roboquant/src/main/kotlin/org/roboquant/common/Asset.kt b/roboquant/src/main/kotlin/org/roboquant/common/Asset.kt index 3bc0bc18..6bb28262 100644 --- a/roboquant/src/main/kotlin/org/roboquant/common/Asset.kt +++ b/roboquant/src/main/kotlin/org/roboquant/common/Asset.kt @@ -148,28 +148,6 @@ data class Forex(override val symbol: String, override val currency: Currency) : */ fun Collection.getBySymbol(symbol: String): Asset = first { it.symbol == symbol } -/** - * Find an asset based on its [symbols] name. Will return an empty list if no assets are matched. - */ -fun Collection.findBySymbols(vararg symbols: String): List = findBySymbols(symbols.asList()) - -/** - * Find an asset based on its [symbols] name. Will return an empty list if no assets are matched. - */ -fun Collection.findBySymbols(symbols: Collection): List = filter { it.symbol in symbols } - -/** - * Find all assets based on their [currencyCodes]. Returns an empty list if no matching assets can be found. - */ -fun Collection.findByCurrencies(vararg currencyCodes: String): List = - findByCurrencies(currencyCodes.asList()) - -/** - * Find all assets based on their [currencyCodes]. Returns an empty list if no matching assets can be found. - */ -fun Collection.findByCurrencies(currencyCodes: Collection): List = - filter { it.currency.currencyCode in currencyCodes } - /** * Get all unique symbols from the assets */ diff --git a/roboquant/src/main/kotlin/org/roboquant/common/TimeSeries.kt b/roboquant/src/main/kotlin/org/roboquant/common/TimeSeries.kt index d4549e60..3c99f6ce 100644 --- a/roboquant/src/main/kotlin/org/roboquant/common/TimeSeries.kt +++ b/roboquant/src/main/kotlin/org/roboquant/common/TimeSeries.kt @@ -235,28 +235,6 @@ class TimeSeries(val timeline: Timeline, val values: DoubleArray) : Iterable.flatten(noOverlap: Boolean = true): TimeSeries { - // Optimized path for a map with only one entry - if (size == 1) return values.first() - val sortedTimeSeries = values.sortedBy { it.timeline.first() } - val result = mutableListOf() - var last = Instant.MIN - for (timeSeries in sortedTimeSeries) { - for (entry in timeSeries) { - if (noOverlap && entry.time <= last) continue - result.add(entry) - last = entry.time - } - - } - return TimeSeries(result) -} - /** * Normalize all the timeseries in his map */ diff --git a/roboquant/src/main/kotlin/org/roboquant/common/extensions.kt b/roboquant/src/main/kotlin/org/roboquant/common/extensions.kt index a63c6206..dff38773 100644 --- a/roboquant/src/main/kotlin/org/roboquant/common/extensions.kt +++ b/roboquant/src/main/kotlin/org/roboquant/common/extensions.kt @@ -17,7 +17,6 @@ package org.roboquant.common -import kotlinx.coroutines.delay import org.hipparchus.stat.descriptive.moment.StandardDeviation import org.roboquant.common.Config.EPS import java.lang.Integer.max @@ -28,8 +27,6 @@ import java.time.Instant import java.time.LocalDate import java.time.ZoneId import java.time.ZoneOffset -import java.time.ZonedDateTime -import java.time.temporal.ChronoUnit import kotlin.math.absoluteValue import kotlin.math.ln @@ -50,20 +47,6 @@ operator fun Instant.compareTo(timeframe: Timeframe): Int { } } -/** - * Delay until this time is reached - */ -suspend fun Instant.delayUntil() { - val now = Instant.now() - delay(now.until(this, ChronoUnit.MILLIS).coerceAtLeast(0L)) -} - -/** - * Get the instant as ZonedDateTime UTC - */ -fun Instant.toUTC(): ZonedDateTime = atZone(ZoneOffset.UTC) - - fun Instant.sameDay(other: Instant, zoneId: ZoneId = ZoneOffset.UTC): Boolean { val dt1 = LocalDate.ofInstant(this, zoneId) val dt2 = LocalDate.ofInstant(other, zoneId) diff --git a/roboquant/src/main/kotlin/org/roboquant/feeds/AggregatorLiveFeed.kt b/roboquant/src/main/kotlin/org/roboquant/feeds/AggregatorLiveFeed.kt index 037cf24f..2a5738dc 100644 --- a/roboquant/src/main/kotlin/org/roboquant/feeds/AggregatorLiveFeed.kt +++ b/roboquant/src/main/kotlin/org/roboquant/feeds/AggregatorLiveFeed.kt @@ -45,6 +45,7 @@ import kotlin.reflect.KClass * @property remaining should any remaining actions be sent, default is true * */ +@Suppress("unused") class AggregatorLiveFeed( private val feed: LiveFeed, private val aggregationPeriod: TimeSpan, diff --git a/roboquant/src/main/kotlin/org/roboquant/feeds/csv/CSVConfig.kt b/roboquant/src/main/kotlin/org/roboquant/feeds/csv/CSVConfig.kt index 812ee162..61605609 100644 --- a/roboquant/src/main/kotlin/org/roboquant/feeds/csv/CSVConfig.kt +++ b/roboquant/src/main/kotlin/org/roboquant/feeds/csv/CSVConfig.kt @@ -54,7 +54,6 @@ data class CSVConfig( var assetBuilder: AssetBuilder = StockBuilder() ) { - private val pattern by lazy { Pattern.compile(filePattern) } private var isInitialized = false @@ -75,11 +74,15 @@ data class CSVConfig( return CSVConfig( filePattern = ".*.txt", timeParser = AutoDetectTimeParser(2), - assetBuilder = { name: String -> Stock(name.removeSuffix(".us.txt").replace('-', '.').uppercase(), Currency.USD) } + assetBuilder = { name: String -> + Stock( + name.removeSuffix(".us.txt").replace('-', '.').uppercase(), + Currency.USD + ) + } ) } - /** * Returns a CSVConfig suited for parsing MT5 CSV files * @@ -210,8 +213,6 @@ data class CSVConfig( return file.isFile && pattern.matcher(name).matches() && name !in fileSkip } - - /** * Merge a config map into this CSV config * diff --git a/roboquant/src/main/kotlin/org/roboquant/traders/FlexTrader.kt b/roboquant/src/main/kotlin/org/roboquant/traders/FlexTrader.kt index 70e4618d..d0d2da36 100644 --- a/roboquant/src/main/kotlin/org/roboquant/traders/FlexTrader.kt +++ b/roboquant/src/main/kotlin/org/roboquant/traders/FlexTrader.kt @@ -18,7 +18,6 @@ package org.roboquant.traders import org.roboquant.brokers.Account import org.roboquant.brokers.Position -import org.roboquant.brokers.exposure import org.roboquant.common.* import org.roboquant.feeds.Event import org.roboquant.feeds.PriceItem @@ -113,22 +112,6 @@ open class FlexTrader( return SingleTrader() } - /** - * Capital-based flex trader. - */ - fun capitalBased( - configure: FlexPolicyConfig.() -> Unit = {} - ): FlexTrader { - class CapiltalBasedTrader : FlexTrader(configure) { - override fun amountPerOrder(account: Account): Amount { - val capital = account.positions.values.exposure + account.buyingPower - val amount = account.convert(capital) - return amount * config.orderPercentage - } - } - return CapiltalBasedTrader() - } - /** * Return a FlexTrader that generates bracket orders, with the following characteristics: * - a market order for entry diff --git a/roboquant/src/main/kotlin/org/roboquant/traders/SignalResolution.kt b/roboquant/src/main/kotlin/org/roboquant/traders/SignalResolution.kt index ada082ff..ebb6daab 100644 --- a/roboquant/src/main/kotlin/org/roboquant/traders/SignalResolution.kt +++ b/roboquant/src/main/kotlin/org/roboquant/traders/SignalResolution.kt @@ -43,25 +43,3 @@ private class SignalShuffleTrader(private val trader: Trader, private val random */ fun Trader.shuffleSignals(random: Random = Config.random): Trader = SignalShuffleTrader(this, random) -/** - * Shuffle signals before processing them in the trader, avoiding favoring assets that appear always first in the - * actions of an event. - * - * @property trader - * @constructor Create empty Signal resolver - */ -private class SkipSymbolsTrader(private val trader: Trader, private val symbols: List) : Trader by trader { - - override fun create(signals: List, account: Account, event: Event): List { - val newSignals = signals.filter { it.asset.symbol !in symbols } - return trader.create(newSignals, account, event) - } -} - -/** - * Skip [symbols] that should not be converted into orders by removing them from the signals - */ -fun Trader.skipSymbols(vararg symbols: String): Trader = SkipSymbolsTrader(this, symbols.asList()) - - - diff --git a/roboquant/src/test/kotlin/org/roboquant/feeds/PriceItemTest.kt b/roboquant/src/test/kotlin/org/roboquant/feeds/PriceItemTest.kt index 987c4e0b..eb197fac 100644 --- a/roboquant/src/test/kotlin/org/roboquant/feeds/PriceItemTest.kt +++ b/roboquant/src/test/kotlin/org/roboquant/feeds/PriceItemTest.kt @@ -17,6 +17,7 @@ package org.roboquant.feeds import org.roboquant.TestData +import org.roboquant.common.Amount import org.roboquant.common.Stock import kotlin.test.Test import kotlin.test.assertContains @@ -36,6 +37,9 @@ internal class PriceItemTest { var price = p.getPrice() assertEquals(9.5, price) + val amt = p.getPriceAmount() + assertEquals(Amount("USD", 9.5), amt) + price = p.getPrice("WEIGHTED") assertEquals(9.25, price) diff --git a/roboquant/src/test/kotlin/org/roboquant/traders/CircuitBreakerTest.kt b/roboquant/src/test/kotlin/org/roboquant/traders/CircuitBreakerTest.kt new file mode 100644 index 00000000..e6c289b2 --- /dev/null +++ b/roboquant/src/test/kotlin/org/roboquant/traders/CircuitBreakerTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2020-2024 Neural Layer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.roboquant.traders + +import org.roboquant.TestData +import org.roboquant.brokers.Account +import org.roboquant.common.* +import org.roboquant.feeds.Event +import org.roboquant.orders.MarketOrder +import org.roboquant.orders.Instruction +import org.roboquant.strategies.Signal +import java.time.Instant +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class CircuitBreakerTest { + + private class MyTrader : Trader { + override fun create(signals: List, account: Account, event: Event): List { + return listOf( + MarketOrder(Stock("A"), 10), + MarketOrder(Stock("B"), 10), + MarketOrder(Stock("C"), 10) + ) + } + + } + + @Test + fun test() { + val account = TestData.usAccount() + val time = Instant.now() + val policy = CircuitBreaker(MyTrader(), 8, 1.hours) + var orders = policy.create(emptyList(), account, Event.empty(time)) + assertEquals(3, orders.size) + + orders = policy.create(emptyList(), account, Event.empty(time + 30.minutes)) + assertEquals(3, orders.size) + + orders = policy.create(emptyList(), account, Event.empty(time + 50.minutes)) + assertEquals(0, orders.size) + + orders = policy.create(emptyList(), account, Event.empty(time + 51.minutes)) + assertEquals(0, orders.size) + + orders = policy.create(emptyList(), account, Event.empty(time + 120.minutes)) + assertEquals(3, orders.size) + + } +} diff --git a/roboquant/src/test/kotlin/org/roboquant/traders/FlexTraderTest.kt b/roboquant/src/test/kotlin/org/roboquant/traders/FlexTraderTest.kt new file mode 100644 index 00000000..9ccbdc81 --- /dev/null +++ b/roboquant/src/test/kotlin/org/roboquant/traders/FlexTraderTest.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2020-2024 Neural Layer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.roboquant.traders + +import org.roboquant.TestData +import org.roboquant.brokers.sim.execution.InternalAccount +import org.roboquant.common.* +import org.roboquant.feeds.Event +import org.roboquant.feeds.PriceItem +import org.roboquant.feeds.TradePrice +import org.roboquant.orders.* +import org.roboquant.strategies.Signal +import java.time.Instant +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +internal class FlexTraderTest { + + @Test + fun order() { + val policy = FlexTrader() + val signals = mutableListOf() + val event = Event(Instant.now(), emptyList()) + val account = InternalAccount(Currency.USD).toAccount() + val orders = policy.create(signals, account, event) + assertTrue(orders.isEmpty()) + } + + @Test + fun order3() { + val policy = FlexTrader() + val orders = run(policy) + assertTrue(orders.isNotEmpty()) + + val order = orders.first() + assertTrue(order is MarketOrder) + assertEquals("TEST123", order.asset.symbol) + assertEquals(Size(204), order.size) + } + + @Test + fun orderMinPrice() { + val policy = FlexTrader { + minPrice = 10.USD + } + val asset = Stock("TEST123") + val signals = listOf(Signal.buy(asset)) + + val event1 = Event(Instant.now(), listOf(TradePrice(asset, 5.0))) + val account = TestData.usAccount() + val orders1 = policy.create(signals, account, event1) + assertTrue(orders1.isEmpty()) + + val event2 = Event(Instant.now(), listOf(TradePrice(asset, 15.0))) + val orders2 = policy.create(signals, account, event2) + assertTrue(orders2.isNotEmpty()) + } + + @Test + fun order2() { + + class MyTrader(val percentage: Double = 0.05) : FlexTrader() { + + override fun createOrder(signal: Signal, size: Size, priceItem: PriceItem): Instruction { + val asset = signal.asset + val direction = if (size.isPositive) 1.0 else -1.0 + val percentage = percentage * direction + val price = priceItem.getPrice(config.priceType) + + return BracketOrder( + MarketOrder(asset, size), + LimitOrder(asset, size, price * (1 + percentage)), + StopOrder(asset, size, price * (1 - percentage)) + ) + } + } + + val policy = MyTrader() + val signals = mutableListOf() + val event = Event(Instant.now(), emptyList()) + val account = InternalAccount(Currency.USD).toAccount() + val orders = policy.create(signals, account, event) + assertTrue(orders.isEmpty()) + + } + + + private fun run(policy: FlexTrader): List { + val asset = Stock("TEST123") + val signals = listOf(Signal.buy(asset)) + val event = Event(Instant.now(), listOf(TradePrice(asset, 5.0))) + val account = TestData.usAccount() + return policy.create(signals, account, event) + } + + @Test + fun predefined() { + val policy = FlexTrader.bracketOrders() + val orders = run(policy) + assertTrue(orders.isNotEmpty()) + + val first = orders.first() + assertTrue(first is BracketOrder) + + val entry = first.entry + val stop = first.stopLoss + val profit = first.takeProfit + + assertTrue(entry is MarketOrder) + + assertTrue(stop is StopOrder) + assertEquals(5.0 * 0.99, stop.stop) + + assertTrue(profit is TrailOrder) + assertEquals(0.05, profit.trailPercentage) + } + + @Test + fun predefined2() { + val policy = FlexTrader.limitOrders() + val orders = run(policy) + assertTrue(orders.isNotEmpty()) + + val first = orders.first() + assertTrue(first is LimitOrder) + assertEquals(5 * 0.99, first.limit) + } + + @Test + fun predefined3() { + val policy = FlexTrader.singleAsset() + val orders = run(policy) + assertTrue(orders.isNotEmpty()) + + val first = orders.first() + assertTrue(first is MarketOrder) + } + + @Test + fun chaining() { + val policy = FlexTrader() + .circuitBreaker(10, 1.days) + val signals = mutableListOf() + val event = Event(Instant.now(), emptyList()) + val account = InternalAccount(Currency.USD).toAccount() + val orders = policy.create(signals, account, event) + assertTrue(orders.isEmpty()) + } + + +} diff --git a/roboquant/src/test/kotlin/org/roboquant/traders/SignalResolutionTest.kt b/roboquant/src/test/kotlin/org/roboquant/traders/SignalResolutionTest.kt new file mode 100644 index 00000000..0db2fbfe --- /dev/null +++ b/roboquant/src/test/kotlin/org/roboquant/traders/SignalResolutionTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2020-2024 Neural Layer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.roboquant.traders + +import org.roboquant.TestData +import org.roboquant.common.Stock +import org.roboquant.feeds.Event +import org.roboquant.feeds.TradePrice +import org.roboquant.strategies.Signal +import java.time.Instant +import kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class SignalResolutionTest { + + @Test + fun testSignalShuffle() { + val policy = FlexTrader().shuffleSignals(Random(42)) + val account = TestData.usAccount() + val assets = listOf(Stock("A"), Stock("B"), Stock("C"), Stock("D")) + val signals = assets.map { Signal(it, 1.0) } + val items = assets.map { TradePrice(it, 100.0, 10.0) } + val orders = policy.create(signals, account, Event(Instant.now(), items)) + assertEquals(signals.size, orders.size) + } + +}