├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ ├── io │ └── codera │ │ └── quant │ │ ├── Application.java │ │ ├── BackTestApplication.java │ │ ├── backtest │ │ └── BollingerBandsBackTest.java │ │ ├── config │ │ ├── Config.java │ │ ├── ContractBuilder.java │ │ └── IbConnectionHandler.java │ │ ├── context │ │ ├── IbTradingContext.java │ │ └── TradingContext.java │ │ ├── exception │ │ ├── CriterionViolationException.java │ │ ├── NoOrderAvailable.java │ │ ├── NotEnoughDataToCalculateDiffMean.java │ │ ├── NotEnoughDataToCalculateZScore.java │ │ └── PriceNotAvailableException.java │ │ ├── observers │ │ ├── AccountObserver.java │ │ ├── HistoryObserver.java │ │ ├── IbAccountObserver.java │ │ ├── IbHistoryObserver.java │ │ ├── IbMarketDataObserver.java │ │ ├── IbOrderObserver.java │ │ ├── MarketDataObserver.java │ │ └── OrderObserver.java │ │ ├── strategy │ │ ├── AbstractStrategy.java │ │ ├── BackTestResult.java │ │ ├── Criterion.java │ │ ├── IbPerMinuteStrategyRunner.java │ │ ├── Strategy.java │ │ ├── StrategyRunner.java │ │ ├── criterion │ │ │ ├── NoOpenOrdersExistEntryCriterion.java │ │ │ ├── OpenIbOrdersExistForAllSymbolsExitCriterion.java │ │ │ ├── OpenOrdersExistForAllSymbolsExitCriterion.java │ │ │ ├── common │ │ │ │ └── NoPendingOrdersCommonCriterion.java │ │ │ └── stoploss │ │ │ │ └── DefaultStopLossCriterion.java │ │ ├── kalman │ │ │ └── KalmanFilterStrategy.java │ │ └── meanrevertion │ │ │ ├── BollingerBandsStrategy.java │ │ │ ├── ZScore.java │ │ │ ├── ZScoreEntryCriterion.java │ │ │ └── ZScoreExitCriterion.java │ │ └── util │ │ ├── Helper.java │ │ └── MathUtil.java │ └── org │ └── lst │ └── trading │ ├── LICENSE │ ├── README.md │ ├── lib │ ├── backtest │ │ ├── BackTest.java │ │ ├── BackTestTradingContext.java │ │ ├── SimpleClosedOrder.java │ │ └── SimpleOrder.java │ ├── csv │ │ ├── CsvReader.java │ │ └── CsvWriter.java │ ├── model │ │ ├── Bar.java │ │ ├── ClosedOrder.java │ │ ├── Order.java │ │ ├── TradingContext.java │ │ └── TradingStrategy.java │ ├── series │ │ ├── DoubleSeries.java │ │ ├── MultipleDoubleSeries.java │ │ └── TimeSeries.java │ └── util │ │ ├── HistoricalPriceService.java │ │ ├── Http.java │ │ ├── Statistics.java │ │ ├── Util.java │ │ └── yahoo │ │ └── YahooFinance.java │ └── main │ ├── BacktestMain.java │ └── strategy │ ├── AbstractTradingStrategy.java │ ├── BuyAndHold.java │ ├── MultipleTradingStrategy.java │ └── kalman │ ├── Cointegration.java │ ├── CointegrationTradingStrategy.java │ └── KalmanFilter.java └── test ├── java └── io │ └── codera │ └── quant │ ├── config │ ├── GuiceJUnit4Runner.java │ └── TestConfig.java │ └── strategy │ └── meanrevertion │ └── ZScoreTest.java └── resources ├── GLD.csv └── USO.csv /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | 21 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 22 | hs_err_pid* 23 | 24 | .DS_Store 25 | .idea/* 26 | target/ 27 | *.iml 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Codera.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Codera Quant Java framework 2 | Codera Quant Java framework allows development of automated algorithmic trading strategies, supports backtesting using historical data taken from Interactive Brokers, Yahoo Finance, local database or CSV files and 3 | paper or live trade execution via Interactive Brokers TWS Java API. 4 | 5 | # Prerequisites 6 | 1. Maven installed 7 | 2. TWS has to be up and running and listening on port 7497 8 | 3. Download TWS jar http://interactivebrokers.github.io/. Downloaded archive usually contains built jar file in IBJts/source/JavaClient dir (as of 9.72.17 version) 9 | 4. Install jar locally 10 | `mvn install:install-file -DgroupId=tws-api -DartifactId=tws-api -Dversion=9.72.17-SNAPSHOT -Dfile=TwsApi.jar` 11 | 5. Add/update maven dependency e.g 12 | ``` 13 | 14 | tws-api 15 | tws-api 16 | 9.72.17-SNAPSHOT 17 | 18 | ``` 19 | 20 | # Strategy execution 21 | `$ mvn exec:java@app -Dexec.args="-h localhost -p 7497 -l "SPY,IWM""` 22 | 23 | # Backtest execution 24 | `mvn exec:java@test` 25 | 26 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | quant.codera.io 6 | quant 7 | 1.0-SNAPSHOT 8 | 9 | 10 | 11 | org.apache.maven.plugins 12 | maven-compiler-plugin 13 | 14 | 1.8 15 | 1.8 16 | 17 | 18 | 19 | org.codehaus.mojo 20 | exec-maven-plugin 21 | 1.2.1 22 | 23 | 24 | app 25 | 26 | java 27 | 28 | 29 | io.codera.quant.Application 30 | 31 | 32 | 33 | test 34 | 35 | java 36 | 37 | 38 | io.codera.quant.BackTestApplication 39 | 40 | 41 | 42 | 43 | 44 | 45 | jar 46 | 47 | quant 48 | http://maven.apache.org 49 | 50 | UTF-8 51 | 1.7.21 52 | 1.2.0 53 | 54 | 55 | 56 | 57 | org.slf4j 58 | slf4j-api 59 | ${slfj4.version} 60 | 61 | 62 | ch.qos.logback 63 | logback-classic 64 | ${logback.version} 65 | 66 | 67 | com.google.inject 68 | guice 69 | 4.1.0 70 | 71 | 72 | redis.clients 73 | jedis 74 | 2.9.0 75 | 76 | 77 | org.mockito 78 | mockito-all 79 | 1.10.19 80 | 81 | 82 | commons-cli 83 | commons-cli 84 | 1.3.1 85 | 86 | 87 | org.apache.commons 88 | commons-math3 89 | 3.6.1 90 | 91 | 92 | org.knowm.xchart 93 | xchart 94 | 3.2.2 95 | 96 | 97 | org.la4j 98 | la4j 99 | 0.6.0 100 | 101 | 102 | io.reactivex 103 | rxjava 104 | 1.2.3 105 | 106 | 107 | org.apache.httpcomponents 108 | httpclient 109 | 4.5.13 110 | 111 | 112 | joda-time 113 | joda-time 114 | 2.9.7 115 | 116 | 117 | org.springframework 118 | spring-web 119 | 4.3.5.RELEASE 120 | 121 | 122 | com.fasterxml.jackson.core 123 | jackson-databind 124 | 2.12.6.1 125 | 126 | 127 | mysql 128 | mysql-connector-java 129 | 8.0.28 130 | 131 | 132 | junit 133 | junit 134 | 4.13.1 135 | 136 | 137 | 138 | tws-api 139 | tws-api 140 | 9.72.17-SNAPSHOT 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/Application.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant; 2 | 3 | import com.google.inject.Guice; 4 | import com.google.inject.Injector; 5 | import io.codera.quant.config.Config; 6 | import io.codera.quant.strategy.Strategy; 7 | import io.codera.quant.strategy.StrategyRunner; 8 | import java.util.Arrays; 9 | import org.apache.commons.cli.CommandLine; 10 | import org.apache.commons.cli.CommandLineParser; 11 | import org.apache.commons.cli.DefaultParser; 12 | import org.apache.commons.cli.Options; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import static com.google.common.base.Preconditions.checkState; 17 | 18 | /** 19 | * Entry point for PT trading app. 20 | */ 21 | public class Application { 22 | 23 | private static final Logger logger = LoggerFactory.getLogger(Application.class); 24 | 25 | public static void main(String[] args) { 26 | Options options = new Options(); 27 | 28 | options.addOption("h", true, "Interactive Brokers host"); 29 | options.addOption("p", true, "Interactive Brokers port"); 30 | options.addOption("l", true, "List of symbols to trade"); 31 | 32 | // create the parser 33 | CommandLineParser parser = new DefaultParser(); 34 | try { 35 | // parse the command line arguments 36 | CommandLine cmd = parser.parse(options, args); 37 | 38 | logger.info("Starting app"); 39 | 40 | checkState(cmd.getOptionValue("h") != null, "host can not be null"); 41 | checkState(cmd.getOptionValue("p") != null, "port can not be null"); 42 | checkState(cmd.getOptionValue("l") != null, "symbol can not be null"); 43 | 44 | Injector injector = Guice.createInjector( 45 | new Config( 46 | cmd.getOptionValue("h"), 47 | Integer.valueOf(cmd.getOptionValue("p")), 48 | cmd.getOptionValue("l") 49 | )); 50 | 51 | StrategyRunner strategyRunner = injector.getInstance(StrategyRunner.class); 52 | Strategy strategy = injector.getInstance(Strategy.class); 53 | String[] symbolList = cmd.getOptionValue("l").split(","); 54 | 55 | strategyRunner.run(strategy, Arrays.asList(symbolList)); 56 | 57 | } 58 | catch(Exception e) { 59 | // oops, something went wrong 60 | logger.error("Something went wrong", e); 61 | } 62 | 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/BackTestApplication.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import com.ib.controller.ApiController; 5 | import io.codera.quant.config.IbConnectionHandler; 6 | import io.codera.quant.context.TradingContext; 7 | import io.codera.quant.strategy.Criterion; 8 | import io.codera.quant.strategy.Strategy; 9 | import io.codera.quant.strategy.criterion.NoOpenOrdersExistEntryCriterion; 10 | import io.codera.quant.strategy.criterion.OpenOrdersExistForAllSymbolsExitCriterion; 11 | import io.codera.quant.strategy.kalman.KalmanFilterStrategy; 12 | import io.codera.quant.util.Helper; 13 | import java.io.IOException; 14 | import java.sql.SQLException; 15 | import java.util.List; 16 | import java.util.Locale; 17 | import org.lst.trading.lib.backtest.BackTest; 18 | import org.lst.trading.lib.backtest.BackTestTradingContext; 19 | import org.lst.trading.lib.model.ClosedOrder; 20 | import org.lst.trading.lib.series.MultipleDoubleSeries; 21 | import org.lst.trading.main.strategy.kalman.Cointegration; 22 | 23 | import static java.lang.String.format; 24 | 25 | /** 26 | * Back test Kalman filter cointegration strategy against SPY/VOO pair using Interactive Brokers 27 | * historical data 28 | */ 29 | public class BackTestApplication { 30 | 31 | public static final String DEFAULT_HOST = "localhost"; 32 | public static final int DEFAULT_IB_PORT = 7497; 33 | public static final int DEFAULT_CLIENT_ID = 0; 34 | public static final int DAYS_OF_HISTORY = 7; 35 | 36 | public static void main(String[] args) throws IOException, SQLException { 37 | 38 | ApiController controller = 39 | new ApiController(new IbConnectionHandler(), valueOf -> {}, valueOf -> {}); 40 | controller.connect(DEFAULT_HOST, DEFAULT_IB_PORT, DEFAULT_CLIENT_ID, null); 41 | 42 | List contracts = ImmutableList.of("SPY", "VOO"); 43 | 44 | MultipleDoubleSeries priceSeries = 45 | Helper.getHistoryForSymbols(controller, DAYS_OF_HISTORY, contracts); 46 | // initialize the backtesting engine 47 | int deposit = 30000; 48 | BackTest backTest = new BackTest(deposit, priceSeries); 49 | backTest.setLeverage(4); 50 | 51 | TradingContext tradingContext = new BackTestTradingContext(); 52 | 53 | Strategy strategy = 54 | new KalmanFilterStrategy( 55 | contracts.get(0), 56 | contracts.get(1), 57 | tradingContext, 58 | new Cointegration(1e-4, 1e-3)); 59 | 60 | Criterion noOpenOrdersExistCriterion = 61 | new NoOpenOrdersExistEntryCriterion(tradingContext, contracts); 62 | Criterion openOrdersExistForAllSymbolsCriterion = 63 | new OpenOrdersExistForAllSymbolsExitCriterion(tradingContext, contracts); 64 | 65 | strategy.addEntryCriterion(noOpenOrdersExistCriterion); 66 | strategy.addExitCriterion(openOrdersExistForAllSymbolsCriterion); 67 | 68 | // do the backtest 69 | BackTest.Result result = backTest.run(strategy); 70 | 71 | // show results 72 | StringBuilder orders = new StringBuilder(); 73 | orders.append("id,amount,side,instrument,from,to,open,close,pl\n"); 74 | for (ClosedOrder order : result.getOrders()) { 75 | orders.append(format(Locale.US, "%d,%d,%s,%s,%s,%s,%f,%f,%f\n", order.getId(), 76 | Math.abs(order.getAmount()), order.isLong() ? "Buy" : "Sell", order.getInstrument(), 77 | order.getOpenInstant(), 78 | order.getCloseInstant(), 79 | order.getOpenPrice(), 80 | order.getClosePrice(), 81 | order.getPl())); 82 | } 83 | System.out.print(orders); 84 | 85 | System.out.println(); 86 | System.out.println("Backtest result of " + strategy.getClass() + ": " + strategy); 87 | System.out.println("Prices: " + priceSeries); 88 | System.out.println(format(Locale.US, "Simulated %d days, Initial deposit %d, Leverage %f", 89 | DAYS_OF_HISTORY, deposit, backTest.getLeverage())); 90 | System.out.println(format(Locale.US, "Commissions = %f", result.getCommissions())); 91 | System.out.println( 92 | format(Locale.US, 93 | "P/L = %.2f, Final value = %.2f, Result = %.2f%%, Annualized = %.2f%%, Sharpe (rf=0%%) = %.2f", 94 | result.getPl(), 95 | result.getFinalValue(), 96 | result.getReturn() * 100, result.getReturn() / (DAYS_OF_HISTORY / 251.) * 100, result.getSharpe())); 97 | // TODO: quick and dirty method to finish the program. Implement a better way 98 | System.exit(0); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/backtest/BollingerBandsBackTest.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.backtest; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import com.ib.controller.ApiController; 5 | import io.codera.quant.config.IbConnectionHandler; 6 | import io.codera.quant.context.TradingContext; 7 | import io.codera.quant.strategy.Criterion; 8 | import io.codera.quant.strategy.Strategy; 9 | import io.codera.quant.strategy.criterion.NoOpenOrdersExistEntryCriterion; 10 | import io.codera.quant.strategy.criterion.OpenOrdersExistForAllSymbolsExitCriterion; 11 | import io.codera.quant.strategy.meanrevertion.BollingerBandsStrategy; 12 | import io.codera.quant.strategy.meanrevertion.ZScore; 13 | import io.codera.quant.strategy.meanrevertion.ZScoreEntryCriterion; 14 | import io.codera.quant.strategy.meanrevertion.ZScoreExitCriterion; 15 | import io.codera.quant.util.Helper; 16 | import io.codera.quant.util.MathUtil; 17 | import java.util.List; 18 | import java.util.Locale; 19 | import org.lst.trading.lib.backtest.BackTest; 20 | import org.lst.trading.lib.backtest.BackTestTradingContext; 21 | import org.lst.trading.lib.model.ClosedOrder; 22 | import org.lst.trading.lib.series.MultipleDoubleSeries; 23 | 24 | import static java.lang.String.format; 25 | 26 | /** 27 | * Back test for {@link io.codera.quant.strategy.meanrevertion.BollingerBandsStrategy} 28 | */ 29 | public class BollingerBandsBackTest { 30 | 31 | public static final String DEFAULT_HOST = "localhost"; 32 | public static final int DEFAULT_IB_PORT = 7497; 33 | public static final int DEFAULT_CLIENT_ID = 0; 34 | public static final int DAYS_OF_HISTORY = 7; 35 | 36 | public static void main(String[] args) { 37 | 38 | ApiController controller = 39 | new ApiController(new IbConnectionHandler(), valueOf -> {}, valueOf -> {}); 40 | controller.connect(DEFAULT_HOST, DEFAULT_IB_PORT, DEFAULT_CLIENT_ID, null); 41 | 42 | List contracts = ImmutableList.of("EWA", "EWC"); 43 | 44 | MultipleDoubleSeries priceSeries = 45 | Helper.getHistoryForSymbols(controller, DAYS_OF_HISTORY, contracts); 46 | // initialize the backtesting engine 47 | int deposit = 30000; 48 | BackTest backTest = new BackTest(deposit, priceSeries); 49 | backTest.setLeverage(4); 50 | 51 | TradingContext tradingContext = new BackTestTradingContext(); 52 | 53 | ZScore zScore = new ZScore(20, new MathUtil()); 54 | 55 | Strategy bollingerBandsStrategy = new BollingerBandsStrategy( 56 | contracts.get(0), 57 | contracts.get(1), 58 | tradingContext, 59 | zScore); 60 | 61 | Criterion zScoreEntryCriterion = new ZScoreEntryCriterion(contracts.get(0), contracts.get(1), 1, zScore, 62 | tradingContext); 63 | 64 | Criterion zScoreExitCriterion = new ZScoreExitCriterion(contracts.get(0), contracts.get(1), 0, zScore, 65 | tradingContext); 66 | 67 | Criterion noOpenOrdersExistCriterion = 68 | new NoOpenOrdersExistEntryCriterion(tradingContext, contracts); 69 | Criterion openOrdersExistForAllSymbolsCriterion = 70 | new OpenOrdersExistForAllSymbolsExitCriterion(tradingContext, contracts); 71 | 72 | bollingerBandsStrategy.addEntryCriterion(noOpenOrdersExistCriterion); 73 | bollingerBandsStrategy.addEntryCriterion(zScoreEntryCriterion); 74 | bollingerBandsStrategy.addExitCriterion(openOrdersExistForAllSymbolsCriterion); 75 | bollingerBandsStrategy.addExitCriterion(zScoreExitCriterion); 76 | 77 | // do the backtest 78 | BackTest.Result result = backTest.run(bollingerBandsStrategy); 79 | 80 | // show results 81 | StringBuilder orders = new StringBuilder(); 82 | orders.append("id,amount,side,instrument,from,to,open,close,pl\n"); 83 | for (ClosedOrder order : result.getOrders()) { 84 | orders.append(format(Locale.US, "%d,%d,%s,%s,%s,%s,%f,%f,%f\n", order.getId(), 85 | Math.abs(order.getAmount()), order.isLong() ? "Buy" : "Sell", order.getInstrument(), 86 | order.getOpenInstant(), 87 | order.getCloseInstant(), 88 | order.getOpenPrice(), 89 | order.getClosePrice(), 90 | order.getPl())); 91 | } 92 | System.out.println(orders); 93 | 94 | System.out.println("Prices: " + priceSeries); 95 | System.out.println(format(Locale.US, "Simulated %d days, Initial deposit %d, Leverage %f", 96 | DAYS_OF_HISTORY, deposit, backTest.getLeverage())); 97 | System.out.println(format(Locale.US, "Commissions = %f", result.getCommissions())); 98 | System.out.println( 99 | format(Locale.US, 100 | "P/L = %.2f, Final value = %.2f, Result = %.2f%%, Annualized = %.2f%%, Sharpe (rf=0%%) = %.2f", 101 | result.getPl(), 102 | result.getFinalValue(), 103 | result.getReturn() * 100, result.getReturn() / (DAYS_OF_HISTORY / 251.) * 100, result.getSharpe())); 104 | // TODO: quick and dirty method to finish the program. Implement a better way 105 | System.exit(0); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/config/Config.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.config; 2 | 3 | import com.google.inject.AbstractModule; 4 | import com.google.inject.Provides; 5 | import com.ib.client.OrderType; 6 | import com.ib.controller.ApiController; 7 | import io.codera.quant.context.IbTradingContext; 8 | import io.codera.quant.context.TradingContext; 9 | import io.codera.quant.strategy.Criterion; 10 | import io.codera.quant.strategy.IbPerMinuteStrategyRunner; 11 | import io.codera.quant.strategy.Strategy; 12 | import io.codera.quant.strategy.StrategyRunner; 13 | import io.codera.quant.strategy.criterion.NoOpenOrdersExistEntryCriterion; 14 | import io.codera.quant.strategy.criterion.OpenIbOrdersExistForAllSymbolsExitCriterion; 15 | import io.codera.quant.strategy.criterion.common.NoPendingOrdersCommonCriterion; 16 | import io.codera.quant.strategy.criterion.stoploss.DefaultStopLossCriterion; 17 | import io.codera.quant.strategy.meanrevertion.BollingerBandsStrategy; 18 | import io.codera.quant.strategy.meanrevertion.ZScore; 19 | import io.codera.quant.strategy.meanrevertion.ZScoreEntryCriterion; 20 | import io.codera.quant.strategy.meanrevertion.ZScoreExitCriterion; 21 | import io.codera.quant.util.MathUtil; 22 | import java.sql.SQLException; 23 | import java.util.Arrays; 24 | import java.util.List; 25 | 26 | /** 27 | */ 28 | public class Config extends AbstractModule { 29 | 30 | private final String host; 31 | private final int port; 32 | private final String symbolList; 33 | 34 | public Config(String host, int port, String symbolList) { 35 | this.host = host; 36 | this.port = port; 37 | this.symbolList = symbolList; 38 | } 39 | 40 | @Override 41 | protected void configure() { 42 | bind(StrategyRunner.class).to(IbPerMinuteStrategyRunner.class); 43 | } 44 | 45 | @Provides 46 | ApiController apiController() { 47 | ApiController controller = 48 | new ApiController(new IbConnectionHandler(), valueOf -> { 49 | }, valueOf -> {}); 50 | controller.connect(host, port, 0, null); 51 | return controller; 52 | } 53 | 54 | @Provides 55 | TradingContext tradingContext(ApiController controller) throws SQLException, ClassNotFoundException { 56 | return new IbTradingContext( 57 | controller, 58 | new ContractBuilder(), 59 | OrderType.MKT, 60 | // DriverManager.getConnection("jdbc:mysql://localhost/fx", "root", "admin"), 61 | 2 62 | ); 63 | } 64 | 65 | @Provides 66 | Strategy strategy(TradingContext tradingContext) { 67 | List contracts = Arrays.asList(symbolList.split(",")); 68 | 69 | ZScore zScore = new ZScore(20, new MathUtil()); 70 | 71 | Strategy strategy = new BollingerBandsStrategy( 72 | contracts.get(0), 73 | contracts.get(1), 74 | tradingContext, 75 | zScore); 76 | 77 | Criterion zScoreEntryCriterion = new ZScoreEntryCriterion(contracts.get(0), contracts.get(1), 1, zScore, 78 | tradingContext); 79 | 80 | Criterion zScoreExitCriterion = new ZScoreExitCriterion(contracts.get(0), contracts.get(1), 0, zScore, 81 | tradingContext); 82 | 83 | Criterion noPendingOrdersCommonCriterion = 84 | new NoPendingOrdersCommonCriterion(tradingContext, contracts); 85 | 86 | Criterion noOpenOrdersExistCriterion = 87 | new NoOpenOrdersExistEntryCriterion(tradingContext, contracts); 88 | 89 | Criterion openOrdersExistForAllSymbolsCriterion = 90 | new OpenIbOrdersExistForAllSymbolsExitCriterion(tradingContext, contracts); 91 | 92 | Criterion stopLoss = new DefaultStopLossCriterion(contracts, -100, tradingContext); 93 | 94 | strategy.addCommonCriterion(noPendingOrdersCommonCriterion); 95 | 96 | strategy.addEntryCriterion(noOpenOrdersExistCriterion); 97 | strategy.addEntryCriterion(zScoreEntryCriterion); 98 | 99 | strategy.addExitCriterion(openOrdersExistForAllSymbolsCriterion); 100 | strategy.addEntryCriterion(zScoreExitCriterion); 101 | 102 | strategy.addStopLossCriterion(stopLoss); 103 | 104 | return strategy; 105 | 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/config/ContractBuilder.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.config; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import com.ib.client.Contract; 5 | import com.ib.client.Types; 6 | import java.util.Map; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import static com.google.common.base.Preconditions.checkArgument; 11 | 12 | /** 13 | * Builds the contract 14 | */ 15 | public class ContractBuilder { 16 | 17 | private final static Logger log = LoggerFactory.getLogger(ContractBuilder.class); 18 | private static final Map futuresMap = ImmutableMap.of("ES=F", 50, "YM=F", 5, 19 | "TF=F", 50); 20 | 21 | public Contract build(String symbolName) { 22 | Contract contract = new Contract(); 23 | 24 | if(symbolName.contains("/")) { 25 | log.debug("{} is a Forex symbol", symbolName); 26 | 27 | String[] fxSymbols = symbolName.split("/"); 28 | 29 | contract.symbol(fxSymbols[0]); 30 | contract.exchange("IDEALPRO"); 31 | contract.secType(Types.SecType.CASH); 32 | contract.currency(fxSymbols[1]); 33 | return contract; 34 | } else if(symbolName.contains("=F")){ 35 | String s = symbolName.replace("=F", ""); 36 | Map futuresMap = ImmutableMap.of( 37 | "ES", "GLOBEX", 38 | "YM", "ECBOT", 39 | "TF", "NYBOT"); 40 | contract.symbol(s); 41 | contract.exchange(futuresMap.get(s)); 42 | // contract.expiry("201706"); 43 | contract.secType(Types.SecType.FUT); 44 | contract.currency("USD"); 45 | log.info("Contract " + contract); 46 | return contract; 47 | } 48 | contract.symbol(symbolName); 49 | contract.localSymbol(symbolName); 50 | contract.exchange("SMART"); 51 | contract.primaryExch("ARCA"); 52 | contract.secType(Types.SecType.STK); 53 | contract.currency("USD"); 54 | return contract; 55 | } 56 | 57 | /** 58 | * If its a forex symbol, then for some strategies it is necessary to flip the price to 59 | * understand the price based on USD (e.g. how much dollars is needed to buy 1 unit of currency 60 | * in traded pair) 61 | * 62 | * @param symbol symbol name 63 | * @return adjusted price, basically 1/ 64 | */ 65 | public static double getSymbolPrice(String symbol, double price) { 66 | if(symbol.contains("/")) { 67 | String[] fxSymbols = symbol.split("/"); 68 | if(fxSymbols[0].equals("USD")) { 69 | return 1/price; 70 | } 71 | } 72 | return price; 73 | } 74 | 75 | public static Integer getFutureMultiplier(String futureSymbol) { 76 | checkArgument(futureSymbol != null, "symbol is null"); 77 | // TODO (Dsinyakov) : refactor to throw exception instead of returning null 78 | return futuresMap.get(futureSymbol); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/config/IbConnectionHandler.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.config; 2 | 3 | import com.google.common.collect.Lists; 4 | import com.ib.controller.ApiController.IConnectionHandler; 5 | import java.util.ArrayList; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | /** 10 | * 11 | */ 12 | public class IbConnectionHandler implements IConnectionHandler { 13 | 14 | private static Logger logger = LoggerFactory.getLogger(IbConnectionHandler.class); 15 | private ArrayList accountList = Lists.newArrayList(); 16 | 17 | @Override 18 | public void connected() { 19 | logger.info("Connected"); 20 | } 21 | 22 | @Override 23 | public void disconnected() { 24 | logger.info("Disconnected"); 25 | } 26 | 27 | @Override 28 | public void accountList(ArrayList list) { 29 | show("Received account list"); 30 | accountList.clear(); 31 | accountList.addAll(list); 32 | } 33 | 34 | @Override 35 | public void error(Exception e) { 36 | logger.error(e.getMessage()); 37 | e.printStackTrace(); 38 | } 39 | 40 | @Override 41 | public void message(int id, int errorCode, String errorMsg) { 42 | logger.info("Message id: {}, errorCode: {}, errorMsg: {}", id, errorCode, errorMsg); 43 | } 44 | 45 | @Override 46 | public void show(String string) { 47 | logger.info(string); 48 | } 49 | 50 | public ArrayList getAccountList() { 51 | return accountList; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/context/IbTradingContext.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.context; 2 | 3 | import com.google.common.collect.Lists; 4 | import com.google.common.collect.Maps; 5 | import com.ib.client.Contract; 6 | import com.ib.client.OrderState; 7 | import com.ib.client.OrderStatus; 8 | import com.ib.client.OrderType; 9 | import com.ib.client.TickType; 10 | import com.ib.client.Types; 11 | import com.ib.controller.ApiController; 12 | import io.codera.quant.config.ContractBuilder; 13 | import io.codera.quant.exception.NoOrderAvailable; 14 | import io.codera.quant.exception.PriceNotAvailableException; 15 | import io.codera.quant.observers.AccountObserver; 16 | import io.codera.quant.observers.HistoryObserver; 17 | import io.codera.quant.observers.IbAccountObserver; 18 | import io.codera.quant.observers.IbHistoryObserver; 19 | import io.codera.quant.observers.IbMarketDataObserver; 20 | import io.codera.quant.observers.IbOrderObserver; 21 | import io.codera.quant.observers.MarketDataObserver; 22 | import io.codera.quant.observers.OrderObserver; 23 | import java.math.BigDecimal; 24 | import java.math.RoundingMode; 25 | import java.sql.Connection; 26 | import java.sql.PreparedStatement; 27 | import java.sql.SQLException; 28 | import java.time.Instant; 29 | import java.time.LocalDateTime; 30 | import java.time.format.DateTimeFormatter; 31 | import java.util.List; 32 | import java.util.Map; 33 | import java.util.concurrent.atomic.AtomicInteger; 34 | import org.lst.trading.lib.backtest.SimpleClosedOrder; 35 | import org.lst.trading.lib.backtest.SimpleOrder; 36 | import org.lst.trading.lib.model.ClosedOrder; 37 | import org.lst.trading.lib.model.Order; 38 | import org.lst.trading.lib.series.DoubleSeries; 39 | import org.slf4j.Logger; 40 | import org.slf4j.LoggerFactory; 41 | import rx.Observable; 42 | import rx.Subscriber; 43 | 44 | import static com.google.common.base.Preconditions.checkArgument; 45 | 46 | /** 47 | * Interactive Brokers trading context. 48 | */ 49 | public class IbTradingContext implements TradingContext { 50 | 51 | private List contracts; 52 | private Map ibContracts; 53 | private Map ibOrders; 54 | private final ApiController controller; 55 | private Map> contractPrices; 56 | private Map observers; 57 | private OrderType orderType; 58 | private AtomicInteger orderId = new AtomicInteger(0); 59 | private double availableFunds; 60 | private double netValue; 61 | private int leverage; 62 | private final ContractBuilder contractBuilder; 63 | private Connection connection; 64 | 65 | private static final Logger log = LoggerFactory.getLogger(IbTradingContext.class); 66 | 67 | private IbTradingContext(ApiController controller, ContractBuilder contractBuilder, int leverage, 68 | OrderType orderType) { 69 | this.controller = controller; 70 | this.contractBuilder = contractBuilder; 71 | this.leverage = leverage; 72 | this.orderType = orderType; 73 | } 74 | 75 | public IbTradingContext(ApiController controller, ContractBuilder contractBuilder, 76 | OrderType orderType, int leverage) 77 | throws ClassNotFoundException, SQLException { 78 | this(controller, contractBuilder, leverage, orderType); 79 | this.contracts = Lists.newArrayList(); 80 | this.contractPrices = Maps.newConcurrentMap(); 81 | 82 | this.observers = Maps.newConcurrentMap(); 83 | this.ibContracts = Maps.newConcurrentMap(); 84 | this.ibOrders = Maps.newConcurrentMap(); 85 | AccountObserver accountObserver = new IbAccountObserver(this); 86 | ((IbAccountObserver)accountObserver) 87 | .observableCashBalance().subscribe(aDouble -> availableFunds = aDouble); 88 | ((IbAccountObserver)accountObserver) 89 | .observableNetValue().subscribe(aDouble -> netValue = aDouble); 90 | controller.reqAccountUpdates(true, "", accountObserver); 91 | } 92 | 93 | public IbTradingContext(ApiController controller, ContractBuilder contractBuilder, 94 | OrderType orderType, Connection connection, int leverage) 95 | throws ClassNotFoundException, SQLException { 96 | this(controller, contractBuilder, orderType, leverage); 97 | this.connection = connection; 98 | } 99 | 100 | @Override 101 | public double getLastPrice(String contract) throws PriceNotAvailableException { 102 | checkArgument(contract != null, "contract is null"); 103 | if(!contractPrices.containsKey(contract) || 104 | !contractPrices.get(contract).containsKey(TickType.ASK)) { 105 | throw new PriceNotAvailableException(); 106 | } 107 | double price = contractPrices.get(contract).get(TickType.ASK); 108 | if (connection != null) { 109 | try { 110 | String sql = "INSERT INTO quotes (symbol, price) VALUES (?, ?)"; 111 | PreparedStatement stmt = connection.prepareStatement(sql); 112 | stmt.setString(1, contract); 113 | stmt.setDouble(2, price); 114 | stmt.execute(); 115 | 116 | } catch (SQLException e) { 117 | log.error("Could not insert record into database: " + contract + " - " + price, e); 118 | } 119 | 120 | } 121 | return contractPrices.get(contract).get(TickType.ASK); 122 | } 123 | 124 | public double getLastPrice(String contract, TickType tickType) throws PriceNotAvailableException { 125 | checkArgument(contract != null, "contract is null"); 126 | checkArgument(tickType != null, "tickType is null"); 127 | if(!contractPrices.containsKey(contract) || !contractPrices.get(contract). 128 | containsKey(tickType)) { 129 | throw new PriceNotAvailableException(); 130 | } 131 | return contractPrices.get(contract).get(tickType); 132 | } 133 | 134 | @Override 135 | public void addContract(String contractSymbol) { 136 | contracts.add(contractSymbol); 137 | 138 | IbMarketDataObserver marketDataObserver = new IbMarketDataObserver(contractSymbol); 139 | observers.put(contractSymbol, marketDataObserver); 140 | 141 | Contract contract = contractBuilder.build(contractSymbol); 142 | ibContracts.put(contractSymbol, contract); 143 | 144 | controller.reqTopMktData(contract, "", false, marketDataObserver); 145 | 146 | marketDataObserver.priceObservable().subscribe(new Subscriber() { 147 | @Override 148 | public void onCompleted() {} 149 | @Override 150 | public void onError(Throwable throwable) {} 151 | 152 | @Override 153 | public void onNext(MarketDataObserver.Price price) { 154 | if(contractPrices.containsKey(contractSymbol)) { 155 | contractPrices.get(contractSymbol).put(price.getTickType(), price.getPrice()); 156 | } else { 157 | Map map = Maps.newConcurrentMap(); 158 | map.put(price.getTickType(), price.getPrice()); 159 | contractPrices.put(contractSymbol, map); 160 | } 161 | 162 | } 163 | }); 164 | } 165 | 166 | @Override 167 | public void removeContract(String contractSymbol) { 168 | contracts.remove(contractSymbol); 169 | contractPrices.remove(contractSymbol); 170 | ibContracts.remove(contractSymbol); 171 | controller.cancelTopMktData(observers.get(contractSymbol)); 172 | } 173 | 174 | @Override 175 | public List getContracts() { 176 | return contracts; 177 | } 178 | 179 | @Override 180 | public double getAvailableFunds() { 181 | return availableFunds; 182 | } 183 | 184 | @Override 185 | public double getNetValue() { 186 | return netValue; 187 | } 188 | 189 | @Override 190 | public double getLeverage() { 191 | return leverage; 192 | } 193 | 194 | @Override 195 | public MarketDataObserver getObserver(String contractSymbol) { 196 | checkArgument(contractSymbol != null, "contractSymbol is null"); 197 | 198 | return observers.get(contractSymbol); 199 | } 200 | 201 | @Override 202 | public Order order(String contractSymbol, boolean buy, int amount) 203 | throws PriceNotAvailableException { 204 | checkArgument(contractSymbol != null, "contractSymbol is null"); 205 | 206 | Order order = new IbOrder( 207 | orderId.getAndIncrement(), 208 | contractSymbol, 209 | Instant.now(), 210 | getLastPrice(contractSymbol), 211 | buy ? amount : -amount, 212 | submitIbOrder(contractSymbol, buy, amount, getLastPrice(contractSymbol)) 213 | ); 214 | 215 | ibOrders.put(contractSymbol, order); 216 | 217 | return order; 218 | } 219 | 220 | @Override 221 | public ClosedOrder close(Order order) throws PriceNotAvailableException { 222 | checkArgument(order != null, "order is null"); 223 | 224 | log.debug("Amount taken from {} order that isLong {} : {}", order.getInstrument(), 225 | order.isLong(), 226 | order.getAmount()); 227 | 228 | IbClosedOrder closedOrder = new IbClosedOrder( 229 | (SimpleOrder) order, 230 | Instant.now(), 231 | getLastPrice(order.getInstrument()), 232 | submitIbOrder( 233 | order.getInstrument(), 234 | order.isShort(), 235 | order.getAmount(), 236 | getLastPrice(order.getInstrument()))); 237 | 238 | ibOrders.remove(order.getInstrument()); 239 | ibOrders.put(order.getInstrument(), closedOrder); 240 | 241 | return closedOrder; 242 | 243 | } 244 | 245 | public void setOrderType(OrderType orderType) { 246 | this.orderType = orderType; 247 | } 248 | 249 | public Order getLastOrderBySymbol(String symbol) throws NoOrderAvailable { 250 | checkArgument(symbol != null, "symbol is null"); 251 | if(!ibOrders.containsKey(symbol)) { 252 | throw new NoOrderAvailable(); 253 | } 254 | return ibOrders.get(symbol); 255 | } 256 | 257 | public DoubleSeries getHistory(String symbol, int daysOfHistory) { 258 | DateTimeFormatter formatter = 259 | DateTimeFormatter.ofPattern("yyyyMMdd HH:mm:ss"); 260 | String date = LocalDateTime.now().format(formatter); 261 | 262 | ContractBuilder contractBuilder = new ContractBuilder(); 263 | 264 | Contract contract = contractBuilder.build(symbol); 265 | HistoryObserver historyObserver = new IbHistoryObserver(symbol); 266 | controller.reqHistoricalData(contract, date, daysOfHistory, Types.DurationUnit.DAY, 267 | Types.BarSize._1_min, Types.WhatToShow.TRADES, false, historyObserver); 268 | return ((IbHistoryObserver)historyObserver).observableDoubleSeries() 269 | .toBlocking() 270 | .first(); 271 | 272 | } 273 | 274 | public DoubleSeries getHistoryInMinutes(String symbol, int numberOfMinutes) { 275 | DateTimeFormatter formatter = 276 | DateTimeFormatter.ofPattern("yyyyMMdd HH:mm:ss"); 277 | String date = LocalDateTime.now().format(formatter); 278 | ContractBuilder contractBuilder = new ContractBuilder(); 279 | 280 | Contract contract = contractBuilder.build(symbol); 281 | HistoryObserver historyObserver = new IbHistoryObserver(symbol); 282 | controller.reqHistoricalData(contract, date, numberOfMinutes * 60, Types.DurationUnit.SECOND, 283 | Types.BarSize._1_min, Types.WhatToShow.TRADES, false, historyObserver); 284 | 285 | DoubleSeries history = ((IbHistoryObserver) historyObserver).observableDoubleSeries() 286 | .toBlocking() 287 | .first(); 288 | // We might need to pull history for last day if time of request is after market is closed 289 | if(history.size() == 0 || history.size() < numberOfMinutes) { 290 | controller.reqHistoricalData(contract, date, 1, Types.DurationUnit.DAY, 291 | Types.BarSize._1_min, Types.WhatToShow.TRADES, false, historyObserver); 292 | 293 | history = ((IbHistoryObserver) historyObserver).observableDoubleSeries() 294 | .toBlocking() 295 | .first(); 296 | 297 | return history.tail(numberOfMinutes); 298 | } 299 | 300 | return history; 301 | } 302 | 303 | private Observable submitIbOrder(String contractSymbol, boolean buy, int amount, 304 | double price) { 305 | com.ib.client.Order ibOrder = new com.ib.client.Order(); 306 | 307 | if(buy) { 308 | ibOrder.action(Types.Action.BUY); 309 | } else { 310 | ibOrder.action(Types.Action.SELL); 311 | amount = -amount; 312 | } 313 | 314 | ibOrder.orderType(orderType); 315 | if(orderType == OrderType.LMT) { 316 | ibOrder.lmtPrice(price); 317 | } 318 | ibOrder.totalQuantity(Math.abs(amount)); 319 | 320 | OrderObserver orderObserver = new IbOrderObserver(); 321 | log.debug("Sending order for {} in amount of {}", contractSymbol, amount); 322 | 323 | controller.placeOrModifyOrder(ibContracts.get(contractSymbol), ibOrder, orderObserver); 324 | 325 | return ((IbOrderObserver)orderObserver).observableOrderState(); 326 | } 327 | 328 | public class IbOrder extends SimpleOrder { 329 | 330 | private OrderStatus orderStatus; 331 | 332 | public IbOrder(int id, 333 | String contractSymbol, 334 | Instant openInstant, 335 | double openPrice, 336 | int amount, 337 | Observable observableOrderState) { 338 | super(id, contractSymbol, openInstant, openPrice, amount); 339 | this.orderStatus = OrderStatus.Inactive; 340 | observableOrderState.subscribe(newOrderState -> orderStatus = newOrderState.status()); 341 | log.info("{} OPEN order in amount of {} at price {}", contractSymbol, amount, openPrice); 342 | } 343 | 344 | public OrderStatus getOrderStatus() { 345 | return orderStatus; 346 | } 347 | 348 | } 349 | public class IbClosedOrder extends SimpleClosedOrder { 350 | 351 | private OrderStatus orderStatus; 352 | 353 | public IbClosedOrder(SimpleOrder simpleOrder, 354 | Instant closeInstant, 355 | double closePrice, 356 | Observable observableOrderState) { 357 | super(simpleOrder, closePrice, closeInstant); 358 | this.orderStatus = OrderStatus.Inactive; 359 | observableOrderState.subscribe(newOrderState -> { 360 | orderStatus = newOrderState.status(); 361 | if(newOrderState.status() == OrderStatus.Filled) { 362 | ibOrders.remove(simpleOrder.getInstrument()); 363 | } 364 | }); 365 | log.info("{} CLOSE order in amount of {} at price {}", 366 | simpleOrder.getInstrument(), -simpleOrder.getAmount(), closePrice); 367 | } 368 | 369 | public OrderStatus getOrderStatus() { 370 | return orderStatus; 371 | } 372 | 373 | } 374 | 375 | @Override 376 | public double getChangeBySymbol(String symbol) throws PriceNotAvailableException { 377 | 378 | double closePrice = getLastPrice(symbol, TickType.CLOSE); 379 | double currentPrice = getLastPrice(symbol); 380 | 381 | BigDecimal diff = BigDecimal.valueOf(currentPrice).add(BigDecimal.valueOf(-closePrice)); 382 | 383 | BigDecimal res = diff.multiply(BigDecimal.valueOf(100)) 384 | .divide(BigDecimal.valueOf(closePrice), RoundingMode.HALF_UP); 385 | BigDecimal rounded = res.setScale(2, RoundingMode.HALF_UP); 386 | return rounded.doubleValue(); 387 | 388 | } 389 | 390 | public double getChangeBySymbol(String symbol, double price) throws PriceNotAvailableException { 391 | 392 | double closePrice = getLastPrice(symbol, TickType.CLOSE); 393 | 394 | BigDecimal diff = BigDecimal.valueOf(price).add(BigDecimal.valueOf(-closePrice)); 395 | 396 | BigDecimal res = diff.multiply(BigDecimal.valueOf(100)) 397 | .divide(BigDecimal.valueOf(closePrice), RoundingMode.HALF_UP); 398 | BigDecimal rounded = res.setScale(2, RoundingMode.HALF_UP); 399 | return rounded.doubleValue(); 400 | 401 | } 402 | 403 | } 404 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/context/TradingContext.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.context; 2 | 3 | import io.codera.quant.exception.NoOrderAvailable; 4 | import io.codera.quant.exception.PriceNotAvailableException; 5 | import io.codera.quant.observers.MarketDataObserver; 6 | import java.time.Instant; 7 | import java.util.List; 8 | import java.util.stream.Stream; 9 | import org.lst.trading.lib.model.ClosedOrder; 10 | import org.lst.trading.lib.model.Order; 11 | import org.lst.trading.lib.series.DoubleSeries; 12 | import org.lst.trading.lib.series.TimeSeries; 13 | 14 | /** 15 | * Contains all data needed to run strategy: 16 | * contract prices, balances etc. 17 | */ 18 | public interface TradingContext { 19 | 20 | /** 21 | * Returns the time of current tick 22 | * @return timestamp 23 | */ 24 | default Instant getTime() { 25 | return Instant.now(); 26 | } 27 | 28 | /** 29 | * Returns last price of the contract 30 | * 31 | * @param contract contract name 32 | * @return last price 33 | */ 34 | double getLastPrice(String contract) throws PriceNotAvailableException; 35 | 36 | /** 37 | * Returns history of prices. 38 | * 39 | * @param contract contract name 40 | * @return historical collection of prices 41 | */ 42 | default Stream> getHistory(String contract) { 43 | throw new UnsupportedOperationException(); 44 | } 45 | 46 | /** 47 | * Returns history of prices. 48 | * 49 | * @param contract contract name 50 | * @param numberOfDays seconds of history to return before current time instant 51 | * @return historical collection of prices 52 | */ 53 | default DoubleSeries getHistory(String contract, int numberOfDays) { 54 | throw new UnsupportedOperationException(); 55 | } 56 | 57 | /** 58 | * Returns history of prices. 59 | * 60 | * @param contract contract name 61 | * @param numberOfMinutes seconds of history to return before current time instant 62 | * @return historical collection of prices 63 | */ 64 | default DoubleSeries getHistoryInMinutes(String contract, int numberOfMinutes) { 65 | throw new UnsupportedOperationException(); 66 | } 67 | 68 | /** 69 | * Adds contract into trading contract. 70 | * 71 | * @param contract contract name 72 | */ 73 | void addContract(String contract); 74 | 75 | /** 76 | * Removes contract from context. 77 | * 78 | * @param contract contract name 79 | */ 80 | void removeContract(String contract); 81 | 82 | /** 83 | * returns a collection of current contracts in context. 84 | * 85 | * @return collection of contracts 86 | */ 87 | List getContracts(); 88 | 89 | /** 90 | * returns funds currently available for trading. 91 | * 92 | * @return funds currently available for trading 93 | */ 94 | double getAvailableFunds(); 95 | 96 | /** 97 | * Returns cash balance 98 | * @return cache balance 99 | */ 100 | double getNetValue(); 101 | 102 | /** 103 | * Returns leverage 104 | * 105 | * @return leverage 106 | */ 107 | double getLeverage(); 108 | 109 | /** 110 | * Returns contract observer 111 | * 112 | * @param contractSymbol 113 | * @return 114 | */ 115 | default MarketDataObserver getObserver(String contractSymbol){ 116 | throw new UnsupportedOperationException(); 117 | } 118 | 119 | /** 120 | * Place a contract order 121 | * 122 | * @param contractSymbol contract symbol 123 | * @param buy buy or sell 124 | * @param amount amount 125 | * @return {@link Order} object 126 | */ 127 | Order order(String contractSymbol, boolean buy, int amount) throws PriceNotAvailableException; 128 | 129 | /** 130 | * Close existing order 131 | * @param order order to close 132 | * @return {@link ClosedOrder} object 133 | */ 134 | ClosedOrder close(Order order) throws PriceNotAvailableException; 135 | 136 | /** 137 | * Returns last order of th symbol 138 | * 139 | * @param symbol contract symbol 140 | * @return {@link Order} object 141 | * @throws NoOrderAvailable if no orders available 142 | */ 143 | Order getLastOrderBySymbol(String symbol) throws NoOrderAvailable; 144 | 145 | /** 146 | * Returns symbol change if available 147 | * @param symbol symbol 148 | * @return symbol change since prior day close 149 | */ 150 | default double getChangeBySymbol(String symbol) throws PriceNotAvailableException { 151 | throw new UnsupportedOperationException(); 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/exception/CriterionViolationException.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.exception; 2 | 3 | /** 4 | * 5 | */ 6 | public class CriterionViolationException extends Exception { 7 | 8 | public CriterionViolationException(String msg) { 9 | super(msg); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/exception/NoOrderAvailable.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.exception; 2 | 3 | /** 4 | * Thrown if there are no orders for contract 5 | */ 6 | public class NoOrderAvailable extends Exception { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/exception/NotEnoughDataToCalculateDiffMean.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.exception; 2 | 3 | /** 4 | * Created by beastie on 3/14/17. 5 | */ 6 | public class NotEnoughDataToCalculateDiffMean extends Exception { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/exception/NotEnoughDataToCalculateZScore.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.exception; 2 | 3 | /** 4 | * 5 | */ 6 | public class NotEnoughDataToCalculateZScore extends Exception { 7 | 8 | public NotEnoughDataToCalculateZScore() { 9 | super(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/exception/PriceNotAvailableException.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.exception; 2 | 3 | /** 4 | * Thrown when price is not yet available 5 | */ 6 | public class PriceNotAvailableException extends Exception { 7 | 8 | public PriceNotAvailableException() { 9 | super(); 10 | } 11 | 12 | public PriceNotAvailableException(String msg) { 13 | super(msg); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/observers/AccountObserver.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.observers; 2 | 3 | import com.ib.controller.ApiController.IAccountHandler; 4 | import com.ib.controller.Position; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | /** 9 | * 10 | */ 11 | public interface AccountObserver extends IAccountHandler { 12 | 13 | Logger logger = LoggerFactory.getLogger(AccountObserver.class); 14 | 15 | default void accountValue(String account, String key, String value, String currency) { 16 | if(key.equals("NetLiquidation") && currency.equals("USD")) { 17 | logger.debug(String.format("account: %s, key: %s, value: %s, currency: %s", 18 | account, key, value, currency)); 19 | setNetValue(Double.valueOf(value)); 20 | } 21 | if(key.equals("AvailableFunds") && currency.equals("USD")) { 22 | logger.debug(String.format("account: %s, key: %s, value: %s, currency: %s", 23 | account, key, value, currency)); 24 | setCashBalance(Double.valueOf(value)); 25 | } 26 | } 27 | 28 | default void accountTime(String timeStamp) { 29 | logger.debug(String.format("account time: %s", timeStamp)); 30 | } 31 | 32 | default void accountDownloadEnd(String account) { 33 | logger.debug(String.format("account download end: %s", account)); 34 | } 35 | 36 | @Override 37 | default void updatePortfolio(Position position) { 38 | updateSymbolPosition(position.contract().symbol(), position.position()); 39 | } 40 | 41 | void setCashBalance(double balance); 42 | void setNetValue(double netValue); 43 | void updateSymbolPosition(String symbol, double position); 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/observers/HistoryObserver.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.observers; 2 | 3 | import com.ib.controller.ApiController.IHistoricalDataHandler; 4 | 5 | /** 6 | * 7 | */ 8 | public interface HistoryObserver extends IHistoricalDataHandler { 9 | 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/observers/IbAccountObserver.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.observers; 2 | 3 | import io.codera.quant.context.TradingContext; 4 | import rx.Observable; 5 | import rx.subjects.PublishSubject; 6 | 7 | /** 8 | * 9 | */ 10 | public class IbAccountObserver implements AccountObserver { 11 | 12 | private final PublishSubject cashBalanceSubject; 13 | private final PublishSubject netValueSubject; 14 | private final TradingContext tradingContext; 15 | 16 | public IbAccountObserver(TradingContext tradingContext) { 17 | cashBalanceSubject = PublishSubject.create(); 18 | netValueSubject = PublishSubject.create(); 19 | this.tradingContext = tradingContext; 20 | } 21 | 22 | @Override 23 | public void setCashBalance(double balance) { 24 | cashBalanceSubject.onNext(balance); 25 | } 26 | 27 | @Override 28 | public void setNetValue(double netValue) { 29 | logger.debug("Setting net value"); 30 | netValueSubject.onNext(netValue); 31 | } 32 | 33 | @Override 34 | public void updateSymbolPosition(String symbol, double position) { 35 | logger.info("{} position: {}", symbol, position); 36 | 37 | } 38 | 39 | public Observable observableCashBalance() { 40 | return cashBalanceSubject.asObservable(); 41 | } 42 | 43 | public Observable observableNetValue() { 44 | return netValueSubject.asObservable(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/observers/IbHistoryObserver.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.observers; 2 | 3 | import com.ib.controller.Bar; 4 | import io.codera.quant.config.ContractBuilder; 5 | import java.time.Instant; 6 | import org.joda.time.DateTime; 7 | import org.joda.time.DateTimeZone; 8 | import org.joda.time.LocalDateTime; 9 | import org.lst.trading.lib.series.DoubleSeries; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import rx.Observable; 13 | import rx.subjects.PublishSubject; 14 | 15 | /** 16 | * 17 | */ 18 | public class IbHistoryObserver implements HistoryObserver { 19 | private static final Logger logger = LoggerFactory.getLogger(IbHistoryObserver.class); 20 | private final PublishSubject priceSubject; 21 | private DoubleSeries doubleSeries; 22 | private final String symbol; 23 | 24 | public IbHistoryObserver(String symbol) { 25 | priceSubject = PublishSubject.create(); 26 | this.symbol = symbol; 27 | } 28 | 29 | @Override 30 | public void historicalData(Bar bar, boolean hasGaps) { 31 | if(doubleSeries == null) { 32 | doubleSeries = new DoubleSeries(this.symbol); 33 | } 34 | 35 | DateTime dt = new DateTime(bar.time() * 1000); 36 | 37 | if(dt.minuteOfDay().get() >= 390 && dt.minuteOfDay().get() <= 390 + 390) { 38 | logger.debug("{} {} {}", bar.formattedTime(), symbol, bar.close()); 39 | 40 | doubleSeries.add( 41 | ContractBuilder.getSymbolPrice(symbol, bar.close()), 42 | Instant.ofEpochMilli(new LocalDateTime(bar.time() * 1000).toDateTime(DateTimeZone.UTC) 43 | .getMillis())); 44 | } 45 | 46 | } 47 | 48 | @Override 49 | public void historicalDataEnd() { 50 | priceSubject.onNext(doubleSeries); 51 | logger.debug("End of historic data for " + symbol); 52 | } 53 | 54 | public Observable observableDoubleSeries() { 55 | return priceSubject.asObservable(); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/observers/IbMarketDataObserver.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.observers; 2 | 3 | import com.ib.client.TickType; 4 | import com.ib.client.Types; 5 | import com.ib.controller.ApiController.ITopMktDataHandler; 6 | import io.codera.quant.config.ContractBuilder; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import rx.Observable; 10 | import rx.subjects.PublishSubject; 11 | 12 | /** 13 | * Wraps IB {@link ITopMktDataHandler} into observer to simplify 14 | * access to data (price) feed 15 | */ 16 | public class IbMarketDataObserver implements MarketDataObserver { 17 | 18 | private static final Logger log = LoggerFactory.getLogger(IbMarketDataObserver.class); 19 | 20 | private final String symbol; 21 | private final PublishSubject priceSubject; 22 | 23 | public IbMarketDataObserver(String symbol) { 24 | this.symbol = symbol; 25 | this.priceSubject = PublishSubject.create(); 26 | } 27 | 28 | @Override 29 | public void tickPrice(TickType tickType, double price, int canAutoExecute) { 30 | if(price == -1.0) { // do not update price with bogus value when market is about ot be closed 31 | return; 32 | } 33 | double realPrice = ContractBuilder.getSymbolPrice(symbol, price); 34 | 35 | priceSubject.onNext(new Price(tickType, realPrice)); 36 | 37 | } 38 | 39 | public String getSymbol() { 40 | return symbol; 41 | } 42 | 43 | public Observable priceObservable() { 44 | return priceSubject.asObservable(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/observers/IbOrderObserver.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.observers; 2 | 3 | import com.ib.client.OrderState; 4 | import rx.Observable; 5 | import rx.subjects.PublishSubject; 6 | 7 | /** 8 | * 9 | */ 10 | public class IbOrderObserver implements OrderObserver { 11 | 12 | private final PublishSubject orderSubject; 13 | 14 | public IbOrderObserver() { 15 | orderSubject = PublishSubject.create(); 16 | } 17 | 18 | @Override 19 | public void orderState(OrderState orderState) { 20 | orderSubject.onNext(orderState); 21 | } 22 | 23 | public Observable observableOrderState() { 24 | return orderSubject.asObservable(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/observers/MarketDataObserver.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.observers; 2 | 3 | import com.ib.client.TickType; 4 | import com.ib.client.Types; 5 | import com.ib.controller.ApiController.ITopMktDataHandler; 6 | import rx.Observable; 7 | 8 | /** 9 | * 10 | */ 11 | public interface MarketDataObserver extends ITopMktDataHandler { 12 | 13 | String getSymbol(); 14 | Observable priceObservable(); 15 | 16 | @Override 17 | default void tickSize(TickType tickType, int size) {} 18 | 19 | @Override 20 | default void tickString(TickType tickType, String value) {} 21 | 22 | @Override 23 | default void tickSnapshotEnd() {} 24 | 25 | @Override 26 | default void marketDataType(Types.MktDataType marketDataType) {} 27 | 28 | class Price { 29 | private TickType tickType; 30 | private double price; 31 | Price(TickType tickType, double price) { 32 | this.tickType = tickType; 33 | this.price = price; 34 | } 35 | 36 | public TickType getTickType() { 37 | return tickType; 38 | } 39 | 40 | public void setTickType(TickType tickType) { 41 | this.tickType = tickType; 42 | } 43 | 44 | public double getPrice() { 45 | return price; 46 | } 47 | 48 | public void setPrice(double price) { 49 | this.price = price; 50 | } 51 | 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/observers/OrderObserver.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.observers; 2 | 3 | import com.ib.client.OrderStatus; 4 | import com.ib.controller.ApiController; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | /** 9 | * 10 | */ 11 | public interface OrderObserver extends ApiController.IOrderHandler { 12 | 13 | Logger logger = LoggerFactory.getLogger(OrderObserver.class); 14 | 15 | default void orderStatus(OrderStatus status, double filled, double remaining, double avgFillPrice, 16 | long permId, int parentId, double lastFillPrice, int clientId, String whyHeld) { 17 | logger.info("Order status update: OrderStatus = {}, filled {}, remaining {}, avgFillPrice =" + 18 | " {}, permId = {}, parentId = {}, lastFillPrice = {}, clientId = {}, whyHeld = {}", 19 | status, filled, remaining, avgFillPrice, permId, parentId, lastFillPrice, clientId, 20 | whyHeld); 21 | } 22 | 23 | @Override 24 | default void handle(int errorCode, String errorMsg) { 25 | logger.error("errorCode = {}, errorMsg = {}", errorCode, errorMsg); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/strategy/AbstractStrategy.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.strategy; 2 | 3 | import com.google.common.collect.Lists; 4 | import io.codera.quant.context.TradingContext; 5 | import io.codera.quant.exception.CriterionViolationException; 6 | import java.util.List; 7 | 8 | import static com.google.common.base.Preconditions.checkArgument; 9 | 10 | /** 11 | * Abstract strategy class. 12 | */ 13 | public abstract class AbstractStrategy implements Strategy { 14 | 15 | protected List commonCriteria = Lists.newCopyOnWriteArrayList(); 16 | protected List entryCriteria = Lists.newCopyOnWriteArrayList(); 17 | protected List exitCriteria = Lists.newCopyOnWriteArrayList(); 18 | protected List stopLossCriteria = Lists.newCopyOnWriteArrayList(); 19 | protected List symbols = Lists.newLinkedList(); 20 | protected final TradingContext tradingContext; 21 | 22 | public AbstractStrategy(TradingContext tradingContext) { 23 | this.tradingContext = tradingContext; 24 | } 25 | 26 | @Override 27 | public void addEntryCriterion(Criterion criterion) { 28 | criterion.init(); 29 | entryCriteria.add(criterion); 30 | } 31 | 32 | @Override 33 | public void removeEntryCriterion(Criterion criterion) { 34 | entryCriteria.remove(criterion); 35 | } 36 | 37 | @Override 38 | public void addCommonCriterion(Criterion criterion) { 39 | criterion.init(); 40 | commonCriteria.add(criterion); 41 | } 42 | 43 | @Override 44 | public void removeCommonCriterion(Criterion criterion) { 45 | commonCriteria.remove(criterion); 46 | } 47 | 48 | 49 | @Override 50 | public void addExitCriterion(Criterion criterion) { 51 | criterion.init(); 52 | exitCriteria.add(criterion); 53 | } 54 | 55 | @Override 56 | public void removeExitCriterion(Criterion criterion) { 57 | exitCriteria.remove(criterion); 58 | } 59 | 60 | @Override 61 | public boolean isCommonCriteriaMet() { 62 | return testCriteria(commonCriteria); 63 | } 64 | 65 | @Override 66 | public boolean isEntryCriteriaMet() { 67 | return testCriteria(entryCriteria); 68 | } 69 | 70 | @Override 71 | public boolean isExitCriteriaMet() { 72 | return !exitCriteria.isEmpty() && testCriteria(exitCriteria); 73 | } 74 | 75 | @Override 76 | public boolean isStopLossCriteriaMet() { 77 | return !stopLossCriteria.isEmpty() && testCriteria(stopLossCriteria); 78 | } 79 | 80 | @Override 81 | public void addStopLossCriterion(Criterion criterion) { 82 | checkArgument(criterion != null, "criterion is null"); 83 | 84 | stopLossCriteria.add(criterion); 85 | } 86 | 87 | @Override 88 | public BackTestResult getBackTestResult() { 89 | throw new UnsupportedOperationException(); 90 | } 91 | 92 | @Override 93 | public void addSymbol(String symbol) { 94 | symbols.add(symbol); 95 | tradingContext.addContract(symbol); 96 | } 97 | 98 | @Override 99 | public TradingContext getTradingContext() { 100 | return tradingContext; 101 | } 102 | 103 | private boolean testCriteria(List criteria) { 104 | if(criteria.size() == 0) { 105 | return true; 106 | } 107 | 108 | for(Criterion criterion: criteria) { 109 | try { 110 | if(!criterion.isMet()) { 111 | log.debug("{} criterion was NOT met", criterion.getClass().getName()); 112 | return false; 113 | } 114 | log.debug("{} criterion was met", criterion.getClass().getName()); 115 | } catch (CriterionViolationException e) { 116 | log.debug("{} criterion was NOT met", criterion.getClass().getName()); 117 | return false; 118 | } 119 | } 120 | return true; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/strategy/BackTestResult.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.strategy; 2 | 3 | /** 4 | * Represents additional data collected specifically for backtesting. 5 | */ 6 | public interface BackTestResult { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/strategy/Criterion.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.strategy; 2 | 3 | import io.codera.quant.exception.CriterionViolationException; 4 | 5 | /** 6 | * Criterion interface. 7 | */ 8 | public interface Criterion { 9 | 10 | /** 11 | * To be run if criterion needs tobe initialized .E.g. to get the historical or other data needed 12 | * for calculation. 13 | */ 14 | default void init(){} 15 | 16 | /** 17 | * Check if criterion has been met 18 | * @return true if criteria is met, false otherwise 19 | */ 20 | boolean isMet() throws CriterionViolationException; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/strategy/IbPerMinuteStrategyRunner.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.strategy; 2 | 3 | import java.util.Date; 4 | import java.util.List; 5 | import java.util.Timer; 6 | import java.util.TimerTask; 7 | import org.joda.time.DateTime; 8 | 9 | /** 10 | * Strategy runner that executes every minute 11 | */ 12 | public class IbPerMinuteStrategyRunner implements StrategyRunner { 13 | 14 | @Override 15 | public void run(Strategy strategy, List symbols) { 16 | 17 | for(String symbol : symbols) { 18 | strategy.addSymbol(symbol); 19 | } 20 | 21 | Timer timer = new Timer(true); 22 | DateTime dt = new DateTime(); 23 | 24 | timer.schedule( 25 | new TriggerTick(strategy), 26 | new Date((dt.getMillis() - (dt.getSecondOfMinute() * 1000)) + 59000), 27 | 60000); 28 | 29 | } 30 | 31 | @Override 32 | public void stop(Strategy strategy, List symbols) { 33 | 34 | } 35 | 36 | private class TriggerTick extends TimerTask { 37 | 38 | private final Strategy strategy; 39 | 40 | TriggerTick(Strategy strategy) { 41 | this.strategy = strategy; 42 | } 43 | 44 | @Override 45 | public void run() { 46 | strategy.onTick(); 47 | } 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/strategy/Strategy.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.strategy; 2 | 3 | import io.codera.quant.context.TradingContext; 4 | import io.codera.quant.exception.PriceNotAvailableException; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | /** 9 | * Strategy interface. 10 | */ 11 | public interface Strategy { 12 | 13 | Logger log = LoggerFactory.getLogger(Strategy.class); 14 | 15 | /** 16 | * Executed every time when new data is received. 17 | * Drives placing trades based on loaded entry and exit criteria and lot sizes. 18 | * This method will also update {@link BackTestResult object if run in backtest mode}. 19 | */ 20 | default void onTick() { 21 | 22 | if(isCommonCriteriaMet()) { 23 | if(isEntryCriteriaMet()) { 24 | try { 25 | openPosition(); 26 | } catch (PriceNotAvailableException e) { 27 | log.error("Price for requested contract is not available"); 28 | } 29 | } else if(isStopLossCriteriaMet()) { 30 | try { 31 | closePosition(); 32 | } catch (PriceNotAvailableException e) { 33 | log.error("Price for requested contract is not available"); 34 | } 35 | } else if(isExitCriteriaMet()) { 36 | try { 37 | closePosition(); 38 | } catch (PriceNotAvailableException e) { 39 | log.error("Price for requested contract is not available"); 40 | } 41 | } 42 | } 43 | } 44 | 45 | /** 46 | * Calculates the lot size based on configured trade context and strategy logic. 47 | * @param contract instrument/contract name 48 | * @param buy true of buy, false if sell 49 | * @return size of the lot 50 | */ 51 | int getLotSize(String contract, boolean buy); 52 | 53 | /** 54 | * Checks if common criterion is met for current tick. 55 | * @return true if met, false otherwise 56 | */ 57 | boolean isCommonCriteriaMet(); 58 | 59 | /** 60 | * Checks if entry criterion is met for current tick. 61 | * @return true if met, false otherwise 62 | */ 63 | boolean isEntryCriteriaMet(); 64 | 65 | /** 66 | * Checks if exit criterion is met for current tick. 67 | * @return true if met, false otherwise 68 | */ 69 | boolean isExitCriteriaMet(); 70 | 71 | /** 72 | * Checks if stop loss criterion is met for current tick. 73 | * @return true if met, false otherwise 74 | */ 75 | default boolean isStopLossCriteriaMet() { 76 | return false; 77 | } 78 | 79 | /** 80 | * Adds stop loss criterion. 81 | * @param criterion common criterion 82 | */ 83 | default void addStopLossCriterion(Criterion criterion) {} 84 | 85 | /** 86 | * Adds common criterion. 87 | * @param criterion common criterion 88 | */ 89 | void addCommonCriterion(Criterion criterion); 90 | 91 | /** 92 | * Adds entry criterion. 93 | * @param criterion entry criterion 94 | */ 95 | void addEntryCriterion(Criterion criterion); 96 | 97 | /** 98 | * Removes common criterion. 99 | * @param criterion common criterion 100 | */ 101 | void removeCommonCriterion(Criterion criterion); 102 | 103 | /** 104 | * Removes entry criterion. 105 | * @param criterion entry criterion 106 | */ 107 | void removeEntryCriterion(Criterion criterion); 108 | 109 | /** 110 | * Adds exit criterion. 111 | * @param criterion exit criterion 112 | */ 113 | void addExitCriterion(Criterion criterion); 114 | 115 | /** 116 | * Remove exit criterion. 117 | * @param criterion exit criterion 118 | */ 119 | void removeExitCriterion(Criterion criterion); 120 | 121 | /** 122 | * Returns additional data needed for back testing. 123 | * @return {@link BackTestResult} object 124 | */ 125 | BackTestResult getBackTestResult(); 126 | 127 | /** 128 | * Add symbol to run strategy against. 129 | * @param symbol contract symbol 130 | */ 131 | void addSymbol(String symbol); 132 | 133 | /** 134 | * Returns strategy {@link TradingContext} 135 | * @return 136 | */ 137 | TradingContext getTradingContext(); 138 | 139 | /** 140 | * Opens position in one or several contracts when entry {@link Criterion} is met. 141 | */ 142 | void openPosition() throws PriceNotAvailableException; 143 | 144 | /** 145 | * Closes position for contract when exit {@link Criterion} is met. 146 | */ 147 | void closePosition() throws PriceNotAvailableException; 148 | 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/strategy/StrategyRunner.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.strategy; 2 | 3 | import io.codera.quant.context.TradingContext; 4 | import java.util.List; 5 | 6 | /** 7 | * Runs strategy in defined trading context. 8 | */ 9 | public interface StrategyRunner { 10 | 11 | /** 12 | * Run specified {@link Strategy} for symbols collection in given {@link TradingContext}. 13 | * 14 | * @param strategy strategy to run 15 | * @param symbols list 16 | */ 17 | void run(Strategy strategy, List symbols); 18 | 19 | /** 20 | * Stop the specified strategy for specified symbols. 21 | * 22 | * @param strategy strategy to stop 23 | * @param symbols symbols list 24 | */ 25 | void stop(Strategy strategy, List symbols); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/strategy/criterion/NoOpenOrdersExistEntryCriterion.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.strategy.criterion; 2 | 3 | import io.codera.quant.context.TradingContext; 4 | import io.codera.quant.exception.CriterionViolationException; 5 | import io.codera.quant.exception.NoOrderAvailable; 6 | import io.codera.quant.strategy.Criterion; 7 | import java.util.List; 8 | import org.lst.trading.lib.model.Order; 9 | 10 | 11 | /** 12 | * Checks that no open orders available for specified symbols 13 | */ 14 | public class NoOpenOrdersExistEntryCriterion implements Criterion { 15 | 16 | protected final List symbols; 17 | protected final TradingContext tradingContext; 18 | 19 | public NoOpenOrdersExistEntryCriterion(TradingContext tradingContext, List symbols) { 20 | this.tradingContext = tradingContext; 21 | this.symbols = symbols; 22 | } 23 | 24 | @Override 25 | public boolean isMet() throws CriterionViolationException { 26 | for(String symbol : symbols) { 27 | try { 28 | Order order = tradingContext.getLastOrderBySymbol(symbol); 29 | if(order != null) { 30 | return false; 31 | } 32 | } catch (NoOrderAvailable ignored) { 33 | } 34 | } 35 | return true; 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/strategy/criterion/OpenIbOrdersExistForAllSymbolsExitCriterion.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.strategy.criterion; 2 | 3 | import com.ib.client.OrderStatus; 4 | import io.codera.quant.context.TradingContext; 5 | import io.codera.quant.exception.CriterionViolationException; 6 | import io.codera.quant.exception.NoOrderAvailable; 7 | import io.codera.quant.strategy.Criterion; 8 | import java.util.List; 9 | import org.lst.trading.lib.model.Order; 10 | 11 | /** 12 | * Created by beastie on 1/23/17. 13 | */ 14 | public class OpenIbOrdersExistForAllSymbolsExitCriterion implements Criterion { 15 | 16 | protected final List symbols; 17 | protected final TradingContext tradingContext; 18 | 19 | public OpenIbOrdersExistForAllSymbolsExitCriterion(TradingContext tradingContext, 20 | List symbols) { 21 | this.tradingContext = tradingContext; 22 | this.symbols = symbols; 23 | } 24 | 25 | @Override 26 | public boolean isMet() throws CriterionViolationException { 27 | for(String symbol : symbols) { 28 | try { 29 | Order order = tradingContext.getLastOrderBySymbol(symbol); 30 | if(order.getOrderStatus() != OrderStatus.Filled) { 31 | return false; 32 | } 33 | } catch (NoOrderAvailable e) { 34 | return false; 35 | } 36 | } 37 | return true; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/strategy/criterion/OpenOrdersExistForAllSymbolsExitCriterion.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.strategy.criterion; 2 | 3 | import io.codera.quant.context.TradingContext; 4 | import io.codera.quant.exception.CriterionViolationException; 5 | import io.codera.quant.exception.NoOrderAvailable; 6 | import java.util.List; 7 | 8 | /** 9 | * All orders exist for specified 10 | */ 11 | public class OpenOrdersExistForAllSymbolsExitCriterion extends NoOpenOrdersExistEntryCriterion { 12 | public OpenOrdersExistForAllSymbolsExitCriterion(TradingContext tradingContext, 13 | List symbols) { 14 | super(tradingContext, symbols); 15 | } 16 | 17 | @Override 18 | public boolean isMet() throws CriterionViolationException { 19 | for(String symbol : symbols) { 20 | try { 21 | tradingContext.getLastOrderBySymbol(symbol); 22 | 23 | } catch (NoOrderAvailable e) { 24 | return false; 25 | } 26 | } 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/strategy/criterion/common/NoPendingOrdersCommonCriterion.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.strategy.criterion.common; 2 | 3 | 4 | import com.ib.client.OrderStatus; 5 | import io.codera.quant.context.TradingContext; 6 | import io.codera.quant.exception.CriterionViolationException; 7 | import io.codera.quant.exception.NoOrderAvailable; 8 | import io.codera.quant.strategy.Criterion; 9 | import java.util.List; 10 | 11 | /** 12 | * Test if there are any orders that are in pending (not filled yet) state 13 | */ 14 | public class NoPendingOrdersCommonCriterion implements Criterion { 15 | 16 | private final TradingContext tradingContext; 17 | private final List symbols; 18 | 19 | public NoPendingOrdersCommonCriterion(TradingContext tradingContext, List symbols) { 20 | this.tradingContext = tradingContext; 21 | this.symbols = symbols; 22 | } 23 | 24 | @Override 25 | public boolean isMet() throws CriterionViolationException { 26 | for(String symbol : symbols) { 27 | try { 28 | if(tradingContext.getLastOrderBySymbol(symbol) != null 29 | && tradingContext.getLastOrderBySymbol(symbol).getOrderStatus() != OrderStatus.Filled) { 30 | return false; 31 | } 32 | } catch (NoOrderAvailable noOrderAvailable) {} // Do nothing here as there is not order 33 | } 34 | return true; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/strategy/criterion/stoploss/DefaultStopLossCriterion.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.strategy.criterion.stoploss; 2 | 3 | import io.codera.quant.context.TradingContext; 4 | import io.codera.quant.exception.CriterionViolationException; 5 | import io.codera.quant.exception.NoOrderAvailable; 6 | import io.codera.quant.exception.PriceNotAvailableException; 7 | import io.codera.quant.strategy.Criterion; 8 | import java.util.List; 9 | import org.lst.trading.lib.model.Order; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | /** 14 | * 15 | */ 16 | public class DefaultStopLossCriterion implements Criterion { 17 | 18 | private final double thresholdAmount; 19 | private final TradingContext tradingContext; 20 | private final List symbols; 21 | private static final Logger log = LoggerFactory.getLogger(DefaultStopLossCriterion.class); 22 | 23 | public DefaultStopLossCriterion(List symbols, double thresholdAmount, 24 | TradingContext tradingContext) { 25 | this.tradingContext = tradingContext; 26 | this.thresholdAmount = thresholdAmount; 27 | this.symbols = symbols; 28 | } 29 | 30 | 31 | @Override 32 | public boolean isMet() throws CriterionViolationException { 33 | // check if there are open orders 34 | double totalPl = 0.0; 35 | for(String symbol : symbols) { 36 | try { 37 | Order order = tradingContext.getLastOrderBySymbol(symbol); 38 | double symbolPl = (tradingContext.getLastPrice(symbol) * order.getAmount()) 39 | + (order.getOpenPrice() * -order.getAmount()); 40 | log.debug("Symbol P/L: {}", symbolPl); 41 | totalPl += symbolPl; 42 | } catch (NoOrderAvailable | PriceNotAvailableException noOrderAvailable) { 43 | return false; 44 | } 45 | } 46 | log.debug("Total PL: {}", totalPl); 47 | return totalPl <= thresholdAmount; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/strategy/kalman/KalmanFilterStrategy.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.strategy.kalman; 2 | 3 | import io.codera.quant.config.ContractBuilder; 4 | import io.codera.quant.context.IbTradingContext; 5 | import io.codera.quant.context.TradingContext; 6 | import io.codera.quant.exception.CriterionViolationException; 7 | import io.codera.quant.exception.NoOrderAvailable; 8 | import io.codera.quant.exception.PriceNotAvailableException; 9 | import io.codera.quant.strategy.AbstractStrategy; 10 | import io.codera.quant.strategy.Criterion; 11 | import java.util.List; 12 | import java.util.Queue; 13 | import java.util.concurrent.ConcurrentLinkedQueue; 14 | import org.apache.commons.math3.stat.StatUtils; 15 | import org.lst.trading.lib.series.DoubleSeries; 16 | import org.lst.trading.lib.series.MultipleDoubleSeries; 17 | import org.lst.trading.lib.series.TimeSeries; 18 | import org.lst.trading.main.strategy.kalman.Cointegration; 19 | 20 | /** 21 | * Kalman filter strategy 22 | */ 23 | public class KalmanFilterStrategy extends AbstractStrategy { 24 | 25 | private final String firstSymbol; 26 | private final String secondSymbol; 27 | private final Cointegration cointegration; 28 | private double beta; 29 | private double baseAmount; 30 | private double sd; 31 | private ErrorIsMoreThanStandardDeviationEntry mainEntry; 32 | private KalmanFilterExitCriterion mainExit; 33 | 34 | public KalmanFilterStrategy(String firstSymbol, String secondSymbol, 35 | TradingContext tradingContext, Cointegration cointegration) { 36 | super(tradingContext); 37 | this.firstSymbol = firstSymbol; 38 | this.secondSymbol = secondSymbol; 39 | this.cointegration = cointegration; 40 | this.mainEntry = new ErrorIsMoreThanStandardDeviationEntry(); 41 | this.mainExit = new KalmanFilterExitCriterion(); 42 | addEntryCriterion(mainEntry); 43 | addExitCriterion(mainExit); 44 | } 45 | 46 | public void setErrorQueueSize(int size) { 47 | mainEntry.setErrorQueueSize(size); 48 | } 49 | 50 | public void setEntrySdMultiplier(double multiplier) { 51 | mainEntry.setSdMultiplier(multiplier); 52 | } 53 | 54 | public void setExitMultiplier(double multiplier) { 55 | mainExit.setSdMultiplier(multiplier); 56 | } 57 | 58 | public void openPosition() throws PriceNotAvailableException { 59 | 60 | tradingContext.order(secondSymbol, cointegration.getError() < 0, (int) baseAmount); 61 | log.debug("Order of {} in amount {}", secondSymbol, (int)baseAmount); 62 | 63 | tradingContext.order(firstSymbol, cointegration.getError() > 0, (int) (baseAmount * beta)); 64 | log.debug("Order of {} in amount {}", firstSymbol, (int) (baseAmount * beta)); 65 | } 66 | 67 | public void closePosition() throws PriceNotAvailableException { 68 | try { 69 | tradingContext.close(tradingContext.getLastOrderBySymbol(firstSymbol)); 70 | } catch (NoOrderAvailable noOrderAvailable) { 71 | log.error("No order available for {}", firstSymbol); 72 | } 73 | try { 74 | tradingContext.close(tradingContext.getLastOrderBySymbol(secondSymbol)); 75 | } catch (NoOrderAvailable noOrderAvailable) { 76 | log.error("No order available for {}", secondSymbol); 77 | } 78 | } 79 | 80 | @Override 81 | public int getLotSize(String contract, boolean buy) { 82 | throw new UnsupportedOperationException(); 83 | } 84 | 85 | public class ErrorIsMoreThanStandardDeviationEntry implements Criterion { 86 | 87 | private Queue errorQueue = new ConcurrentLinkedQueue<>(); 88 | private static final int ERROR_QUEUE_SIZE_DEFAULT = 30; 89 | 90 | public void setSdMultiplier(double sdMultiplier) { 91 | this.sdMultiplier = sdMultiplier; 92 | } 93 | 94 | private double sdMultiplier; 95 | 96 | public void setErrorQueueSize(int errorQueueSize) { 97 | this.errorQueueSize = errorQueueSize; 98 | } 99 | 100 | private int errorQueueSize; 101 | 102 | 103 | public ErrorIsMoreThanStandardDeviationEntry() { 104 | errorQueueSize = ERROR_QUEUE_SIZE_DEFAULT; 105 | sdMultiplier = 1; 106 | } 107 | 108 | @Override 109 | public void init() { 110 | if(tradingContext instanceof IbTradingContext) { 111 | 112 | DoubleSeries firstSymbolHistory = 113 | tradingContext.getHistory(firstSymbol, 2); 114 | 115 | DoubleSeries secondSymbolHistory = 116 | tradingContext.getHistory(secondSymbol, 2); 117 | 118 | MultipleDoubleSeries multipleDoubleSeries = new MultipleDoubleSeries(firstSymbolHistory, 119 | secondSymbolHistory); 120 | 121 | for (TimeSeries.Entry> entry : multipleDoubleSeries) { 122 | // TODO (dsinyakov): remove cointegration logic duplication below 123 | 124 | double x = entry.getItem().get(0); 125 | double y = entry.getItem().get(1); 126 | if(firstSymbol.contains("=F") && secondSymbol.contains("=F")) { 127 | x = x * ContractBuilder.getFutureMultiplier(firstSymbol); 128 | y = y * ContractBuilder.getFutureMultiplier(secondSymbol); 129 | } 130 | cointegration.step(x, y); 131 | 132 | double error = cointegration.getError(); 133 | errorQueue.add(error); 134 | if (errorQueue.size() > errorQueueSize + 1) { 135 | errorQueue.poll(); 136 | } 137 | } 138 | } 139 | } 140 | 141 | @Override 142 | public boolean isMet() throws CriterionViolationException { 143 | log.debug("Evaluating ErrorIsMoreThanStandardDeviationEntry criteria"); 144 | double x; 145 | try { 146 | x = tradingContext.getLastPrice(firstSymbol); 147 | log.info("Current {} price {}", firstSymbol, x); 148 | 149 | } catch (PriceNotAvailableException e) { 150 | log.error("Price for " + firstSymbol + " is not available."); 151 | return false; 152 | } 153 | double y; 154 | try { 155 | y = tradingContext.getLastPrice(secondSymbol); 156 | log.info("Current {} price {}", secondSymbol, y); 157 | } catch (PriceNotAvailableException e) { 158 | log.error("Price for " + secondSymbol + " is not available."); 159 | return false; 160 | } 161 | beta = cointegration.getBeta(); 162 | 163 | if(firstSymbol.contains("=F") && secondSymbol.contains("=F")) { 164 | x = x * ContractBuilder.getFutureMultiplier(firstSymbol); 165 | y = y * ContractBuilder.getFutureMultiplier(secondSymbol); 166 | } 167 | cointegration.step(x, y); 168 | 169 | double error = cointegration.getError(); 170 | errorQueue.add(error); 171 | log.debug("Error Queue size: {}", errorQueue.size()); 172 | if (errorQueue.size() > errorQueueSize + 1) { 173 | errorQueue.poll(); 174 | } 175 | 176 | if (errorQueue.size() > errorQueueSize) { 177 | log.debug("Kalman filter queue is > " + errorQueueSize); 178 | Object[] errors = errorQueue.toArray(); 179 | double[] lastValues = new double[errorQueueSize/2]; 180 | 181 | for (int i = errors.length - 1, lastValIndex = 0; 182 | i > errors.length - 1 - errorQueueSize/2; 183 | i--, lastValIndex++) { 184 | lastValues[lastValIndex] = Double.valueOf(errors[i].toString()); 185 | } 186 | 187 | sd = Math.sqrt(StatUtils.variance(lastValues)); 188 | double realSd = sdMultiplier * sd; 189 | log.info("error={}, sd={}", error, realSd); 190 | if (Math.abs(error) > realSd) { 191 | log.debug("error is bigger than square root of standard deviation"); 192 | log.debug("Net value {}", tradingContext.getNetValue()); 193 | if(secondSymbol.contains("=F")) { 194 | //Exchange Underlying Product description Trading Class Intraday Initial 1 Intraday Maintenance 1 Overnight Initial Overnight Maintenance Currency Has Options 195 | //GLOBEX ES E-mini S&P 500 ES 3665 2932 7330 5864 USD 196 | // Yes 197 | //ECBOT YM Mini Sized Dow Jones Industrial Average $5 YM 3218.125 2574.50 6436.25 5149 USD Yes 198 | 199 | baseAmount = 4; 200 | beta = 1; 201 | } else { 202 | baseAmount = 203 | (tradingContext.getNetValue() * 0.5 * Math.min(4, tradingContext.getLeverage())) 204 | / (y + beta * x); 205 | log.debug("baseAmount={}, sd={}, beta={}", baseAmount, cointegration.getError(), beta); 206 | 207 | } 208 | if (beta > 0 && baseAmount * beta >= 1) { 209 | log.info("error={}, sd={}", error, realSd); 210 | log.info("{} price {}; {} price {}", firstSymbol, x, secondSymbol, y); 211 | return true; 212 | 213 | } 214 | 215 | } 216 | } 217 | return false; 218 | } 219 | } 220 | 221 | class KalmanFilterExitCriterion implements Criterion { 222 | 223 | private double sdMultiplier; 224 | 225 | public void setSdMultiplier(double sdMultiplier) { 226 | this.sdMultiplier = sdMultiplier; 227 | } 228 | 229 | @Override 230 | public boolean isMet() throws CriterionViolationException { 231 | 232 | log.debug("Evaluating KalmanFilterExitCriterion criteria"); 233 | try { 234 | if(tradingContext.getLastOrderBySymbol(secondSymbol).isLong() && 235 | cointegration.getError() > sdMultiplier * sd || 236 | tradingContext.getLastOrderBySymbol(secondSymbol).isShort() && 237 | cointegration.getError() < -sdMultiplier * sd ) { 238 | log.info("error={}, sd={}", cointegration.getError(), sd); 239 | log.info("{} price {}; {} price {}", firstSymbol, tradingContext.getLastPrice(firstSymbol), 240 | secondSymbol, tradingContext.getLastPrice(secondSymbol)); 241 | return true; 242 | } 243 | 244 | } catch (NoOrderAvailable noOrderAvailable) { 245 | log.debug("No orders available for " + secondSymbol); 246 | return false; 247 | } catch (PriceNotAvailableException e) { 248 | log.debug("No price available for some symbol"); 249 | return false; 250 | } 251 | return false; 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/strategy/meanrevertion/BollingerBandsStrategy.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.strategy.meanrevertion; 2 | 3 | import io.codera.quant.context.TradingContext; 4 | import io.codera.quant.exception.NoOrderAvailable; 5 | import io.codera.quant.exception.PriceNotAvailableException; 6 | import io.codera.quant.strategy.AbstractStrategy; 7 | 8 | /** 9 | * Bollinger bands strategy 10 | */ 11 | public class BollingerBandsStrategy extends AbstractStrategy { 12 | 13 | private final String firstSymbol; 14 | private final String secondSymbol; 15 | private final ZScore zScore; 16 | 17 | public BollingerBandsStrategy(String firstSymbol, String secondSymbol, 18 | TradingContext tradingContext, ZScore zScore) { 19 | 20 | super(tradingContext); 21 | this.firstSymbol = firstSymbol; 22 | this.secondSymbol = secondSymbol; 23 | this.zScore = zScore; 24 | } 25 | 26 | @Override 27 | public int getLotSize(String contract, boolean buy) { 28 | return 0; 29 | } 30 | 31 | @Override 32 | public void openPosition() throws PriceNotAvailableException { 33 | double hedgeRatio = Math.abs(zScore.getHedgeRatio()); 34 | 35 | double baseAmount = 36 | (tradingContext.getNetValue() * 0.5 * Math.min(4, tradingContext.getLeverage())) 37 | / (tradingContext.getLastPrice(secondSymbol) + hedgeRatio * tradingContext.getLastPrice 38 | (firstSymbol)); 39 | 40 | tradingContext.order(firstSymbol, zScore.getLastCalculatedZScore() < 0, 41 | (int) (baseAmount * hedgeRatio) > 1 ? (int) (baseAmount * hedgeRatio) : 1); 42 | log.debug("Order of {} in amount {}", firstSymbol, (int) (baseAmount * hedgeRatio)); 43 | 44 | tradingContext.order(secondSymbol, zScore.getLastCalculatedZScore() > 0, (int) baseAmount); 45 | log.debug("Order of {} in amount {}", secondSymbol, (int) baseAmount); 46 | } 47 | 48 | @Override 49 | public void closePosition() throws PriceNotAvailableException { 50 | try { 51 | tradingContext.close(tradingContext.getLastOrderBySymbol(firstSymbol)); 52 | } catch (NoOrderAvailable noOrderAvailable) { 53 | log.error("No order available for {}", firstSymbol); 54 | } 55 | try { 56 | tradingContext.close(tradingContext.getLastOrderBySymbol(secondSymbol)); 57 | } catch (NoOrderAvailable noOrderAvailable) { 58 | log.error("No order available for {}", secondSymbol); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/strategy/meanrevertion/ZScore.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.strategy.meanrevertion; 2 | 3 | import com.google.common.util.concurrent.AtomicDouble; 4 | import io.codera.quant.util.MathUtil; 5 | import org.apache.commons.math3.linear.MatrixUtils; 6 | import org.apache.commons.math3.linear.RealMatrix; 7 | import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; 8 | import org.apache.commons.math3.stat.regression.OLSMultipleLinearRegression; 9 | 10 | import static com.google.common.base.Preconditions.checkArgument; 11 | 12 | /** 13 | * 14 | */ 15 | public class ZScore { 16 | 17 | private double[] firstSymbolHistory; 18 | private double[] secondSymbolHistory; 19 | private int lookback; 20 | private int historyArraySize; 21 | private MathUtil u; 22 | 23 | private double[] x; 24 | private double[] y; 25 | private double[] yPort; 26 | 27 | private AtomicDouble lastCalculatedZScore; 28 | private AtomicDouble lastCalculatedHedgeRatio; 29 | 30 | private int historyIndex = 0; 31 | 32 | public ZScore(double[] firstSymbolHistory, double[] secondSymbolHistory, int lookback, 33 | MathUtil utils) { 34 | checkArgument(firstSymbolHistory.length == lookback * 2 - 1, "firstSymbolHistory should be of" + 35 | " " + (lookback * 2 - 1) + " size"); 36 | checkArgument(secondSymbolHistory.length == lookback * 2 - 1, "secondHistory should be of " + 37 | (lookback * 2 - 1) + " size"); 38 | 39 | this.firstSymbolHistory = firstSymbolHistory; 40 | this.secondSymbolHistory = secondSymbolHistory; 41 | this.lookback = lookback; 42 | this.u = utils; 43 | } 44 | 45 | public ZScore(int lookback, 46 | MathUtil utils) { 47 | this.lookback = lookback; 48 | this.historyArraySize = (lookback * 2 - 1); 49 | this.u = utils; 50 | firstSymbolHistory = new double[historyArraySize]; 51 | secondSymbolHistory = new double[historyArraySize]; 52 | } 53 | 54 | public double get(double firstSymbolPrice, double secondSymbolPrice) { 55 | checkArgument(firstSymbolPrice > 0, "firstSymbolPrice can not be <= 0"); 56 | checkArgument(secondSymbolPrice > 0, "secondSymbolPrice can not be <= 0"); 57 | 58 | if(firstSymbolHistory[firstSymbolHistory.length - 1] == 0 && 59 | secondSymbolHistory[secondSymbolHistory.length - 1] == 0) { 60 | firstSymbolHistory[historyIndex] = firstSymbolPrice; 61 | secondSymbolHistory[historyIndex] = secondSymbolPrice; 62 | historyIndex++; 63 | return 0.0; 64 | } 65 | 66 | if(x == null && y == null && yPort == null) { 67 | x = new double[lookback]; 68 | y = new double[lookback]; 69 | yPort = new double[lookback]; 70 | 71 | System.arraycopy(firstSymbolHistory, 0, x, 0, lookback - 1); 72 | System.arraycopy(secondSymbolHistory, 0, y, 0, lookback - 1); 73 | 74 | for(int i = lookback - 1; i < lookback * 2 - 1; i++) { 75 | x[lookback - 1] = firstSymbolHistory[i]; 76 | y[lookback - 1] = secondSymbolHistory[i]; 77 | 78 | RealMatrix xMatrix = MatrixUtils.createRealMatrix(lookback, 2); 79 | xMatrix.setColumn(0, x); 80 | xMatrix.setColumn(1, u.ones(lookback)); 81 | 82 | OLSMultipleLinearRegression ols = new OLSMultipleLinearRegression(0); 83 | ols.setNoIntercept(true); 84 | ols.newSampleData(y, xMatrix.getData()); 85 | 86 | double hedgeRatio = ols.estimateRegressionParameters()[0]; 87 | 88 | double yP = (-hedgeRatio * x[lookback - 1]) + y[lookback - 1]; 89 | yPort[i + 1 - yPort.length] = yP; 90 | 91 | System.arraycopy(x, 1, x, 0, lookback - 1); 92 | System.arraycopy(y, 1, y, 0, lookback - 1); 93 | 94 | } 95 | 96 | } 97 | 98 | System.arraycopy(yPort, 1, yPort, 0, lookback - 1); 99 | x[lookback - 1] = firstSymbolPrice; 100 | y[lookback - 1] = secondSymbolPrice; 101 | 102 | RealMatrix xMatrix = MatrixUtils.createRealMatrix(lookback, 2); 103 | xMatrix.setColumn(0, x); 104 | xMatrix.setColumn(1, u.ones(lookback)); 105 | 106 | OLSMultipleLinearRegression ols = new OLSMultipleLinearRegression(0); 107 | ols.setNoIntercept(true); 108 | ols.newSampleData(y, xMatrix.getData()); 109 | 110 | double hedgeRatio = ols.estimateRegressionParameters()[0]; 111 | 112 | if(lastCalculatedHedgeRatio == null) { 113 | lastCalculatedHedgeRatio = new AtomicDouble(); 114 | } 115 | 116 | lastCalculatedHedgeRatio.set(hedgeRatio); 117 | 118 | double yP = (-hedgeRatio * firstSymbolPrice) + secondSymbolPrice; 119 | yPort[lookback - 1] = yP; 120 | 121 | DescriptiveStatistics ds = new DescriptiveStatistics(yPort); 122 | 123 | double movingAverage = ds.getMean(); 124 | double standardDeviation = ds.getStandardDeviation(); 125 | 126 | System.arraycopy(x, 1, x, 0, lookback - 1); 127 | System.arraycopy(y, 1, y, 0, lookback - 1); 128 | 129 | double zScore = (yPort[lookback - 1] - movingAverage)/standardDeviation; 130 | 131 | if(lastCalculatedZScore == null) { 132 | lastCalculatedZScore = new AtomicDouble(); 133 | } 134 | 135 | lastCalculatedZScore.set(zScore); 136 | return lastCalculatedZScore.get(); 137 | } 138 | 139 | public double getHedgeRatio() { 140 | return lastCalculatedHedgeRatio.get(); 141 | } 142 | 143 | public double getLastCalculatedZScore() { 144 | return lastCalculatedZScore.get(); 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/strategy/meanrevertion/ZScoreEntryCriterion.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.strategy.meanrevertion; 2 | 3 | import io.codera.quant.context.TradingContext; 4 | import io.codera.quant.exception.CriterionViolationException; 5 | import io.codera.quant.exception.PriceNotAvailableException; 6 | import io.codera.quant.strategy.Criterion; 7 | 8 | /** 9 | * 10 | */ 11 | public class ZScoreEntryCriterion implements Criterion { 12 | 13 | private final String firstSymbol; 14 | private final String secondSymbol; 15 | private ZScore zScore; 16 | private TradingContext tradingContext; 17 | private final double entryZScore; 18 | 19 | public ZScoreEntryCriterion(String firstSymbol, String secondSymbol, 20 | double entryZScore, ZScore zScore, TradingContext tradingContext) { 21 | this.zScore = zScore; 22 | this.firstSymbol = firstSymbol; 23 | this.secondSymbol = secondSymbol; 24 | this.tradingContext = tradingContext; 25 | this.entryZScore = entryZScore; 26 | } 27 | 28 | @Override 29 | public boolean isMet() throws CriterionViolationException { 30 | try { 31 | double zs = 32 | zScore.get( 33 | tradingContext.getLastPrice(firstSymbol), tradingContext.getLastPrice(secondSymbol)); 34 | if(zs < -entryZScore || zs > entryZScore) { 35 | return true; 36 | } 37 | } catch (PriceNotAvailableException e) { 38 | return false; 39 | } 40 | return false; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/strategy/meanrevertion/ZScoreExitCriterion.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.strategy.meanrevertion; 2 | 3 | import io.codera.quant.context.TradingContext; 4 | import io.codera.quant.exception.CriterionViolationException; 5 | import io.codera.quant.exception.NoOrderAvailable; 6 | import io.codera.quant.exception.PriceNotAvailableException; 7 | import io.codera.quant.strategy.Criterion; 8 | 9 | /** 10 | * 11 | */ 12 | public class ZScoreExitCriterion implements Criterion { 13 | 14 | private final String firstSymbol; 15 | private final String secondSymbol; 16 | private ZScore zScore; 17 | private TradingContext tradingContext; 18 | private double exitZScore; 19 | 20 | public ZScoreExitCriterion(String firstSymbol, String secondSymbol, 21 | ZScore zScore, TradingContext tradingContext) { 22 | this.zScore = zScore; 23 | this.firstSymbol = firstSymbol; 24 | this.secondSymbol = secondSymbol; 25 | this.tradingContext = tradingContext; 26 | } 27 | 28 | public ZScoreExitCriterion(String firstSymbol, String secondSymbol, 29 | double exitZScore, ZScore zScore, TradingContext tradingContext) { 30 | this(firstSymbol, secondSymbol, zScore, tradingContext); 31 | this.exitZScore = exitZScore; 32 | } 33 | 34 | @Override 35 | public boolean isMet() throws CriterionViolationException { 36 | try { 37 | double zs = 38 | zScore.get( 39 | tradingContext.getLastPrice(firstSymbol), tradingContext.getLastPrice(secondSymbol)); 40 | if(tradingContext.getLastOrderBySymbol(firstSymbol).isShort() && zs < exitZScore || 41 | tradingContext.getLastOrderBySymbol(firstSymbol).isLong() && zs > exitZScore) { 42 | return true; 43 | } 44 | } catch (PriceNotAvailableException e) { 45 | return false; 46 | } catch (NoOrderAvailable noOrderAvailable) { 47 | noOrderAvailable.printStackTrace(); 48 | return false; 49 | } 50 | return false; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/io/codera/quant/util/Helper.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.util; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.google.common.collect.ImmutableList; 6 | import com.google.common.collect.Lists; 7 | import com.ib.client.Contract; 8 | import com.ib.client.Types; 9 | import com.ib.controller.ApiController; 10 | import io.codera.quant.config.ContractBuilder; 11 | import io.codera.quant.observers.HistoryObserver; 12 | import io.codera.quant.observers.IbHistoryObserver; 13 | import java.io.IOException; 14 | import java.lang.reflect.Field; 15 | import java.nio.charset.Charset; 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.nio.file.Paths; 19 | import java.time.LocalDateTime; 20 | import java.time.format.DateTimeFormatter; 21 | import java.util.List; 22 | import org.joda.time.DateTime; 23 | import org.joda.time.Days; 24 | import org.joda.time.DurationFieldType; 25 | import org.joda.time.LocalDate; 26 | import org.lst.trading.lib.series.DoubleSeries; 27 | import org.lst.trading.lib.series.MultipleDoubleSeries; 28 | import org.slf4j.Logger; 29 | import org.slf4j.LoggerFactory; 30 | import org.springframework.http.MediaType; 31 | import org.springframework.http.converter.HttpMessageConverter; 32 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 33 | import org.springframework.web.client.RestTemplate; 34 | 35 | /** 36 | * 37 | */ 38 | public class Helper { 39 | 40 | private static Logger logger = LoggerFactory.getLogger(Helper.class); 41 | 42 | public static void getFxQuotes(String baseSymbol, String symbol) throws IOException, 43 | NoSuchFieldException, IllegalAccessException { 44 | DateTime dt1 = new DateTime(2012, 3, 26, 12, 0, 0, 0); 45 | DateTime dt2 = new DateTime(2017, 1, 1, 12, 0, 0, 0); 46 | LocalDate startDate = new LocalDate(dt1); 47 | LocalDate endDate = new LocalDate(dt2); 48 | 49 | RestTemplate restOperations = new RestTemplate(); 50 | 51 | List> converters = restOperations.getMessageConverters(); 52 | for (HttpMessageConverter converter : converters) { 53 | if (converter instanceof MappingJackson2HttpMessageConverter) { 54 | MappingJackson2HttpMessageConverter jsonConverter = (MappingJackson2HttpMessageConverter) converter; 55 | jsonConverter.setObjectMapper(new ObjectMapper()); 56 | jsonConverter.setSupportedMediaTypes( 57 | ImmutableList.of(new MediaType("application", "json", MappingJackson2HttpMessageConverter.DEFAULT_CHARSET), 58 | new MediaType("text", "javascript", MappingJackson2HttpMessageConverter.DEFAULT_CHARSET))); 59 | } 60 | } 61 | 62 | int days = Days.daysBetween(startDate, endDate).getDays(); 63 | 64 | List lines = Lists.newLinkedList(); 65 | lines.add("Date, Adj Close"); 66 | Path file = Paths.get(String.format("/Users/beastie/Downloads/%s%s_quotes.csv", 67 | baseSymbol.toLowerCase(), 68 | symbol.toLowerCase())); 69 | for (int i = 0; i < days; i++) { 70 | LocalDate d = startDate.withFieldAdded(DurationFieldType.days(), i); 71 | 72 | Response res = 73 | restOperations.getForObject(String.format("http://api.fixer.io/%s?base=%s&symbols=%s", 74 | d, baseSymbol, symbol), 75 | Response.class); 76 | Field field = Response.Rates.class.getField(symbol.toLowerCase()); 77 | double resSymbol = (double) field.get(res.rates); 78 | lines.add(String.format("%s, %s", d, resSymbol)); 79 | logger.info("{} {}/{} - {}", res.date, baseSymbol, symbol, resSymbol); 80 | 81 | } 82 | Files.write(file, lines, Charset.forName("UTF-8")); 83 | 84 | } 85 | 86 | public static MultipleDoubleSeries getHistoryForSymbols( 87 | ApiController controller, 88 | int daysOfHistory, 89 | List symbols 90 | ) { 91 | 92 | DateTimeFormatter formatter = 93 | DateTimeFormatter.ofPattern("yyyyMMdd HH:mm:ss"); 94 | String date = LocalDateTime.now().format(formatter); 95 | 96 | ContractBuilder contractBuilder = new ContractBuilder(); 97 | 98 | List doubleSeries = Lists.newArrayList(); 99 | for(String symbol : symbols) { 100 | Contract contract = contractBuilder.build(symbol); 101 | HistoryObserver historyObserver = new IbHistoryObserver(symbol); 102 | controller.reqHistoricalData(contract, date, daysOfHistory, Types.DurationUnit.DAY, 103 | Types.BarSize._1_min, Types.WhatToShow.TRADES, false, historyObserver); 104 | doubleSeries.add(((IbHistoryObserver)historyObserver).observableDoubleSeries() 105 | .toBlocking() 106 | .first()); 107 | 108 | } 109 | 110 | return new MultipleDoubleSeries(doubleSeries); 111 | } 112 | 113 | 114 | static class Response { 115 | private String base; 116 | private String date; 117 | private Rates rates; 118 | 119 | public Rates getRates() { 120 | return rates; 121 | } 122 | 123 | public void setRates(Rates rates) { 124 | this.rates = rates; 125 | } 126 | 127 | public String getBase() { 128 | return base; 129 | } 130 | 131 | public void setBase(String base) { 132 | this.base = base; 133 | } 134 | 135 | public String getDate() { 136 | return date; 137 | } 138 | 139 | public void setDate(String date) { 140 | this.date = date; 141 | } 142 | 143 | static class Rates { 144 | 145 | @JsonProperty("AUD") 146 | public double aud; 147 | 148 | @JsonProperty("CAD") 149 | public double cad; 150 | 151 | @JsonProperty("CHF") 152 | public double chf; 153 | 154 | @JsonProperty("CYP") 155 | public double cyp; 156 | 157 | @JsonProperty("CZK") 158 | public double czk; 159 | 160 | @JsonProperty("DKK") 161 | public double dkk; 162 | 163 | @JsonProperty("EEK") 164 | public double eek; 165 | 166 | @JsonProperty("GBP") 167 | public double gbp; 168 | 169 | @JsonProperty("HKD") 170 | public double hkd; 171 | 172 | @JsonProperty("HUF") 173 | public double huf; 174 | 175 | @JsonProperty("ISK") 176 | public double isk; 177 | 178 | @JsonProperty("JPY") 179 | public double jpy; 180 | 181 | @JsonProperty("KRW") 182 | public double krw; 183 | 184 | @JsonProperty("LTL") 185 | public double ltl; 186 | 187 | @JsonProperty("LVL") 188 | public double lvl; 189 | 190 | @JsonProperty("MTL") 191 | public double mtl; 192 | 193 | @JsonProperty("NOK") 194 | public double nok; 195 | 196 | @JsonProperty("NZD") 197 | public double nzd; 198 | 199 | @JsonProperty("PLN") 200 | public double pln; 201 | 202 | @JsonProperty("ROL") 203 | public double rol; 204 | 205 | @JsonProperty("SEK") 206 | public double sek; 207 | 208 | @JsonProperty("SGD") 209 | public double sgd; 210 | 211 | @JsonProperty("SIT") 212 | public double sit; 213 | 214 | @JsonProperty("SKK") 215 | public double skk; 216 | 217 | @JsonProperty("TRY") 218 | public double trl; 219 | 220 | @JsonProperty("ZAR") 221 | public double zar; 222 | 223 | @JsonProperty("EUR") 224 | public double eur; 225 | 226 | @JsonProperty("BGN") 227 | public double bgn; 228 | 229 | @JsonProperty("BRL") 230 | public double brl; 231 | 232 | @JsonProperty("CNY") 233 | public double cny; 234 | 235 | @JsonProperty("HRK") 236 | public double hrk; 237 | 238 | @JsonProperty("IDR") 239 | public double idr; 240 | 241 | @JsonProperty("ILS") 242 | public double ils; 243 | 244 | @JsonProperty("INR") 245 | public double inr; 246 | 247 | @JsonProperty("MXN") 248 | public double mxn; 249 | 250 | @JsonProperty("MYR") 251 | public double myr; 252 | 253 | @JsonProperty("PHP") 254 | public double php; 255 | 256 | @JsonProperty("RON") 257 | public double ron; 258 | 259 | @JsonProperty("RUB") 260 | public double rub; 261 | 262 | @JsonProperty("THB") 263 | public double thb; 264 | 265 | @JsonProperty("USD") 266 | public double usd; 267 | 268 | @Override 269 | public String toString() { 270 | return 271 | "aud=" + aud + 272 | ", cad=" + cad + 273 | ", chf=" + chf + 274 | ", cyp=" + cyp + 275 | ", czk=" + czk + 276 | ", dkk=" + dkk + 277 | ", eek=" + eek + 278 | ", gbp=" + gbp + 279 | ", hkd=" + hkd + 280 | ", huf=" + huf + 281 | ", isk=" + isk + 282 | ", jpy=" + jpy + 283 | ", krw=" + krw + 284 | ", ltl=" + ltl + 285 | ", lvl=" + lvl + 286 | ", mtl=" + mtl + 287 | ", nok=" + nok + 288 | ", nzd=" + nzd + 289 | ", pln=" + pln + 290 | ", rol=" + rol + 291 | ", sek=" + sek + 292 | ", sgd=" + sgd + 293 | ", sit=" + sit + 294 | ", skk=" + skk + 295 | ", trl=" + trl + 296 | ", zar=" + zar + 297 | ", eur=" + eur + 298 | ", bgn=" + bgn + 299 | ", brl=" + brl + 300 | ", cny=" + cny + 301 | ", hrk=" + hrk + 302 | ", idr=" + idr + 303 | ", ils=" + ils + 304 | ", inr=" + inr + 305 | ", mxn=" + mxn + 306 | ", myr=" + myr + 307 | ", php=" + php + 308 | ", ron=" + ron + 309 | ", rub=" + rub + 310 | ", thb=" + thb + 311 | ", usd=" + usd; 312 | } 313 | } 314 | } 315 | 316 | 317 | public static double getMean(double[] data) { 318 | double sum = 0.0; 319 | for(double a : data) 320 | sum += a; 321 | return sum/data.length; 322 | } 323 | 324 | private static double getVariance(double[] data) { 325 | double mean = getMean(data); 326 | double temp = 0; 327 | for(double a :data) 328 | temp += (a-mean)*(a-mean); 329 | return temp/data.length; 330 | } 331 | 332 | public static double getStdDev(double[] data) { 333 | return Math.sqrt(getVariance(data)); 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 lukstei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/README.md: -------------------------------------------------------------------------------- 1 | ## What? 2 | 3 | This is a general purpose lightweight backtesting engine for stocks, written in modern Java 8. 4 | 5 | Some advantages to other backtesting implementations are: 6 | 7 | * It uses a callback model and since it is implemented in java it should be pretty performant when running many backtests 8 | * Implemented in a mature programming language 9 | * Easily extensible 10 | * Strategies are easily debuggable using a java IDE 11 | * Lightweight and therefore the backtesting engine is easily verifiable 12 | * No dependencies 13 | * Backtesting results are further analyzable in R or Excel since it uses a CSV output format 14 | 15 | 16 | ### Cointegration/Pairs trading 17 | 18 | I've written this library to primarily try out this strategy. 19 | 20 | The cointegration strategy, or also known as pairs trading strategy, tries to take two stocks and create a linear model to find a 21 | optimal hedge ratio between them in order create a stationary process. 22 | 23 | Assume stocks A and B with prices `Pa` and `Pb` respectively, we set `Pa = alpha + beta*Pb` and try to find optimal `alpha` and `beta`. 24 | One method to find `alpha` and `beta` is using a so called Kalman Filter which is a dynamic bayesian model and we use it as an online linear regression model to get our values. 25 | 26 | After we've found the values we look at the residuals given by `residuals = Pa - alpha - beta*Pb`, 27 | and if the last residual is greater than some threshold value you go short `n` A stocks and long `n*beta` B stocks, for some fixed `n`. 28 | 29 | For further explanation and a formal definition of cointegration and the strategy you may want to look at: 30 | 31 | * https://www.quantopian.com/posts/how-to-build-a-pairs-trading-strategy-on-quantopian or 32 | * Ernie Chan's book Algorithmic Trading: Winning Strategies and Their Rationale 33 | 34 | A good introduction video series to the Kalman filter can be found at Udacity (https://www.udacity.com/wiki/cs373/unit-2). 35 | 36 | ## How? 37 | 38 | ### Running a backtest 39 | 40 | Run a backtest skeleton: 41 | 42 | ```java 43 | void doBacktest() { 44 | String x = "GLD"; 45 | String y = "GDX"; 46 | 47 | // initialize the trading strategy 48 | TradingStrategy strategy = new CointegrationTradingStrategy(x, y); 49 | 50 | // download historical prices 51 | YahooFinance finance = new YahooFinance(); 52 | MultipleDoubleSeries priceSeries = new MultipleDoubleSeries(finance.getHistoricalAdjustedPrices(x).toBlocking().first(), finance.getHistoricalAdjustedPrices(y).toBlocking().first()); 53 | 54 | // initialize the backtesting engine 55 | int deposit = 15000; 56 | Backtest backtest = new Backtest(deposit, priceSeries); 57 | backtest.setLeverage(4); 58 | 59 | // run the backtest 60 | Backtest.Result result = backtest.run(strategy); 61 | 62 | // show results 63 | System.out.println(format(Locale.US, "P/L = %.2f, Final value = %.2f, Result = %.2f%%, Annualized = %.2f%%, Sharpe (rf=0%%) = %.2f", result.getPl(), result.getFinalValue(), result.getReturn() * 100, result.getReturn() / (days / 251.) * 100, result.getSharpe())); 64 | } 65 | ``` 66 | 67 | ### Creating a new strategy 68 | 69 | Just create a class which implements `org.lst.trading.lib.model.TradingStrategy`, for example a simple buy and hold strategy might look like this: 70 | 71 | ```java 72 | public class BuyAndHold implements TradingStrategy { 73 | Map mOrders; 74 | TradingContext mContext; 75 | 76 | @Override public void onStart(TradingContext context) { 77 | mContext = context; 78 | } 79 | 80 | @Override public void onTick() { 81 | if (mOrders == null) { 82 | mOrders = new HashMap<>(); 83 | mContext.getInstruments().stream().forEach(instrument -> mOrders.put(instrument, mContext.order(instrument, true, 1))); 84 | } 85 | } 86 | } 87 | ``` 88 | 89 | The `onTick()` method is called for every price change, all relevant information (like historical prices, etc..) is available through 90 | `TradingContext` and also orders can be submitted through it. 91 | 92 | 93 | ## Interesting classes to look at 94 | 95 | * [Backtest](https://github.com/lukstei/trading-backtest/blob/master/src/main/java/org/lst/trading/lib/backtest/Backtest.java): The core class which runs the backtest 96 | * package `org.lst.trading.lib.series`: 97 | * [TimeSeries](https://github.com/lukstei/trading-backtest/blob/master/src/main/java/org/lst/trading/lib/series/TimeSeries.java): A general purpose generic time series data structure implementation and which handles stuff like mapping, merging and filtering. 98 | * [DoubleSeries](https://github.com/lukstei/trading-backtest/blob/master/src/main/java/org/lst/trading/lib/series/DoubleSeries.java): A time series class which has doubles as values. (corresponds to a pandas.Series (python)) 99 | * [MultipleDoubleSeries](https://github.com/lukstei/trading-backtest/blob/master/src/main/java/org/lst/trading/lib/series/MultipleDoubleSeries.java): A time series class which has multiple doubles as values. (corresponds to a pandas.DataFrame or a R Dataframe) 100 | * [KalmanFilter](https://github.com/lukstei/trading-backtest/blob/master/src/main/java/org/lst/trading/main/strategy/kalman/KalmanFilter.java): A general purpose and fast Kalman filter implementation. 101 | * [Cointegration](https://github.com/lukstei/trading-backtest/blob/master/src/main/java/org/lst/trading/main/strategy/kalman/Cointegration.java): A cointegration model using a Kalman filter. 102 | * [CointegrationTradingStrategy](https://github.com/lukstei/trading-backtest/blob/master/src/main/java/org/lst/trading/main/strategy/kalman/CointegrationTradingStrategy.java): The cointegration strategy implementation. 103 | 104 | 105 | ### Example run of the cointegration strategy 106 | 107 | To run a backtest, edit and then run the main class `org.lst.trading.main.BacktestMain`. 108 | By default the cointegration strategy is executed with the `GLD` vs. `GDX` ETF's and you might get a result like this: 109 | 110 | `$ ./gradlew run` 111 | 112 | ``` 113 | 19:35:28.327 [RxCachedThreadScheduler-1] DEBUG org.lst.trading.lib.util.Http - GET http://ichart.yahoo.com/table.csv?s=GLD&a=0&b=1&c=2010&d=0&e=6&f=2016&g=d&ignore=.csv 114 | 19:35:29.655 [RxCachedThreadScheduler-1] DEBUG org.lst.trading.lib.util.Http - GET http://ichart.yahoo.com/table.csv?s=GDX&a=0&b=1&c=2010&d=0&e=6&f=2016&g=d&ignore=.csv 115 | 116 | 1,364,Buy,GDX,2010-02-23T00:00:00Z,2010-02-25T00:00:00Z,40.658018,41.566845,330.813028 117 | ... 118 | 577,1081,Sell,GDX,2015-12-23T00:00:00Z,2015-12-28T00:00:00Z,13.970000,13.790000,194.580000 119 | 578,145,Buy,GLD,2015-12-23T00:00:00Z,2015-12-28T00:00:00Z,102.309998,102.269997,-5.800145 120 | 121 | Backtest result of class org.lst.trading.main.strategy.kalman.CointegrationTradingStrategy: CointegrationStrategy{mY='GDX', mX='GLD'} 122 | Prices: MultipleDoubleSeries{mNames={GLD, GDX, from=2010-01-04T00:00:00Z, to=2016-01-05T00:00:00Z, size=1512} 123 | Simulated 1512 days, Initial deposit 15000, Leverage 4.000000 124 | Commissions = 2938.190000 125 | P/L = 22644.75, Final value = 37644.75, Result = 150.97%, Annualized = 25.06%, Sharpe (rf=0%) = 1.37 126 | 127 | Orders: /var/folders/_5/jv4ptlps2ydb4_ptyj_l2y100000gn/T/out-7373128809679149089.csv 128 | Statistics: /var/folders/_5/jv4ptlps2ydb4_ptyj_l2y100000gn/T/out-1984107031930922019.csv 129 | ``` 130 | 131 | To further investigate the results you can import the CSV files into some data analysis tool like R or Excel. 132 | 133 | I've created a R script which does some rudimentary analysis (in `src/main/r/report.r`). 134 | 135 | The return curve of the above strategy plotted using R: 136 | 137 | ![Returns](https://raw.githubusercontent.com/lukstei/trading-backtest/master/img/coint-returns.png) 138 | 139 | This is a plot of the implied residuals: 140 | 141 | ![Resiuals](https://raw.githubusercontent.com/lukstei/trading-backtest/master/img/coint-residuals.png) 142 | 143 | The cointegration can be quite profitable however the difficulty is to find some good cointegrated pairs. 144 | 145 | You might want to try for example Coca-Cola (KO) and Pepsi (PEP), gold (GLD) and gold miners (GDX) or Austrialia stock index (EWA) and Canada stock index (EWC) (both Canada and Australia are commodity based economies). 146 | 147 | 148 | ## Why? 149 | 150 | I'm generally interested in algorithmic trading and I read about the cointegration trading strategy in Ernest Chans Book and wanted to try it out. 151 | I know many people prefer using tools like Matlab and R to try out their strategies, and I also agree with them you can't get 152 | a prototype running faster using these technologies, however after the prototyping phase I prefer to implement my strategies 153 | in a "full blown" programming language where I have a mature IDE, good debugging tools and less 'magic' where I know exactly what is going on under the hood. 154 | 155 | This is a side project and I'm not planning to extend this further. 156 | 157 | It is thought as a educational project, if you want to do something similar, this may be a good starting point or if you just want to try out different strategies. 158 | I thought it might be useful for someone so I decided to make this open source. 159 | Feel free to do anything what you want with the code. 160 | 161 | ## Who? 162 | 163 | My name is Lukas Steinbrecher, I'm currently in the last year of the business informatics (Economics and Computer Science) master at Vienna UT. 164 | I'm interested in financial markets, (algorithmic) trading, computer science and also bayesian statistics (especially MCMC methods) and I'm currently a CFA Level 1 candidate. 165 | 166 | If you have any questions or comments feel free to contact me via lukas@lukstei.com or on [lukstei.com](https://lukstei.com). 167 | 168 | ## License 169 | 170 | MIT 171 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/backtest/BackTest.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.backtest; 2 | 3 | import io.codera.quant.strategy.Strategy; 4 | import java.util.ArrayList; 5 | import java.util.Collections; 6 | import java.util.Iterator; 7 | import java.util.List; 8 | import org.lst.trading.lib.model.ClosedOrder; 9 | import org.lst.trading.lib.series.DoubleSeries; 10 | import org.lst.trading.lib.series.MultipleDoubleSeries; 11 | import org.lst.trading.lib.series.TimeSeries; 12 | import org.lst.trading.lib.util.Statistics; 13 | 14 | import static org.lst.trading.lib.util.Util.check; 15 | 16 | public class BackTest { 17 | 18 | public static class Result { 19 | DoubleSeries mPlHistory; 20 | DoubleSeries mMarginHistory; 21 | double mPl; 22 | List mOrders; 23 | double mInitialFund; 24 | double mFinalValue; 25 | double mCommissions; 26 | 27 | public Result(double pl, DoubleSeries plHistory, DoubleSeries marginHistory, List orders, double initialFund, double finalValue, double commisions) { 28 | mPl = pl; 29 | mPlHistory = plHistory; 30 | mMarginHistory = marginHistory; 31 | mOrders = orders; 32 | mInitialFund = initialFund; 33 | mFinalValue = finalValue; 34 | mCommissions = commisions; 35 | } 36 | 37 | public DoubleSeries getMarginHistory() { 38 | return mMarginHistory; 39 | } 40 | 41 | public double getInitialFund() { 42 | return mInitialFund; 43 | } 44 | 45 | public DoubleSeries getAccountValueHistory() { 46 | return getPlHistory().plus(mInitialFund); 47 | } 48 | 49 | public double getFinalValue() { 50 | return mFinalValue; 51 | } 52 | 53 | public double getReturn() { 54 | return mFinalValue / mInitialFund - 1; 55 | } 56 | 57 | public double getAnnualizedReturn() { 58 | return getReturn() * 250 / getDaysCount(); 59 | } 60 | 61 | public double getSharpe() { 62 | return Statistics.sharpe(Statistics.returns(getAccountValueHistory().toArray())); 63 | } 64 | 65 | public double getMaxDrawdown() { 66 | return Statistics.drawdown(getAccountValueHistory().toArray())[0]; 67 | } 68 | 69 | public double getMaxDrawdownPercent() { 70 | return Statistics.drawdown(getAccountValueHistory().toArray())[1]; 71 | } 72 | 73 | public int getDaysCount() { 74 | return mPlHistory.size(); 75 | } 76 | 77 | public DoubleSeries getPlHistory() { 78 | return mPlHistory; 79 | } 80 | 81 | public double getPl() { 82 | return mPl; 83 | } 84 | 85 | public double getCommissions() { 86 | return mCommissions; 87 | } 88 | 89 | public List getOrders() { 90 | return mOrders; 91 | } 92 | } 93 | 94 | MultipleDoubleSeries mPriceSeries; 95 | double mDeposit; 96 | double mLeverage = 1; 97 | 98 | Strategy mStrategy; 99 | BackTestTradingContext mContext; 100 | 101 | Iterator>> mPriceIterator; 102 | Result mResult; 103 | 104 | public BackTest(double deposit, MultipleDoubleSeries priceSeries) { 105 | check(priceSeries.isAscending()); 106 | mDeposit = deposit; 107 | mPriceSeries = priceSeries; 108 | } 109 | 110 | public void setLeverage(double leverage) { 111 | mLeverage = leverage; 112 | } 113 | 114 | public double getLeverage() { 115 | return mLeverage; 116 | } 117 | 118 | public Result run(Strategy strategy) { 119 | initialize(strategy); 120 | while (nextStep()) ; 121 | return mResult; 122 | } 123 | 124 | public void initialize(Strategy strategy) { 125 | mStrategy = strategy; 126 | mContext = (BackTestTradingContext) strategy.getTradingContext(); 127 | 128 | mContext.mInstruments = mPriceSeries.getNames(); 129 | mContext.mHistory = new MultipleDoubleSeries(mContext.mInstruments); 130 | mContext.mInitialFunds = mDeposit; 131 | mContext.mLeverage = mLeverage; 132 | 133 | mPriceIterator = mPriceSeries.iterator(); 134 | nextStep(); 135 | } 136 | 137 | public boolean nextStep() { 138 | if (!mPriceIterator.hasNext()) { 139 | finish(); 140 | return false; 141 | } 142 | 143 | TimeSeries.Entry> entry = mPriceIterator.next(); 144 | 145 | mContext.mPrices = entry.getItem(); 146 | mContext.mInstant = entry.getInstant(); 147 | mContext.mPl.add(mContext.getPl(), entry.getInstant()); 148 | mContext.mFundsHistory.add(mContext.getAvailableFunds(), entry.getInstant()); 149 | if (mContext.getAvailableFunds() < 0) { 150 | finish(); 151 | return false; 152 | } 153 | 154 | mStrategy.onTick(); 155 | 156 | mContext.mHistory.add(entry); 157 | 158 | return true; 159 | } 160 | 161 | public Result getResult() { 162 | return mResult; 163 | } 164 | 165 | private void finish() { 166 | for (SimpleOrder order : new ArrayList<>(mContext.mOrders)) { 167 | mContext.close(order); 168 | } 169 | 170 | // TODO (replace below code with BackTest results implementation 171 | // mStrategy.onEnd(); 172 | 173 | List orders = Collections.unmodifiableList(mContext.mClosedOrders); 174 | mResult = new Result(mContext.mClosedPl, mContext.mPl, mContext.mFundsHistory, orders, mDeposit, mDeposit + mContext.mClosedPl, mContext.mCommissions); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/backtest/BackTestTradingContext.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.backtest; 2 | 3 | import com.google.common.collect.Maps; 4 | import io.codera.quant.context.TradingContext; 5 | import io.codera.quant.exception.NoOrderAvailable; 6 | import io.codera.quant.exception.PriceNotAvailableException; 7 | import java.math.BigDecimal; 8 | import java.math.RoundingMode; 9 | import java.text.SimpleDateFormat; 10 | import java.time.Instant; 11 | import java.util.ArrayList; 12 | import java.util.Date; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.TimeZone; 16 | import java.util.stream.Stream; 17 | import org.lst.trading.lib.model.ClosedOrder; 18 | import org.lst.trading.lib.model.Order; 19 | import org.lst.trading.lib.series.DoubleSeries; 20 | import org.lst.trading.lib.series.MultipleDoubleSeries; 21 | import org.lst.trading.lib.series.TimeSeries; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | import static com.google.common.base.Preconditions.checkArgument; 26 | import static org.lst.trading.lib.util.Util.check; 27 | 28 | public class BackTestTradingContext implements TradingContext { 29 | Instant mInstant; 30 | List mPrices; 31 | List mInstruments; 32 | DoubleSeries mPl = new DoubleSeries("pl"); 33 | DoubleSeries mFundsHistory = new DoubleSeries("funds"); 34 | MultipleDoubleSeries mHistory; 35 | double mInitialFunds; 36 | double mCommissions; 37 | private Map orders; 38 | private Map closePriceMap = Maps.newConcurrentMap(); 39 | 40 | int mOrderId = 1; 41 | 42 | List mOrders = new ArrayList<>(); 43 | 44 | double mClosedPl = 0; 45 | List mClosedOrders = new ArrayList<>(); 46 | double mLeverage; 47 | private static Logger logger = LoggerFactory.getLogger(BackTestTradingContext.class); 48 | 49 | @Override public Instant getTime() { 50 | return mInstant; 51 | } 52 | 53 | @Override public double getLastPrice(String instrument) { 54 | logger.info("Time: {}", mInstant.toString()); 55 | 56 | Date date = Date.from(mInstant); 57 | SimpleDateFormat hourMinutes = new SimpleDateFormat("HH:mm"); 58 | hourMinutes.setTimeZone(TimeZone.getTimeZone("UTC")); 59 | String formattedHourMinutes = hourMinutes.format(date); 60 | double price = mPrices.get(mInstruments.indexOf(instrument)); 61 | if(formattedHourMinutes.equals("13:00")) { 62 | closePriceMap.put(instrument, price); 63 | } 64 | return price; 65 | } 66 | 67 | @Override public Stream> getHistory(String instrument) { 68 | int index = mInstruments.indexOf(instrument); 69 | return mHistory.reversedStream().map(t -> new TimeSeries.Entry<>(t.getItem().get(index), t.getInstant())); 70 | } 71 | 72 | @Override 73 | public void addContract(String contract) { 74 | throw new UnsupportedOperationException(); 75 | } 76 | 77 | @Override 78 | public void removeContract(String contract) { 79 | throw new UnsupportedOperationException(); 80 | } 81 | 82 | @Override 83 | public List getContracts() { 84 | return mInstruments; 85 | } 86 | 87 | @Override public Order order(String instrument, boolean buy, int amount) { 88 | // check(amount > 0); 89 | logger.info("OPEN {} in amount {}", instrument, (buy ? 1 : -1) * amount); 90 | double price = getLastPrice(instrument); 91 | SimpleOrder order = new SimpleOrder(mOrderId++, instrument, getTime(), price, amount * (buy ? 1 : -1)); 92 | mOrders.add(order); 93 | if(orders == null) { 94 | orders = Maps.newConcurrentMap(); 95 | } 96 | orders.put(instrument, order); 97 | 98 | mCommissions += calculateCommission(order); 99 | 100 | return order; 101 | } 102 | 103 | @Override public ClosedOrder close(Order order) { 104 | logger.info("CLOSE {} in amount {}", order.getInstrument(), -order.getAmount()); 105 | 106 | SimpleOrder simpleOrder = (SimpleOrder) order; 107 | mOrders.remove(simpleOrder); 108 | double price = getLastPrice(order.getInstrument()); 109 | SimpleClosedOrder closedOrder = new SimpleClosedOrder(simpleOrder, price, getTime()); 110 | mClosedOrders.add(closedOrder); 111 | mClosedPl += closedOrder.getPl(); 112 | mCommissions += calculateCommission(order); 113 | if(orders != null) { 114 | orders.remove(order.getInstrument()); 115 | } 116 | 117 | return closedOrder; 118 | } 119 | 120 | @Override 121 | public Order getLastOrderBySymbol(String symbol) throws NoOrderAvailable { 122 | checkArgument(symbol != null, "symbol is null"); 123 | if(orders == null || !orders.containsKey(symbol)) { 124 | throw new NoOrderAvailable(); 125 | } 126 | return orders.get(symbol); 127 | } 128 | 129 | public double getPl() { 130 | return mClosedPl + mOrders.stream().mapToDouble(t -> t.calculatePl(getLastPrice(t.getInstrument()))).sum() - mCommissions; 131 | } 132 | 133 | 134 | @Override public double getAvailableFunds() { 135 | return getNetValue() - mOrders.stream().mapToDouble(t -> Math.abs(t.getAmount()) * t.getOpenPrice() / mLeverage).sum(); 136 | } 137 | 138 | public double getInitialFunds() { 139 | return mInitialFunds; 140 | } 141 | 142 | @Override public double getNetValue() { 143 | return mInitialFunds + getPl(); 144 | } 145 | 146 | @Override public double getLeverage() { 147 | return mLeverage; 148 | } 149 | 150 | double calculateCommission(Order order) { 151 | if(order.getInstrument().contains("/")) { 152 | return Math.abs(order.getAmount()) * order.getOpenPrice() * 0.00002; 153 | } 154 | else if(order.getInstrument().contains("=F")) { 155 | return Math.abs(order.getAmount()) * 2.04; 156 | } 157 | double commissions = Math.max(1, Math.abs(order.getAmount()) * 0.005); 158 | logger.debug("Commissions: {}", commissions); 159 | return commissions; 160 | } 161 | 162 | public double getChangeBySymbol(String symbol) throws PriceNotAvailableException { 163 | 164 | if(!closePriceMap.containsKey(symbol)) { 165 | throw new PriceNotAvailableException(); 166 | } 167 | double closePrice = closePriceMap.get(symbol); 168 | double currentPrice = getLastPrice(symbol); 169 | 170 | BigDecimal diff = BigDecimal.valueOf(currentPrice).add(BigDecimal.valueOf(-closePrice)); 171 | 172 | BigDecimal res = diff.multiply(BigDecimal.valueOf(100)) 173 | .divide(BigDecimal.valueOf(closePrice), RoundingMode.HALF_UP); 174 | BigDecimal rounded = res.setScale(2, RoundingMode.HALF_UP); 175 | return rounded.doubleValue(); 176 | } 177 | } 178 | 179 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/backtest/SimpleClosedOrder.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.backtest; 2 | 3 | import java.time.Instant; 4 | import org.lst.trading.lib.model.ClosedOrder; 5 | 6 | public class SimpleClosedOrder implements ClosedOrder { 7 | SimpleOrder mOrder; 8 | double mClosePrice; 9 | Instant mCloseInstant; 10 | double mPl; 11 | 12 | public SimpleClosedOrder(SimpleOrder order, double closePrice, Instant closeInstant) { 13 | mOrder = order; 14 | mClosePrice = closePrice; 15 | mCloseInstant = closeInstant; 16 | mPl = calculatePl(mClosePrice); 17 | } 18 | 19 | @Override public int getId() { 20 | return mOrder.getId(); 21 | } 22 | 23 | @Override public double getClosePrice() { 24 | return mClosePrice; 25 | } 26 | 27 | @Override public Instant getCloseInstant() { 28 | return mCloseInstant; 29 | } 30 | 31 | @Override public double getPl() { 32 | return mPl; 33 | } 34 | 35 | @Override public boolean isLong() { 36 | return mOrder.isLong(); 37 | } 38 | 39 | @Override public int getAmount() { 40 | return mOrder.getAmount(); 41 | } 42 | 43 | @Override public double getOpenPrice() { 44 | return mOrder.getOpenPrice(); 45 | } 46 | 47 | @Override public Instant getOpenInstant() { 48 | return mOrder.getOpenInstant(); 49 | } 50 | 51 | @Override public String getInstrument() { 52 | return mOrder.getInstrument(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/backtest/SimpleOrder.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.backtest; 2 | 3 | import java.time.Instant; 4 | import org.lst.trading.lib.model.Order; 5 | 6 | public class SimpleOrder implements Order { 7 | int mId; 8 | int mAmount; 9 | double mOpenPrice; 10 | Instant mOpenInstant; 11 | String mInstrument; 12 | 13 | public SimpleOrder(int id, String instrument, Instant openInstant, double openPrice, int amount) { 14 | mId = id; 15 | mInstrument = instrument; 16 | mOpenInstant = openInstant; 17 | mOpenPrice = openPrice; 18 | mAmount = amount; 19 | } 20 | 21 | @Override public int getId() { 22 | return mId; 23 | } 24 | 25 | @Override public int getAmount() { 26 | return mAmount; 27 | } 28 | 29 | @Override public double getOpenPrice() { 30 | return mOpenPrice; 31 | } 32 | 33 | @Override public Instant getOpenInstant() { 34 | return mOpenInstant; 35 | } 36 | 37 | @Override public String getInstrument() { 38 | return mInstrument; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/csv/CsvReader.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.csv; 2 | 3 | import java.time.Duration; 4 | import java.time.Instant; 5 | import java.time.LocalDate; 6 | import java.time.LocalDateTime; 7 | import java.time.format.DateTimeFormatter; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.function.Consumer; 11 | import java.util.function.Function; 12 | import java.util.stream.Stream; 13 | import org.lst.trading.lib.model.Bar; 14 | import org.lst.trading.lib.series.DoubleSeries; 15 | import org.lst.trading.lib.series.MultipleDoubleSeries; 16 | import org.lst.trading.lib.series.TimeSeries; 17 | 18 | import static java.util.stream.Collectors.toList; 19 | 20 | public class CsvReader { 21 | public interface ParseFunction { 22 | T parse(String value); 23 | 24 | String getColumn(); 25 | 26 | default ParseFunction map(Function f) { 27 | return new ParseFunction() { 28 | @Override public F parse(String value) { 29 | return f.apply(ParseFunction.this.parse(value)); 30 | } 31 | 32 | @Override public String getColumn() { 33 | return ParseFunction.this.getColumn(); 34 | } 35 | }; 36 | } 37 | 38 | static Function stripQuotes() { 39 | return s -> { 40 | s = s.replaceFirst("^\"", ""); 41 | s = s.replaceFirst("\"$", ""); 42 | return s; 43 | }; 44 | } 45 | 46 | static ParseFunction ofColumn(String columnName) { 47 | return new ParseFunction() { 48 | @Override public String parse(String value) { 49 | return value; 50 | } 51 | 52 | @Override public String getColumn() { 53 | return columnName; 54 | } 55 | }; 56 | } 57 | 58 | static ParseFunction longColumn(String column) { 59 | return ofColumn(column).map(Long::parseLong); 60 | } 61 | 62 | static ParseFunction doubleColumn(String column) { 63 | return ofColumn(column).map(Double::parseDouble); 64 | } 65 | 66 | static ParseFunction localDateTimeColumn(String column, DateTimeFormatter formatter) { 67 | return ofColumn(column).map(x -> LocalDateTime.from(formatter.parse(x))); 68 | } 69 | 70 | static ParseFunction instantColumn(String column, DateTimeFormatter formatter) { 71 | return ofColumn(column).map(x -> Instant.from(formatter.parse(x))); 72 | } 73 | 74 | static ParseFunction localDateColumn(String column, DateTimeFormatter formatter) { 75 | return ofColumn(column).map(x -> LocalDate.from(formatter.parse(x))); 76 | } 77 | 78 | static ParseFunction localTimeColumn(String column, DateTimeFormatter formatter) { 79 | return ofColumn(column).map(x -> LocalDate.from(formatter.parse(x))); 80 | } 81 | } 82 | 83 | public interface Function2 { 84 | F apply(T1 t1, T2 t2); 85 | } 86 | 87 | private static class SeriesConsumer implements Consumer { 88 | int i = 0; 89 | List mColumns; 90 | TimeSeries mSeries; 91 | ParseFunction mInstantParseFunction; 92 | Function2, T> mF; 93 | 94 | public SeriesConsumer(TimeSeries series, ParseFunction instantParseFunction, Function2, T> f) { 95 | mSeries = series; 96 | mInstantParseFunction = instantParseFunction; 97 | mF = f; 98 | } 99 | 100 | @Override public void accept(String[] parts) { 101 | if (i++ == 0) { 102 | mColumns = Stream.of(parts).map(String::trim).collect(toList()); 103 | } else { 104 | Instant instant = mInstantParseFunction.parse(parts[mColumns.indexOf(mInstantParseFunction.getColumn())]); 105 | mSeries.add(mF.apply(parts, mColumns), instant); 106 | } 107 | } 108 | } 109 | 110 | private static class ListConsumer implements Consumer { 111 | int i = 0; 112 | List mEntries = new ArrayList<>(); 113 | Function mF; 114 | boolean mHasHeader; 115 | 116 | public ListConsumer(Function f, boolean hasHeader) { 117 | mF = f; 118 | mHasHeader = hasHeader; 119 | } 120 | 121 | @Override public void accept(String[] parts) { 122 | if (i++ > 0 || !mHasHeader) { 123 | mEntries.add(mF.apply(parts)); 124 | } 125 | } 126 | 127 | public List getEntries() { 128 | return mEntries; 129 | } 130 | } 131 | 132 | public static > void parse(Stream lines, String sep, T consumer) { 133 | lines.map(l -> l.split(sep)).forEach(consumer); 134 | } 135 | 136 | public static List parse(Stream lines, String sep, boolean hasHeader, Function f) { 137 | ListConsumer consumer = new ListConsumer(f, hasHeader); 138 | parse(lines, sep, consumer); 139 | return consumer.getEntries(); 140 | } 141 | 142 | @SafeVarargs 143 | public static MultipleDoubleSeries parse(Stream lines, String sep, ParseFunction instantF, ParseFunction... columns) { 144 | List columnNames = Stream.of(columns).map(ParseFunction::getColumn).collect(toList()); 145 | MultipleDoubleSeries series = new MultipleDoubleSeries(columnNames); 146 | SeriesConsumer> consumer = new SeriesConsumer<>(series, instantF, (parts, cn) -> Stream.of(columns).map(t -> t.parse(parts[cn.indexOf(t.getColumn())])).collect(toList())); 147 | parse(lines, sep, consumer); 148 | return series; 149 | } 150 | 151 | public static DoubleSeries parse(Stream lines, String sep, ParseFunction instantF, ParseFunction column) { 152 | DoubleSeries series = new DoubleSeries(column.getColumn()); 153 | SeriesConsumer consumer = new SeriesConsumer<>(series, instantF, (parts, columnNames) -> column.parse(parts[columnNames.indexOf(column.getColumn())])); 154 | parse(lines, sep, consumer); 155 | return series; 156 | } 157 | 158 | public static Stream parse(Stream lines, ParseFunction open, ParseFunction high, ParseFunction low, ParseFunction close, ParseFunction volume, ParseFunction instant) { 159 | return lines 160 | .map(l -> l.split(",")) 161 | .flatMap(new Function>() { 162 | public List mColumns; 163 | int i = 0; 164 | 165 | @Override public Stream apply(String[] parts) { 166 | if (i++ == 0) { 167 | mColumns = Stream.of(parts).map(String::trim).collect(toList()); 168 | return Stream.empty(); 169 | } else { 170 | return Stream.of( 171 | new Bar() { 172 | private double mOpen = open.parse(parts[mColumns.indexOf(open.getColumn())]); 173 | private double mHigh = high.parse(parts[mColumns.indexOf(high.getColumn())]); 174 | private double mLow = low.parse(parts[mColumns.indexOf(low.getColumn())]); 175 | private double mClose = close.parse(parts[mColumns.indexOf(close.getColumn())]); 176 | private long mVolume = volume.parse(parts[mColumns.indexOf(volume.getColumn())]); 177 | 178 | @Override public double getOpen() { 179 | return mOpen; 180 | } 181 | 182 | @Override public double getHigh() { 183 | return mHigh; 184 | } 185 | 186 | @Override public double getLow() { 187 | return mLow; 188 | } 189 | 190 | @Override public double getClose() { 191 | return mClose; 192 | } 193 | 194 | @Override public long getVolume() { 195 | return mVolume; 196 | } 197 | 198 | @Override public Instant getStart() { 199 | return instant.parse(parts[mColumns.indexOf(instant.getColumn())]); 200 | } 201 | 202 | @Override public Duration getDuration() { 203 | return null; 204 | } 205 | 206 | @Override public double getWAP() { 207 | return 0; 208 | } 209 | } 210 | ); 211 | } 212 | } 213 | }); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/csv/CsvWriter.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.csv; 2 | 3 | import java.io.BufferedWriter; 4 | import java.io.IOException; 5 | import java.io.OutputStream; 6 | import java.io.OutputStreamWriter; 7 | import java.io.Writer; 8 | import java.util.List; 9 | import java.util.stream.Stream; 10 | import rx.functions.Func1; 11 | 12 | import static java.util.stream.Collectors.joining; 13 | import static java.util.stream.Stream.concat; 14 | import static java.util.stream.Stream.of; 15 | 16 | public class CsvWriter { 17 | public static CsvWriter create(List> columns) { 18 | return new CsvWriter<>(columns); 19 | } 20 | 21 | public static Column column(String name, Func1 f) { 22 | return new Column() { 23 | @Override public String getName() { 24 | return name; 25 | } 26 | 27 | @Override public Func1 getF() { 28 | return f; 29 | } 30 | }; 31 | } 32 | 33 | public interface Column { 34 | String getName(); 35 | 36 | Func1 getF(); 37 | } 38 | 39 | String mSeparator = ","; 40 | List> mColumns; 41 | 42 | public CsvWriter(List> columns) { 43 | mColumns = columns; 44 | } 45 | 46 | public Stream write(Stream values) { 47 | return concat( 48 | of(mColumns.stream().map(Column::getName).collect(joining(mSeparator))), 49 | values.map(x -> mColumns.stream().map(f -> { 50 | Object o = f.getF().call(x); 51 | return o == null ? "" : o.toString(); 52 | }).collect(joining(mSeparator))) 53 | ); 54 | } 55 | 56 | public void writeToStream(Stream values, OutputStream outputStream) throws IOException { 57 | Writer writer = new BufferedWriter(new OutputStreamWriter(outputStream)); 58 | write(values).forEach(x -> { 59 | try { 60 | writer.write(x); 61 | writer.write("\n"); 62 | } catch (IOException e) { 63 | throw new RuntimeException(e); 64 | } 65 | }); 66 | writer.flush(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/model/Bar.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.model; 2 | 3 | import java.time.Duration; 4 | import java.time.Instant; 5 | 6 | public interface Bar extends Comparable { 7 | double getOpen(); 8 | 9 | double getHigh(); 10 | 11 | double getLow(); 12 | 13 | double getClose(); 14 | 15 | long getVolume(); 16 | 17 | Instant getStart(); 18 | 19 | Duration getDuration(); 20 | 21 | double getWAP(); 22 | 23 | default double getAverage() { 24 | return (getHigh() + getLow()) / 2; 25 | } 26 | 27 | @Override default int compareTo(Bar o) { 28 | return getStart().compareTo(o.getStart()); 29 | } 30 | 31 | default Instant getEnd() { 32 | return getStart().plus(getDuration()); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/model/ClosedOrder.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.model; 2 | 3 | import java.time.Instant; 4 | 5 | public interface ClosedOrder extends Order { 6 | double getClosePrice(); 7 | 8 | Instant getCloseInstant(); 9 | 10 | default double getPl() { 11 | return calculatePl(getClosePrice()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/model/Order.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.model; 2 | 3 | import com.ib.client.OrderStatus; 4 | import io.codera.quant.config.ContractBuilder; 5 | import java.time.Instant; 6 | 7 | public interface Order { 8 | int getId(); 9 | 10 | int getAmount(); 11 | 12 | double getOpenPrice(); 13 | 14 | Instant getOpenInstant(); 15 | 16 | String getInstrument(); 17 | 18 | default OrderStatus getOrderStatus(){ 19 | return OrderStatus.Inactive; 20 | } 21 | 22 | default boolean isLong() { 23 | return getAmount() > 0; 24 | } 25 | 26 | default boolean isShort() { 27 | return !isLong(); 28 | } 29 | 30 | default int getSign() { 31 | return isLong() ? 1 : -1; 32 | } 33 | 34 | default double calculatePl(double currentPrice) { 35 | 36 | if(getInstrument().contains("=F")) { 37 | 38 | return getAmount() * (currentPrice - getOpenPrice()) * 39 | ContractBuilder.getFutureMultiplier(getInstrument()); 40 | } 41 | return getAmount() * (currentPrice - getOpenPrice()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/model/TradingContext.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.model; 2 | 3 | import java.time.Instant; 4 | import java.util.List; 5 | import java.util.stream.Stream; 6 | import org.lst.trading.lib.series.TimeSeries; 7 | 8 | public interface TradingContext { 9 | Instant getTime(); 10 | 11 | double getLastPrice(String instrument); 12 | 13 | Stream> getHistory(String instrument); 14 | 15 | Order order(String instrument, boolean buy, int amount); 16 | 17 | ClosedOrder close(Order order); 18 | 19 | double getPl(); 20 | 21 | List getInstruments(); 22 | 23 | double getAvailableFunds(); 24 | 25 | double getInitialFunds(); 26 | 27 | double getNetValue(); 28 | 29 | double getLeverage(); 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/model/TradingStrategy.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.model; 2 | 3 | public interface TradingStrategy { 4 | default void onStart(TradingContext context) { 5 | 6 | } 7 | 8 | default void onTick() { 9 | 10 | } 11 | 12 | default void onEnd() { 13 | 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/series/DoubleSeries.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.series; 2 | 3 | import java.util.List; 4 | import java.util.function.Function; 5 | 6 | public class DoubleSeries extends TimeSeries { 7 | String mName; 8 | 9 | DoubleSeries(List> data, String name) { 10 | super(data); 11 | mName = name; 12 | } 13 | 14 | public DoubleSeries(String name) { 15 | super(); 16 | mName = name; 17 | } 18 | 19 | public String getName() { 20 | return mName; 21 | } 22 | 23 | public void setName(String name) { 24 | mName = name; 25 | } 26 | 27 | public DoubleSeries merge(DoubleSeries other, MergeFunction f) { 28 | return new DoubleSeries(DoubleSeries.merge(this, other, f).mData, mName); 29 | } 30 | 31 | public DoubleSeries mapToDouble(Function f) { 32 | return new DoubleSeries(map(f).mData, mName); 33 | } 34 | 35 | public DoubleSeries plus(DoubleSeries other) { 36 | return merge(other, (x, y) -> x + y); 37 | } 38 | 39 | public DoubleSeries plus(double other) { 40 | return mapToDouble(x -> x + other); 41 | } 42 | 43 | public DoubleSeries mul(DoubleSeries other) { 44 | return merge(other, (x, y) -> x * y); 45 | } 46 | 47 | public DoubleSeries mul(double factor) { 48 | return mapToDouble(x -> x * factor); 49 | } 50 | 51 | public DoubleSeries div(DoubleSeries other) { 52 | return merge(other, (x, y) -> x / y); 53 | } 54 | 55 | public DoubleSeries returns() { 56 | return this.div(lag(1)).plus(-1); 57 | } 58 | 59 | public double getLast() { 60 | return getData().get(size() - 1).getItem(); 61 | } 62 | 63 | public DoubleSeries tail(int n) { 64 | return new DoubleSeries(getData().subList(size() - n, size()), getName()); 65 | } 66 | 67 | public DoubleSeries returns(int days) { 68 | return this.div(lag(days)).plus(-1); 69 | } 70 | 71 | public double[] toArray() { 72 | return stream().mapToDouble(Entry::getItem).toArray(); 73 | } 74 | 75 | @Override public DoubleSeries toAscending() { 76 | return new DoubleSeries(super.toAscending().mData, getName()); 77 | } 78 | 79 | @Override public DoubleSeries toDescending() { 80 | return new DoubleSeries(super.toDescending().mData, getName()); 81 | } 82 | 83 | @Override public DoubleSeries lag(int k) { 84 | return new DoubleSeries(super.lag(k).mData, getName()); 85 | } 86 | 87 | @Override public String toString() { 88 | return mData.isEmpty() ? "DoubleSeries{empty}" : 89 | "DoubleSeries{" + 90 | "mName=" + mName + 91 | ", from=" + mData.get(0).getInstant() + 92 | ", to=" + mData.get(mData.size() - 1).getInstant() + 93 | ", size=" + mData.size() + 94 | '}'; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/series/MultipleDoubleSeries.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.series; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.LinkedList; 6 | import java.util.List; 7 | 8 | import static java.util.stream.Collectors.joining; 9 | import static java.util.stream.Collectors.toList; 10 | 11 | public class MultipleDoubleSeries extends TimeSeries> { 12 | List mNames; 13 | 14 | public MultipleDoubleSeries(Collection names) { 15 | mNames = new ArrayList<>(names); 16 | } 17 | 18 | public MultipleDoubleSeries(List series) { 19 | mNames = new ArrayList<>(); 20 | for (int i = 0; i < series.size(); i++) { 21 | if (i == 0) { 22 | _init(series.get(i)); 23 | } else { 24 | addSeries(series.get(i)); 25 | } 26 | } 27 | } 28 | 29 | public MultipleDoubleSeries(DoubleSeries... series) { 30 | mNames = new ArrayList<>(); 31 | for (int i = 0; i < series.length; i++) { 32 | if (i == 0) { 33 | _init(series[i]); 34 | } else { 35 | addSeries(series[i]); 36 | } 37 | } 38 | } 39 | 40 | void _init(DoubleSeries series) { 41 | mData = new ArrayList<>(); 42 | for (Entry entry : series) { 43 | LinkedList list = new LinkedList<>(); 44 | list.add(entry.mT); 45 | add(new Entry<>(list, entry.mInstant)); 46 | } 47 | mNames.add(series.mName); 48 | } 49 | 50 | public void addSeries(DoubleSeries series) { 51 | mData = merge(this, series, (l, t) -> { 52 | l.add(t); 53 | return l; 54 | }).mData; 55 | mNames.add(series.mName); 56 | } 57 | 58 | public DoubleSeries getColumn(String name) { 59 | int index = getNames().indexOf(name); 60 | List> entries = mData.stream().map(t -> new Entry(t.getItem().get(index), t.getInstant())).collect(toList()); 61 | return new DoubleSeries(entries, name); 62 | } 63 | 64 | public int indexOf(String name) { 65 | return mNames.indexOf(name); 66 | } 67 | 68 | public List getNames() { 69 | return mNames; 70 | } 71 | 72 | @Override public String toString() { 73 | return mData.isEmpty() ? "MultipleDoubleSeries{empty}" : 74 | "MultipleDoubleSeries{" + 75 | "mNames={" + mNames.stream().collect(joining(", ")) + 76 | ", from=" + mData.get(0).getInstant() + 77 | ", to=" + mData.get(mData.size() - 1).getInstant() + 78 | ", size=" + mData.size() + 79 | '}'; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/series/TimeSeries.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.series; 2 | 3 | import java.time.Instant; 4 | import java.util.ArrayList; 5 | import java.util.Collections; 6 | import java.util.Iterator; 7 | import java.util.LinkedList; 8 | import java.util.List; 9 | import java.util.function.Function; 10 | import java.util.stream.IntStream; 11 | import java.util.stream.Stream; 12 | 13 | import static org.lst.trading.lib.util.Util.check; 14 | 15 | public class TimeSeries implements Iterable> { 16 | public static class Entry { 17 | T mT; 18 | Instant mInstant; 19 | 20 | public Entry(T t, Instant instant) { 21 | mT = t; 22 | mInstant = instant; 23 | } 24 | 25 | public T getItem() { 26 | return mT; 27 | } 28 | 29 | public Instant getInstant() { 30 | return mInstant; 31 | } 32 | 33 | @Override 34 | public boolean equals(Object o) { 35 | if (this == o) return true; 36 | if (o == null || getClass() != o.getClass()) return false; 37 | 38 | Entry entry = (Entry) o; 39 | 40 | if (!mInstant.equals(entry.mInstant)) return false; 41 | if (mT != null ? !mT.equals(entry.mT) : entry.mT != null) return false; 42 | return true; 43 | } 44 | 45 | @Override 46 | public int hashCode() { 47 | int result = mT != null ? mT.hashCode() : 0; 48 | result = 31 * result + mInstant.hashCode(); 49 | return result; 50 | } 51 | 52 | @Override public String toString() { 53 | return "Entry{" + 54 | "mInstant=" + mInstant + 55 | ", mT=" + mT + 56 | '}'; 57 | } 58 | } 59 | 60 | List> mData; 61 | 62 | public TimeSeries() { 63 | mData = new ArrayList<>(); 64 | } 65 | 66 | protected TimeSeries(List> data) { 67 | mData = data; 68 | } 69 | 70 | public int size() { 71 | return mData.size(); 72 | } 73 | 74 | public boolean isEmpty() { 75 | return mData.isEmpty(); 76 | } 77 | 78 | public boolean add(Entry tEntry) { 79 | return mData.add(tEntry); 80 | } 81 | 82 | public void add(T item, Instant instant) { 83 | add(new Entry(item, instant)); 84 | } 85 | 86 | public Stream> stream() { 87 | return mData.stream(); 88 | } 89 | 90 | public Stream> reversedStream() { 91 | check(!(mData instanceof LinkedList)); 92 | return IntStream.range(1, mData.size() + 1).mapToObj(i -> mData.get(mData.size() - i)); 93 | } 94 | 95 | @Override public Iterator> iterator() { 96 | return mData.iterator(); 97 | } 98 | 99 | public List> getData() { 100 | return Collections.unmodifiableList(mData); 101 | } 102 | 103 | public Entry get(int index) { 104 | return mData.get(index); 105 | } 106 | 107 | public interface MergeFunction { 108 | F merge(T t1, T t2); 109 | } 110 | 111 | public interface MergeFunction2 { 112 | F merge(T1 t1, T2 t2); 113 | } 114 | 115 | public TimeSeries map(Function f) { 116 | List> newEntries = new ArrayList<>(size()); 117 | for (Entry entry : mData) { 118 | newEntries.add(new Entry<>(f.apply(entry.mT), entry.mInstant)); 119 | } 120 | return new TimeSeries<>(newEntries); 121 | } 122 | 123 | public boolean isAscending() { 124 | return size() <= 1 || get(0).getInstant().isBefore(get(1).mInstant); 125 | } 126 | 127 | public TimeSeries toAscending() { 128 | if (!isAscending()) { 129 | return reverse(); 130 | } 131 | return this; 132 | } 133 | 134 | public TimeSeries toDescending() { 135 | if (isAscending()) { 136 | return reverse(); 137 | } 138 | return this; 139 | } 140 | 141 | public TimeSeries reverse() { 142 | ArrayList> entries = new ArrayList<>(mData); 143 | Collections.reverse(entries); 144 | return new TimeSeries<>(entries); 145 | } 146 | 147 | public TimeSeries lag(int k) { 148 | return lag(k, false, null); 149 | } 150 | 151 | public TimeSeries lag(int k, boolean addEmpty, T emptyVal) { 152 | check(k > 0); 153 | check(mData.size() >= k); 154 | 155 | ArrayList> entries = new ArrayList<>(addEmpty ? mData.size() : mData.size() - k); 156 | if (addEmpty) { 157 | for (int i = 0; i < k; i++) { 158 | entries.add(new Entry<>(emptyVal, mData.get(i).mInstant)); 159 | } 160 | } 161 | 162 | for (int i = k; i < size(); i++) { 163 | entries.add(new Entry<>(mData.get(i - k).getItem(), mData.get(i).getInstant())); 164 | } 165 | 166 | return new TimeSeries<>(entries); 167 | } 168 | 169 | public static TimeSeries merge(TimeSeries t1, TimeSeries t2, MergeFunction2 f) { 170 | check(t1.isAscending()); 171 | check(t2.isAscending()); 172 | 173 | Iterator> i1 = t1.iterator(); 174 | Iterator> i2 = t2.iterator(); 175 | 176 | List> newEntries = new ArrayList<>(); 177 | 178 | while (i1.hasNext() && i2.hasNext()) { 179 | Entry n1 = i1.next(); 180 | Entry n2 = i2.next(); 181 | 182 | while (!n2.mInstant.equals(n1.mInstant)) { 183 | if (n1.mInstant.isBefore(n2.mInstant)) { 184 | if(!i1.hasNext()) { 185 | break; 186 | } 187 | while (i1.hasNext()) { 188 | n1 = i1.next(); 189 | if (!n1.mInstant.isBefore(n2.mInstant)) { 190 | break; 191 | } 192 | } 193 | } else if (n2.mInstant.isBefore(n1.mInstant)) { 194 | while (i2.hasNext()) { 195 | n2 = i2.next(); 196 | if (!n2.mInstant.isBefore(n1.mInstant)) { 197 | break; 198 | } 199 | } 200 | } 201 | } 202 | 203 | if (n2.mInstant.equals(n1.mInstant)) { 204 | newEntries.add(new Entry(f.merge(n1.mT, n2.mT), n1.mInstant)); 205 | } 206 | } 207 | 208 | return new TimeSeries<>(newEntries); 209 | } 210 | 211 | public static TimeSeries merge(TimeSeries t1, TimeSeries t2, MergeFunction f) { 212 | return TimeSeries.merge(t1, t2, f::merge); 213 | } 214 | 215 | @Override public String toString() { 216 | return mData.isEmpty() ? "TimeSeries{empty}" : 217 | "TimeSeries{" + 218 | "from=" + mData.get(0).getInstant() + 219 | ", to=" + mData.get(size() - 1).getInstant() + 220 | ", size=" + mData.size() + 221 | '}'; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/util/HistoricalPriceService.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.util; 2 | 3 | import org.lst.trading.lib.series.DoubleSeries; 4 | import rx.Observable; 5 | 6 | public interface HistoricalPriceService { 7 | Observable getHistoricalAdjustedPrices(String symbol); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/util/Http.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.util; 2 | 3 | import java.io.IOException; 4 | import java.util.function.Consumer; 5 | import org.apache.http.HttpResponse; 6 | import org.apache.http.client.HttpClient; 7 | import org.apache.http.client.methods.HttpGet; 8 | import org.apache.http.impl.client.CloseableHttpClient; 9 | import org.apache.http.impl.client.HttpClients; 10 | import org.apache.http.util.EntityUtils; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import rx.Observable; 14 | import rx.Observable.OnSubscribe; 15 | import rx.Subscriber; 16 | import rx.functions.Func1; 17 | import rx.schedulers.Schedulers; 18 | 19 | public class Http { 20 | private static final Logger log = LoggerFactory.getLogger(Http.class); 21 | 22 | private static CloseableHttpClient client; 23 | 24 | public synchronized static HttpClient getDefaultHttpClient() { 25 | if (client == null) { 26 | client = HttpClients.createDefault(); 27 | } 28 | return client; 29 | } 30 | 31 | public static Observable get(String url, Consumer configureRequest) { 32 | HttpGet request = new HttpGet(url); 33 | configureRequest.accept(request); 34 | 35 | return Observable.create(new OnSubscribe() { 36 | @Override public void call(Subscriber s) { 37 | try { 38 | log.debug("GET {}", url); 39 | s.onNext(getDefaultHttpClient().execute(request)); 40 | s.onCompleted(); 41 | } catch (IOException e) { 42 | s.onError(e); 43 | } 44 | } 45 | }).subscribeOn(Schedulers.io()); 46 | } 47 | 48 | public static Observable get(String url) { 49 | return get(url, x -> { 50 | }); 51 | } 52 | 53 | public static Func1> asString() { 54 | return t -> { 55 | try { 56 | return Observable.just(EntityUtils.toString(t.getEntity())); 57 | } catch (IOException e) { 58 | return Observable.error(e); 59 | } 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/util/Statistics.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.util; 2 | 3 | import org.apache.commons.math3.stat.StatUtils; 4 | 5 | public class Statistics { 6 | public static double[] drawdown(double[] series) { 7 | double max = Double.MIN_VALUE; 8 | double ddPct = Double.MAX_VALUE; 9 | double dd = Double.MAX_VALUE; 10 | 11 | for (double x : series) { 12 | dd = Math.min(x - max, dd); 13 | ddPct = Math.min(x / max - 1, ddPct); 14 | max = Math.max(max, x); 15 | } 16 | 17 | return new double[]{dd, ddPct}; 18 | } 19 | 20 | public static double sharpe(double[] dailyReturns) { 21 | return StatUtils.mean(dailyReturns) / Math.sqrt(StatUtils.variance(dailyReturns)) * Math.sqrt(250); 22 | } 23 | 24 | public static double[] returns(double[] series) { 25 | if (series.length <= 1) { 26 | return new double[0]; 27 | } 28 | 29 | double[] returns = new double[series.length - 1]; 30 | for (int i = 1; i < series.length; i++) { 31 | returns[i - 1] = series[i] / series[i - 1] - 1; 32 | } 33 | 34 | return returns; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/util/Util.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.util; 2 | 3 | 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.nio.file.Paths; 9 | import org.lst.trading.lib.series.MultipleDoubleSeries; 10 | 11 | import static java.util.stream.Collectors.joining; 12 | 13 | public class Util { 14 | 15 | public static Path writeCsv(MultipleDoubleSeries series) { 16 | String data = 17 | "date," + series.getNames().stream().collect(joining(",")) + "\n" + 18 | series.stream().map( 19 | e -> e.getInstant() + "," + e.getItem().stream().map(Object::toString).collect(joining(",")) 20 | ).collect(joining("\n")); 21 | 22 | return writeStringToTempFile(data); 23 | } 24 | 25 | public static Path writeStringToTempFile(String content) { 26 | try { 27 | return writeString(content, Paths.get(File.createTempFile("out-", ".csv").getAbsolutePath())); 28 | } catch (IOException e) { 29 | throw new RuntimeException(e); 30 | } 31 | } 32 | 33 | 34 | public static Path writeString(String content, Path path) { 35 | try { 36 | Files.write(path, content.getBytes()); 37 | return path; 38 | } catch (IOException e) { 39 | throw new RuntimeException(e); 40 | } 41 | } 42 | 43 | public static void check(boolean condition) { 44 | if (!condition) { 45 | throw new RuntimeException(); 46 | } 47 | } 48 | 49 | 50 | public static void check(boolean condition, String message) { 51 | if (!condition) { 52 | throw new RuntimeException(message); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/lib/util/yahoo/YahooFinance.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.lib.util.yahoo; 2 | 3 | import java.io.IOException; 4 | import java.net.URISyntaxException; 5 | import java.net.URL; 6 | import java.sql.Connection; 7 | import java.sql.ResultSet; 8 | import java.sql.SQLException; 9 | import java.sql.Statement; 10 | import java.time.Instant; 11 | import java.time.LocalDate; 12 | import java.time.OffsetDateTime; 13 | import java.time.ZoneOffset; 14 | import java.time.format.DateTimeFormatter; 15 | import java.util.stream.Stream; 16 | import org.lst.trading.lib.csv.CsvReader; 17 | import org.lst.trading.lib.series.DoubleSeries; 18 | import org.lst.trading.lib.util.HistoricalPriceService; 19 | import org.lst.trading.lib.util.Http; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import rx.Observable; 23 | 24 | import static java.lang.String.format; 25 | import static java.nio.file.Files.lines; 26 | import static java.nio.file.Paths.get; 27 | import static org.lst.trading.lib.csv.CsvReader.ParseFunction.doubleColumn; 28 | import static org.lst.trading.lib.csv.CsvReader.ParseFunction.ofColumn; 29 | 30 | public class YahooFinance implements HistoricalPriceService { 31 | public static final String SEP = ","; 32 | public static final CsvReader.ParseFunction DATE_COLUMN = 33 | ofColumn("Date") 34 | .map(s -> LocalDate.from(DateTimeFormatter.ISO_DATE.parse(s)) 35 | .atStartOfDay(ZoneOffset.UTC.normalized()) 36 | .toInstant()); 37 | public static final CsvReader.ParseFunction CLOSE_COLUMN = doubleColumn("Close"); 38 | public static final CsvReader.ParseFunction HIGH_COLUMN = doubleColumn("High"); 39 | public static final CsvReader.ParseFunction LOW_COLUMN = doubleColumn("Low"); 40 | public static final CsvReader.ParseFunction OPEN_COLUMN = doubleColumn("Open"); 41 | public static final CsvReader.ParseFunction ADJ_COLUMN = doubleColumn("Adj Close"); 42 | public static final CsvReader.ParseFunction VOLUME_COLUMN = doubleColumn("Volume"); 43 | public static final OffsetDateTime DEFAULT_FROM = 44 | OffsetDateTime.of(2010, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); 45 | private Connection connection = null; 46 | 47 | public YahooFinance(){} 48 | public YahooFinance(Connection connection) { 49 | this.connection = connection; 50 | } 51 | 52 | private static final Logger log = LoggerFactory.getLogger(YahooFinance.class); 53 | 54 | @Override public Observable getHistoricalAdjustedPrices(String symbol) { 55 | return getHistoricalAdjustedPrices(symbol, DEFAULT_FROM.toInstant()); 56 | } 57 | 58 | public Observable getHistoricalAdjustedPrices(String symbol, Instant from) { 59 | return getHistoricalAdjustedPrices(symbol, from, Instant.now()); 60 | } 61 | 62 | public Observable getHistoricalAdjustedPrices(String symbol, Instant from, Instant to) { 63 | return getHistoricalPricesCsv(symbol, from, to).map(csv -> csvToDoubleSeries(csv, symbol)); 64 | } 65 | 66 | private static Observable getHistoricalPricesCsv(String symbol, Instant from, Instant to) { 67 | return Http.get(createHistoricalPricesUrl(symbol, from, to)) 68 | .flatMap(Http.asString()); 69 | } 70 | 71 | private static DoubleSeries csvToDoubleSeries(String csv, String symbol) { 72 | Stream lines = Stream.of(csv.split("\n")); 73 | DoubleSeries prices = CsvReader.parse(lines, SEP, DATE_COLUMN, ADJ_COLUMN); 74 | prices.setName(symbol); 75 | prices = prices.toAscending(); 76 | return prices; 77 | } 78 | 79 | public DoubleSeries readCsvToDoubleSeries(String csvFilePath, String symbol) 80 | throws IOException { 81 | Stream lines = lines(get(csvFilePath)); 82 | DoubleSeries prices = CsvReader.parse(lines, SEP, DATE_COLUMN, ADJ_COLUMN); 83 | prices.setName(symbol); 84 | prices = prices.toAscending(); 85 | return prices; 86 | } 87 | 88 | public DoubleSeries readCsvToDoubleSeriesFromResource(String csvResourcePath, String symbol) 89 | throws IOException, URISyntaxException { 90 | URL resourceUrl = getResource(csvResourcePath); 91 | Stream lines = lines(get(resourceUrl.toURI())); 92 | DoubleSeries prices = CsvReader.parse(lines, SEP, DATE_COLUMN, ADJ_COLUMN); 93 | prices.setName(symbol); 94 | prices = prices.toAscending(); 95 | return prices; 96 | } 97 | 98 | public DoubleSeries readSeriesFromDb(String symbol) throws SQLException { 99 | if(connection == null) { 100 | return new DoubleSeries(symbol); 101 | } 102 | Statement stmt = connection.createStatement(); 103 | ResultSet rs = stmt.executeQuery(String.format("SELECT * FROM quotes WHERE symbol='%s'", symbol)); 104 | DoubleSeries doubleSeries = new DoubleSeries(symbol); 105 | while(rs.next()) { 106 | doubleSeries.add(rs.getDouble(3), Instant.ofEpochMilli(rs.getTime(4).getTime())); 107 | } 108 | rs.close(); 109 | stmt.close(); 110 | return doubleSeries; 111 | } 112 | 113 | private static String createHistoricalPricesUrl(String symbol, Instant from, Instant to) { 114 | return format("https://ichart.yahoo.com/table.csv?s=%s&%s&%s&g=d&ignore=.csv", symbol, 115 | toYahooQueryDate(from, "abc"), toYahooQueryDate(to, "def")); 116 | } 117 | 118 | private static String toYahooQueryDate(Instant instant, String names) { 119 | OffsetDateTime time = instant.atOffset(ZoneOffset.UTC); 120 | String[] strings = names.split(""); 121 | return format("%s=%d&%s=%d&%s=%d", strings[0], time.getMonthValue() - 1, strings[1], time.getDayOfMonth(), strings[2], time.getYear()); 122 | } 123 | 124 | private URL getResource(String resource){ 125 | URL url ; 126 | 127 | //Try with the Thread Context Loader. 128 | ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 129 | if(classLoader != null){ 130 | url = classLoader.getResource(resource); 131 | if(url != null){ 132 | return url; 133 | } 134 | } 135 | 136 | //Let's now try with the classloader that loaded this class. 137 | classLoader = System.class.getClassLoader(); 138 | if(classLoader != null){ 139 | url = classLoader.getResource(resource); 140 | if(url != null){ 141 | return url; 142 | } 143 | } 144 | 145 | //Last ditch attempt. Get the resource from the classpath. 146 | return ClassLoader.getSystemResource(resource); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/main/BacktestMain.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.main; 2 | 3 | public class BacktestMain { 4 | // public static void main(String[] args) throws URISyntaxException, IOException { 5 | // String x = "EWA"; 6 | // String y = "EWC"; 7 | // 8 | // // initialize the trading strategy 9 | // TradingStrategy strategy = new CointegrationTradingStrategy(x, y); 10 | // 11 | // DoubleSeries ewa = new DoubleSeries("EWA"); 12 | // DoubleSeries ewc = new DoubleSeries("EWC"); 13 | // 14 | // AtomicInteger i = new AtomicInteger(); 15 | // i.set(0); 16 | // 17 | // try (Stream stream = 18 | // lines(get("/Users/beastie/Downloads/EWA.csv"))) { 19 | // 20 | // stream.forEachOrdered(s -> ewa.add(Double.parseDouble(s), Instant.ofEpochSecond(i.getAndAdd 21 | // (86400)))); 22 | // 23 | // } catch (IOException e) { 24 | // e.printStackTrace(); 25 | // } 26 | // 27 | // i.set(0); 28 | // try (Stream stream = 29 | // lines(get("/Users/beastie/Downloads/EWC.csv"))) { 30 | // 31 | // stream.forEachOrdered(s -> ewc.add(Double.parseDouble(s), Instant.ofEpochSecond(i.getAndAdd 32 | // (86400)))); 33 | // 34 | // } catch (IOException e) { 35 | // e.printStackTrace(); 36 | // } 37 | // 38 | // MultipleDoubleSeries priceSeries = new MultipleDoubleSeries(ewa, ewc); 39 | // 40 | // // initialize the backtesting engine 41 | // int deposit = 15000; 42 | // BackTest backTest = new BackTest(deposit, priceSeries); 43 | // backTest.setLeverage(4); 44 | // 45 | // 46 | // // do the backtest 47 | //// BackTest.Result result = backTest.run(strategy); 48 | // 49 | // // show results 50 | // StringBuilder orders = new StringBuilder(); 51 | // orders.append("id,amount,side,instrument,from,to,open,close,pl\n"); 52 | // for (ClosedOrder order : result.getOrders()) { 53 | // orders.append(format(Locale.US, "%d,%d,%s,%s,%s,%s,%f,%f,%f\n", order.getId(), Math.abs(order.getAmount()), order.isLong() ? "Buy" : "Sell", order.getInstrument(), order.getOpenInstant(), order.getCloseInstant(), order.getOpenPrice(), order.getClosePrice(), order.getPl())); 54 | // } 55 | // System.out.print(orders); 56 | // 57 | // int days = priceSeries.size(); 58 | // 59 | // System.out.println(); 60 | // System.out.println("Backtest result of " + strategy.getClass() + ": " + strategy); 61 | // System.out.println("Prices: " + priceSeries); 62 | // System.out.println(format(Locale.US, "Simulated %d days, Initial deposit %d, Leverage %f", days, deposit, backTest.getLeverage())); 63 | // System.out.println(format(Locale.US, "Commissions = %f", result.getCommissions())); 64 | // System.out.println(format(Locale.US, "P/L = %.2f, Final value = %.2f, Result = %.2f%%, Annualized = %.2f%%, Sharpe (rf=0%%) = %.2f", result.getPl(), result.getFinalValue(), result.getReturn() * 100, result.getReturn() / (days / 251.) * 100, result.getSharpe())); 65 | // 66 | // System.out.println("Orders: " + Util.writeStringToTempFile(orders.toString())); 67 | // System.out.println("Statistics: " + Util.writeCsv(new MultipleDoubleSeries(result.getPlHistory(), result.getMarginHistory()))); 68 | // } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/main/strategy/AbstractTradingStrategy.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.main.strategy; 2 | 3 | import org.lst.trading.lib.model.TradingStrategy; 4 | 5 | public abstract class AbstractTradingStrategy implements TradingStrategy { 6 | double mWeight = 1; 7 | 8 | public double getWeight() { 9 | return mWeight; 10 | } 11 | 12 | public void setWeight(double weight) { 13 | mWeight = weight; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/main/strategy/BuyAndHold.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.main.strategy; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import org.lst.trading.lib.model.Order; 6 | import org.lst.trading.lib.model.TradingContext; 7 | import org.lst.trading.lib.model.TradingStrategy; 8 | 9 | public class BuyAndHold implements TradingStrategy { 10 | Map mOrders; 11 | TradingContext mContext; 12 | 13 | @Override public void onStart(TradingContext context) { 14 | mContext = context; 15 | } 16 | 17 | @Override public void onTick() { 18 | if (mOrders == null) { 19 | mOrders = new HashMap<>(); 20 | mContext.getInstruments().stream().forEach(instrument -> mOrders.put(instrument, mContext.order(instrument, true, 1))); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/main/strategy/MultipleTradingStrategy.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.main.strategy; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import org.lst.trading.lib.model.TradingContext; 6 | import org.lst.trading.lib.model.TradingStrategy; 7 | 8 | public class MultipleTradingStrategy implements TradingStrategy { 9 | public static MultipleTradingStrategy of(TradingStrategy... strategies) { 10 | MultipleTradingStrategy strategy = new MultipleTradingStrategy(); 11 | for (TradingStrategy s : strategies) { 12 | strategy.add(s); 13 | } 14 | return strategy; 15 | } 16 | 17 | List mStrategies = new ArrayList<>(); 18 | 19 | public boolean add(TradingStrategy strategy) { 20 | return mStrategies.add(strategy); 21 | } 22 | 23 | public int size() { 24 | return mStrategies.size(); 25 | } 26 | 27 | @Override public void onStart(TradingContext context) { 28 | mStrategies.forEach(t -> t.onStart(context)); 29 | } 30 | 31 | @Override public void onTick() { 32 | mStrategies.forEach(TradingStrategy::onTick); 33 | } 34 | 35 | @Override public void onEnd() { 36 | mStrategies.forEach(TradingStrategy::onEnd); 37 | } 38 | 39 | @Override public String toString() { 40 | return "MultipleStrategy{" + 41 | "mStrategies=" + mStrategies + 42 | '}'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/main/strategy/kalman/Cointegration.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.main.strategy.kalman; 2 | 3 | import org.la4j.Matrix; 4 | 5 | public class Cointegration { 6 | double mDelta; 7 | double mR; 8 | KalmanFilter mFilter; 9 | int mNobs = 2; 10 | 11 | public Cointegration(double delta, double r) { 12 | mDelta = delta; 13 | mR = r; 14 | 15 | Matrix vw = Matrix.identity(mNobs).multiply(mDelta / (1 - delta)); 16 | Matrix a = Matrix.identity(mNobs); 17 | 18 | Matrix x = Matrix.zero(mNobs, 1); 19 | 20 | mFilter = new KalmanFilter(mNobs, 1); 21 | mFilter.setUpdateMatrix(a); 22 | mFilter.setState(x); 23 | mFilter.setStateCovariance(Matrix.zero(mNobs, mNobs)); 24 | mFilter.setUpdateCovariance(vw); 25 | mFilter.setMeasurementCovariance(Matrix.constant(1, 1, r)); 26 | } 27 | 28 | public void step(double x, double y) { 29 | mFilter.setExtractionMatrix(Matrix.from1DArray(1, 2, new double[]{1, x})); 30 | mFilter.step(Matrix.constant(1, 1, y)); 31 | } 32 | 33 | public double getAlpha() { 34 | return mFilter.getState().getRow(0).get(0); 35 | } 36 | 37 | public double getBeta() { 38 | return mFilter.getState().getRow(1).get(0); 39 | } 40 | 41 | public double getVariance() { 42 | return mFilter.getInnovationCovariance().get(0, 0); 43 | } 44 | 45 | public double getError() { 46 | return mFilter.getInnovation().get(0, 0); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/main/strategy/kalman/CointegrationTradingStrategy.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.main.strategy.kalman; 2 | 3 | import org.apache.commons.math3.stat.StatUtils; 4 | import org.lst.trading.lib.model.Order; 5 | import org.lst.trading.lib.model.TradingContext; 6 | import org.lst.trading.lib.series.DoubleSeries; 7 | import org.lst.trading.lib.series.MultipleDoubleSeries; 8 | import org.lst.trading.lib.series.TimeSeries; 9 | import org.lst.trading.lib.util.Util; 10 | import org.lst.trading.main.strategy.AbstractTradingStrategy; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | public class CointegrationTradingStrategy extends AbstractTradingStrategy { 15 | private static Logger log = LoggerFactory.getLogger(CointegrationTradingStrategy.class); 16 | 17 | boolean mReinvest = false; 18 | 19 | String mX, mY; 20 | TradingContext mContext; 21 | Cointegration mCoint; 22 | 23 | DoubleSeries mAlpha; 24 | DoubleSeries mBeta; 25 | DoubleSeries mXs; 26 | DoubleSeries mYs; 27 | DoubleSeries mError; 28 | DoubleSeries mVariance; 29 | DoubleSeries mModel; 30 | 31 | Order mXOrder; 32 | Order mYOrder; 33 | 34 | public CointegrationTradingStrategy(String x, String y) { 35 | this(1, x, y); 36 | } 37 | 38 | public CointegrationTradingStrategy(double weight, String x, String y) { 39 | setWeight(weight); 40 | mX = x; 41 | mY = y; 42 | } 43 | 44 | @Override public void onStart(TradingContext context) { 45 | mContext = context; 46 | mCoint = new Cointegration(1e-4, 1e-3); 47 | mAlpha = new DoubleSeries("alpha"); 48 | mBeta = new DoubleSeries("beta"); 49 | mXs = new DoubleSeries("x"); 50 | mYs = new DoubleSeries("y"); 51 | mError = new DoubleSeries("error"); 52 | mVariance = new DoubleSeries("variance"); 53 | mModel = new DoubleSeries("model"); 54 | } 55 | 56 | @Override public void onTick() { 57 | double x = mContext.getLastPrice(mX); 58 | double y = mContext.getLastPrice(mY); 59 | double alpha = mCoint.getAlpha(); 60 | double beta = mCoint.getBeta(); 61 | 62 | mCoint.step(x, y); 63 | mAlpha.add(alpha, mContext.getTime()); 64 | mBeta.add(beta, mContext.getTime()); 65 | mXs.add(x, mContext.getTime()); 66 | mYs.add(y, mContext.getTime()); 67 | mError.add(mCoint.getError(), mContext.getTime()); 68 | mVariance.add(mCoint.getVariance(), mContext.getTime()); 69 | 70 | double error = mCoint.getError(); 71 | 72 | mModel.add(beta * x + alpha, mContext.getTime()); 73 | 74 | if (mError.size() > 30) { 75 | double[] lastValues = mError.reversedStream().mapToDouble(TimeSeries.Entry::getItem).limit(15).toArray(); 76 | double sd = Math.sqrt(StatUtils.variance(lastValues)); 77 | 78 | if (mYOrder == null && Math.abs(error) > sd) { 79 | double value = mReinvest ? mContext.getNetValue() : mContext.getInitialFunds(); 80 | double baseAmount = (value * getWeight() * 0.5 * Math.min(4, mContext.getLeverage())) / (y + beta * x); 81 | 82 | if (beta > 0 && baseAmount * beta >= 1) { 83 | mYOrder = mContext.order(mY, error < 0, (int) baseAmount); 84 | mXOrder = mContext.order(mX, error > 0, (int) (baseAmount * beta)); 85 | } 86 | //log.debug("Order: baseAmount={}, residual={}, sd={}, beta={}", baseAmount, residual, sd, beta); 87 | } else if (mYOrder != null) { 88 | if (mYOrder.isLong() && error > 0 || !mYOrder.isLong() && error < 0) { 89 | mContext.close(mYOrder); 90 | mContext.close(mXOrder); 91 | 92 | mYOrder = null; 93 | mXOrder = null; 94 | } 95 | } 96 | } 97 | } 98 | 99 | @Override public void onEnd() { 100 | log.debug("Kalman filter statistics: " + Util.writeCsv(new MultipleDoubleSeries(mXs, mYs, mAlpha, mBeta, mError, mVariance, mModel))); 101 | } 102 | 103 | @Override public String toString() { 104 | return "CointegrationStrategy{" + 105 | "mY='" + mY + '\'' + 106 | ", mX='" + mX + '\'' + 107 | '}'; 108 | } 109 | 110 | public DoubleSeries getAlpha() { 111 | return mAlpha; 112 | } 113 | 114 | public DoubleSeries getBeta() { 115 | return mBeta; 116 | } 117 | 118 | public DoubleSeries getXs() { 119 | return mXs; 120 | } 121 | 122 | public DoubleSeries getYs() { 123 | return mYs; 124 | } 125 | 126 | public DoubleSeries getError() { 127 | return mError; 128 | } 129 | 130 | public DoubleSeries getVariance() { 131 | return mVariance; 132 | } 133 | 134 | public DoubleSeries getModel() { 135 | return mModel; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/org/lst/trading/main/strategy/kalman/KalmanFilter.java: -------------------------------------------------------------------------------- 1 | package org.lst.trading.main.strategy.kalman; 2 | 3 | 4 | import org.la4j.LinearAlgebra; 5 | import org.la4j.Matrix; 6 | import org.la4j.matrix.DenseMatrix; 7 | 8 | /** 9 | * n: Number of states 10 | * m: Number of sensors 11 | *

12 | * https://www.udacity.com/wiki/cs373/kalman-filter-matrices 13 | *

14 | * Steps: 15 | *

16 | * PREDICT X' 17 | *

18 | * First, we predict our next x: 19 | *

20 | * x' = Fx + u 21 | *

22 | * UPDATE P' 23 | *

24 | * We also update the covariance matrix according to the next prediction: 25 | *

26 | * P' = FP(transp F) 27 | *

28 | * UPDATE Y 29 | *

30 | * y becomes the difference between the move and what we expected: 31 | *

32 | * y = z - Hx 33 | *

34 | * UPDATE S 35 | *

36 | * S is the covariance of the move, adding up the covariance in move space of the position and the covariance of the measurement: 37 | *

38 | * S = HP(transp H) + R 39 | *

40 | * CALCULATE K 41 | *

42 | * Now I start to wave my hands. I assume this next matrix is called K because this is the work of the Kalman filter. Whatever is happening here, it doesn't depend on u or z. We're computing how much of the difference between the observed move and the expected move to add to x. 43 | *

44 | * K = P (transp H) (inv S) 45 | *

46 | * UPDATE X' 47 | *

48 | * We update x: 49 | *

50 | * x' = x + Ky 51 | *

52 | * SUBTRACT P 53 | *

54 | * And we subtract some uncertainty from P, again not depending on u or z: 55 | *

56 | * P' = P - P(transp H)(inv S)HP 57 | */ 58 | public class KalmanFilter { 59 | private final int mStateCount; // n 60 | private final int mSensorCount; // m 61 | // state 62 | 63 | /** 64 | * stateCount x 1 65 | */ 66 | private Matrix mState; // x, state estimate 67 | 68 | /** 69 | * stateCount x stateCount 70 | *

71 | * Symmetric. 72 | * Down the diagonal of P, we find the variances of the elements of x. 73 | * On the off diagonals, at P[i][j], we find the covariances of x[i] with x[j]. 74 | */ 75 | private Matrix mStateCovariance; // Covariance matrix of x, process noise (w) 76 | 77 | // predict 78 | 79 | /** 80 | * stateCount x stateCount 81 | *

82 | * Kalman filters model a system over time. 83 | * After each tick of time, we predict what the values of x are, and then we measure and do some computation. 84 | * F is used in the update step. Here's how it works: For each value in x, we write an equation to update that value, 85 | * a linear equation in all the variables in x. Then we can just read off the coefficients to make the matrix. 86 | */ 87 | private Matrix mUpdateMatrix; // F, State transition matrix. 88 | 89 | /** 90 | * stateCount x stateCount 91 | *

92 | * Error in the process, after each update this uncertainty is added. 93 | */ 94 | private Matrix mUpdateCovariance; // Q, Estimated error in process. 95 | 96 | /** 97 | * stateCount x 1 98 | *

99 | * The control input, the move vector. 100 | * It's the change to x that we cause, or that we know is happening. 101 | * Since we add it to x, it has dimension n. When the filter updates, it adds u to the new x. 102 | *

103 | * External moves to the system. 104 | */ 105 | private Matrix mMoveVector; // u, Control vector 106 | 107 | 108 | // measurement 109 | 110 | /** 111 | * sensorCount x 1 112 | *

113 | * z: Measurement Vector, It's the outputs from the sensors. 114 | */ 115 | private Matrix mMeasurement; 116 | 117 | /** 118 | * sensorCount x sensorCount 119 | *

120 | * R, the variances and covariances of our sensor measurements. 121 | *

122 | * The Kalman filter algorithm does not change R, because the process can't change our belief about the 123 | * accuracy of our sensors--that's a property of the sensors themselves. 124 | * We know the variance of our sensor either by testing it, or by reading the documentation that came with it, 125 | * or something like that. Note that the covariances here are the covariances of the measurement error. 126 | * A positive number means that if the first sensor is erroneously low, the second tends to be erroneously low, 127 | * or if the first reads high, the second tends to read high; it doesn't mean that if the first sensor reports a 128 | * high number the second will also report a high number 129 | */ 130 | private Matrix mMeasurementCovariance; // R, Covariance matrix of the measurement vector z 131 | 132 | /** 133 | * sensorCount x stateCount 134 | *

135 | * The matrix H tells us what sensor readings we'd get if x were the true state of affairs and our sensors were perfect. 136 | * It's the matrix we use to extract the measurement from the data. 137 | * If we multiply H times a perfectly correct x, we get a perfectly correct z. 138 | */ 139 | private Matrix mExtractionMatrix; // H, Observation matrix. 140 | 141 | // no inputs 142 | private Matrix mInnovation; 143 | private Matrix mInnovationCovariance; 144 | 145 | public KalmanFilter(int stateCount, int sensorCount) { 146 | mStateCount = stateCount; 147 | mSensorCount = sensorCount; 148 | mMoveVector = Matrix.zero(stateCount, 1); 149 | } 150 | 151 | private void step() { 152 | // prediction 153 | Matrix predictedState = mUpdateMatrix.multiply(mState).add(mMoveVector); 154 | Matrix predictedStateCovariance = mUpdateMatrix.multiply(mStateCovariance).multiply(mUpdateMatrix.transpose()).add(mUpdateCovariance); 155 | 156 | // observation 157 | mInnovation = mMeasurement.subtract(mExtractionMatrix.multiply(predictedState)); 158 | mInnovationCovariance = mExtractionMatrix.multiply(predictedStateCovariance).multiply(mExtractionMatrix.transpose()).add(mMeasurementCovariance); 159 | 160 | // update 161 | Matrix kalmanGain = predictedStateCovariance.multiply(mExtractionMatrix.transpose()).multiply(mInnovationCovariance.withInverter(LinearAlgebra.InverterFactory.SMART).inverse()); 162 | mState = predictedState.add(kalmanGain.multiply(mInnovation)); 163 | 164 | int nRow = mStateCovariance.rows(); 165 | mStateCovariance = DenseMatrix.identity(nRow).subtract(kalmanGain.multiply(mExtractionMatrix)).multiply(predictedStateCovariance); 166 | } 167 | 168 | public void step(Matrix measurement, Matrix move) { 169 | mMeasurement = measurement; 170 | mMoveVector = move; 171 | step(); 172 | } 173 | 174 | public void step(Matrix measurement) { 175 | mMeasurement = measurement; 176 | step(); 177 | } 178 | 179 | 180 | public Matrix getState() { 181 | return mState; 182 | } 183 | 184 | public Matrix getStateCovariance() { 185 | return mStateCovariance; 186 | } 187 | 188 | public Matrix getInnovation() { 189 | return mInnovation; 190 | } 191 | 192 | public Matrix getInnovationCovariance() { 193 | return mInnovationCovariance; 194 | } 195 | 196 | public void setState(Matrix state) { 197 | mState = state; 198 | } 199 | 200 | public void setStateCovariance(Matrix stateCovariance) { 201 | mStateCovariance = stateCovariance; 202 | } 203 | 204 | public void setUpdateMatrix(Matrix updateMatrix) { 205 | mUpdateMatrix = updateMatrix; 206 | } 207 | 208 | public void setUpdateCovariance(Matrix updateCovariance) { 209 | mUpdateCovariance = updateCovariance; 210 | } 211 | 212 | public void setMeasurementCovariance(Matrix measurementCovariance) { 213 | mMeasurementCovariance = measurementCovariance; 214 | } 215 | 216 | public void setExtractionMatrix(Matrix h) { 217 | this.mExtractionMatrix = h; 218 | } 219 | 220 | public Matrix getUpdateMatrix() { 221 | return mUpdateMatrix; 222 | } 223 | 224 | public Matrix getUpdateCovariance() { 225 | return mUpdateCovariance; 226 | } 227 | 228 | public Matrix getMeasurementCovariance() { 229 | return mMeasurementCovariance; 230 | } 231 | 232 | public Matrix getExtractionMatrix() { 233 | return mExtractionMatrix; 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/test/java/io/codera/quant/config/GuiceJUnit4Runner.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.config; 2 | 3 | import com.google.inject.Guice; 4 | import org.junit.runners.BlockJUnit4ClassRunner; 5 | import org.junit.runners.model.InitializationError; 6 | 7 | public class GuiceJUnit4Runner extends BlockJUnit4ClassRunner { 8 | 9 | public GuiceJUnit4Runner(Class klass) throws InitializationError { 10 | super(klass); 11 | } 12 | 13 | @Override 14 | public Object createTest() throws Exception { 15 | Object object = super.createTest(); 16 | Guice.createInjector(new TestConfig()).injectMembers(object); 17 | return object; 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /src/test/java/io/codera/quant/config/TestConfig.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.config; 2 | 3 | import com.google.inject.AbstractModule; 4 | import com.google.inject.Provides; 5 | 6 | import com.ib.client.OrderType; 7 | import com.ib.controller.ApiController; 8 | import io.codera.quant.context.IbTradingContext; 9 | import io.codera.quant.context.TradingContext; 10 | import io.codera.quant.strategy.IbPerMinuteStrategyRunner; 11 | import io.codera.quant.strategy.StrategyRunner; 12 | import java.sql.SQLException; 13 | 14 | /** 15 | * Test configuration 16 | */ 17 | public class TestConfig extends AbstractModule { 18 | 19 | private static final String HOST = "localhost"; 20 | private static final int PORT = 7497; 21 | 22 | @Override 23 | protected void configure() { 24 | bind(StrategyRunner.class).to(IbPerMinuteStrategyRunner.class); 25 | } 26 | 27 | @Provides 28 | ApiController apiController() { 29 | ApiController controller = 30 | new ApiController(new IbConnectionHandler(), valueOf -> { 31 | }, valueOf -> {}); 32 | controller.connect(HOST, PORT, 0, null); 33 | return controller; 34 | } 35 | 36 | @Provides 37 | TradingContext tradingContext(ApiController controller) throws SQLException, ClassNotFoundException { 38 | return new IbTradingContext( 39 | controller, 40 | new ContractBuilder(), 41 | OrderType.MKT, 42 | 2 43 | ); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/io/codera/quant/strategy/meanrevertion/ZScoreTest.java: -------------------------------------------------------------------------------- 1 | package io.codera.quant.strategy.meanrevertion; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import com.google.inject.Inject; 5 | 6 | import io.codera.quant.config.GuiceJUnit4Runner; 7 | import io.codera.quant.util.MathUtil; 8 | import io.codera.quant.context.TradingContext; 9 | import java.io.IOException; 10 | import java.net.URISyntaxException; 11 | import java.util.List; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.lst.trading.lib.series.DoubleSeries; 15 | import org.lst.trading.lib.util.yahoo.YahooFinance; 16 | 17 | import static org.junit.Assert.assertEquals; 18 | 19 | 20 | /** 21 | * Tests for {@link ZScore} 22 | */ 23 | @RunWith(GuiceJUnit4Runner.class) 24 | public class ZScoreTest { 25 | @Inject 26 | private TradingContext tradingContext; 27 | private static final List SYMBOLS = ImmutableList.of("GLD", "USO"); 28 | private static final int LOOKBACK = 20; 29 | private static final int MINUTES_OF_HISTORY = LOOKBACK * 2 - 1; 30 | 31 | // This test requires a working IB TWS. 32 | // Test returns inconsistent result, due to probably a bug in historical data retrieval timing 33 | @Test 34 | public void getTest() throws Exception { 35 | for(String symbol : SYMBOLS) { 36 | tradingContext.addContract(symbol); 37 | } 38 | DoubleSeries firstSymbolHistory = 39 | tradingContext.getHistoryInMinutes(SYMBOLS.get(0), MINUTES_OF_HISTORY); 40 | DoubleSeries secondSymbolHistory = 41 | tradingContext.getHistoryInMinutes(SYMBOLS.get(1), MINUTES_OF_HISTORY); 42 | 43 | ZScore zScore = 44 | new ZScore(firstSymbolHistory.toArray(), secondSymbolHistory.toArray(), LOOKBACK, new 45 | MathUtil()); 46 | System.out.println(zScore.get(114.7, 10.30)); 47 | } 48 | 49 | @Test 50 | public void getTestUnit() throws IOException, URISyntaxException { 51 | YahooFinance finance = new YahooFinance(); 52 | DoubleSeries gld = 53 | finance.readCsvToDoubleSeriesFromResource("GLD.csv", SYMBOLS.get(0)); 54 | 55 | DoubleSeries uso = 56 | finance.readCsvToDoubleSeriesFromResource("USO.csv", SYMBOLS.get(1)); 57 | ZScore zScore = new ZScore(gld.toArray(), uso.toArray(), LOOKBACK, new MathUtil()); 58 | assertEquals("Failed", -1.0102216127916113, zScore.get(58.33, 66.35), 0); 59 | assertEquals("Failed", -0.9692409006953596, zScore.get(57.73, 67), 0); 60 | assertEquals("Failed", -0.9618287583543594, zScore.get(57.99, 66.89), 0); 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /src/test/resources/GLD.csv: -------------------------------------------------------------------------------- 1 | Date, Adj Close,,,,, 2 | 2012-03-26,63.65,,,,, 3 | 2012-03-27,62.96,,,,, 4 | 2012-03-28,65.09,,,,, 5 | 2012-03-29,65.16,,,,, 6 | 2012-03-30,66.55,,,,, 7 | 2012-03-31,66.46,,,,, 8 | 2012-04-01,67.48,,,,, 9 | 2012-04-02,67.99,,,,, 10 | 2012-04-03,67.56,,,,, 11 | 2012-04-04,69.68,,,,, 12 | 2012-04-05,70.38,,,,, 13 | 2012-04-06,71.03,,,,, 14 | 2012-04-07,71.12,,,,, 15 | 2012-04-08,67.41,,,,, 16 | 2012-04-09,68.61,,,,, 17 | 2012-04-10,68.15,,,,, 18 | 2012-04-11,67.46,,,,, 19 | 2012-04-12,65.58,,,,, 20 | 2012-04-13,65.3,,,,, 21 | 2012-04-14,66.38,,,,, 22 | 2012-04-15,64.06,,,,, 23 | 2012-04-16,64.7,,,,, 24 | 2012-04-17,65.1,,,,, 25 | 2012-04-18,65.11,,,,, 26 | 2012-04-19,64.23,,,,, 27 | 2012-04-20,62.56,,,,, 28 | 2012-04-21,63.5,,,,, 29 | 2012-04-22,63.29,,,,, 30 | 2012-04-23,62.55,,,,, 31 | 2012-04-24,62.28,,,,, 32 | 2012-04-25,60.91,,,,, 33 | 2012-04-26,60.45,,,,, 34 | 2012-04-27,60.03,,,,, 35 | 2012-04-28,55.92,,,,, 36 | 2012-04-29,55.62,,,,, 37 | 2012-04-30,57.32,,,,, 38 | 2012-05-01,57.68,,,,, 39 | 2012-05-02,56.36,,,,, 40 | 2012-05-03,57.3,,,,, -------------------------------------------------------------------------------- /src/test/resources/USO.csv: -------------------------------------------------------------------------------- 1 | Date, Adj Close,,,,, 2 | 2012-03-26,69.54,,,,, 3 | 2012-03-27,68.7,,,,, 4 | 2012-03-28,69.62,,,,, 5 | 2012-03-29,71.5,,,,, 6 | 2012-03-30,72.34,,,,, 7 | 2012-03-31,70.22,,,,, 8 | 2012-04-01,68.32,,,,, 9 | 2012-04-02,68,,,,, 10 | 2012-04-03,67.88,,,,, 11 | 2012-04-04,68.4,,,,, 12 | 2012-04-05,69.77,,,,, 13 | 2012-04-06,70.3,,,,, 14 | 2012-04-07,69.11,,,,, 15 | 2012-04-08,66.63,,,,, 16 | 2012-04-09,66.7,,,,, 17 | 2012-04-10,65.7,,,,, 18 | 2012-04-11,66.74,,,,, 19 | 2012-04-12,65.57,,,,, 20 | 2012-04-13,66.28,,,,, 21 | 2012-04-14,67.7700000000000,,,,, 22 | 2012-04-15,66.1000000000000,,,,, 23 | 2012-04-16,67.8000000000000,,,,, 24 | 2012-04-17,67.7500000000000,,,,, 25 | 2012-04-18,68.3200000000000,,,,, 26 | 2012-04-19,67.7300000000000,,,,, 27 | 2012-04-20,66.8200000000000,,,,, 28 | 2012-04-21,68.9000000000000,,,,, 29 | 2012-04-22,68.7400000000000,,,,, 30 | 2012-04-23,68.7600000000000,,,,, 31 | 2012-04-24,67.2800000000000,,,,, 32 | 2012-04-25,67.1200000000000,,,,, 33 | 2012-04-26,68.0700000000000,,,,, 34 | 2012-04-27,67.0800000000000,,,,, 35 | 2012-04-28,65.1800000000000,,,,, 36 | 2012-04-29,65.7900000000000,,,,, 37 | 2012-04-30,66.1600000000000,,,,, 38 | 2012-05-01,66.2500000000000,,,,, 39 | 2012-05-02,65.5400000000000,,,,, 40 | 2012-05-03,65.2900000000000,,,,, --------------------------------------------------------------------------------