├── .gitignore ├── README.md ├── assets ├── FIXToMatchingEngineFlows0MQ.png ├── basic.jpg └── complex.jpg ├── docs ├── Fix to Matching Cluster Architecture.md └── SettingUpDevEnvironment.md ├── libs ├── jzmq │ └── jzmq │ │ └── 3.3.0 │ │ ├── jzmq-3.3.0-sources.jar │ │ ├── jzmq-3.3.0-sources.jar.md5 │ │ ├── jzmq-3.3.0-sources.jar.sha1 │ │ ├── jzmq-3.3.0.jar │ │ ├── jzmq-3.3.0.jar.md5 │ │ ├── jzmq-3.3.0.jar.sha1 │ │ ├── jzmq-3.3.0.pom │ │ ├── jzmq-3.3.0.pom.md5 │ │ └── jzmq-3.3.0.pom.sha1 ├── org │ └── quickfixj │ │ ├── quickfixj-core │ │ ├── 1.5.2 │ │ │ ├── quickfixj-core-1.5.2-sources.jar │ │ │ ├── quickfixj-core-1.5.2.jar │ │ │ └── quickfixj-core-1.5.2.pom │ │ └── maven-metadata-local.xml │ │ └── quickfixj-msg-fix42 │ │ ├── 1.5.2 │ │ ├── quickfixj-msg-fix42-1.5.2.jar │ │ └── quickfixj-msg-fix42-1.5.2.pom │ │ └── maven-metadata-local.xml └── reactive4java │ └── reactive4java │ ├── 0.96.2 │ ├── _maven.repositories │ ├── reactive4java-0.96.2.jar │ └── reactive4java-0.96.2.pom │ └── maven-metadata-local.xml ├── pom.xml ├── script └── rt-linux │ ├── .config │ ├── build-rt-kernel │ ├── build-utils │ └── install-rt-kernel └── src ├── main ├── java │ └── com │ │ └── euronextclone │ │ ├── IndicativeMatchPriceCalculator.java │ │ ├── MatchingUnit.java │ │ ├── Order.java │ │ ├── OrderBook.java │ │ ├── OrderEntry.java │ │ ├── OrderSide.java │ │ ├── OrderType.java │ │ ├── Trade.java │ │ ├── TradingMode.java │ │ ├── TradingPhase.java │ │ ├── engine │ │ ├── MatchingEngine.java │ │ └── ValueEvent.java │ │ ├── fix │ │ ├── FixAdapter.java │ │ ├── client │ │ │ ├── FixClient.java │ │ │ ├── FixClientApp.java │ │ │ ├── OrderBuilder.java │ │ │ └── commands │ │ │ │ ├── ClientCommand.java │ │ │ │ ├── PlaceLimitOrder.java │ │ │ │ └── PlaceMarketOrder.java │ │ └── server │ │ │ ├── FixServer.java │ │ │ └── FixServerApp.java │ │ ├── framework │ │ ├── Action.java │ │ └── Factory.java │ │ ├── messaging │ │ ├── Publisher.java │ │ └── Subscriber.java │ │ └── ordertypes │ │ ├── Limit.java │ │ ├── Market.java │ │ ├── MarketToLimit.java │ │ ├── Peg.java │ │ └── PegWithLimit.java └── resources │ ├── FixBrokerA.cfg │ ├── FixBrokerB.cfg │ ├── FixServer.cfg │ └── logback.xml └── test ├── java └── com │ └── euronextclone │ ├── ContinuousMatchingStepDefinitions.java │ ├── MatchingUnitStepDefinitions.java │ ├── RunAllCukesTest.java │ ├── RunFocusCukesTest.java │ └── World.java └── resources └── com └── euronextclone ├── Euronext - market-to-limit-orders.feature ├── Euronext - pure market order.feature ├── Euronext - the peg orders.feature ├── _intellij-support.feature ├── auction-market-order.feature ├── basic-order-book-construction.feature ├── indicative-market-price.feature └── pure-market-orders.feature /.gitignore: -------------------------------------------------------------------------------- 1 | # Java 2 | *.class 3 | 4 | # OS 5 | .DS_Store 6 | 7 | # Maven 8 | target/ 9 | 10 | # IDEA 11 | *.iml 12 | .idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Euronext Clone 2 | Inspired by github Price-Time Matching [Engine](https://gist.github.com/2855852), based on Euronext documentation publically available, and the madness of too many years in finance: 3 | 4 | * [The Peg Orders](http://www.euronext.com/fic/000/041/609/416094.pdf) 5 | * [Pure Market Order](http://www3.production.euronext.com/fic/000/041/480/414808.pdf) 6 | * [Auction](http://www.nyse.com/pdfs/5653_NYSEArca_Auctions.pdf) 7 | * [The Market to limit order](http://www3.production.euronext.com/fic/000/041/480/414806.pdf) 8 | * [Trading](https://europeanequities.nyx.com/en/trading/continuous-trading-process) 9 | * [The Stop Order](http://www3.production.euronext.com/fic/000/041/480/414809.pdf) 10 | * [Stop Order](http://www.euronext.com/fic/000/010/550/105509.pdf) 11 | * [IMP/TOP calculation](http://www.asx.com.au/products/calculate-open-close-prices.htm) 12 | 13 | ## Quick Links 14 | [Setting up dev environment](https://github.com/mattdavey/EuronextClone/blob/master/docs/SettingUpDevEnvironment.md) 15 | 16 | [FIX to matching eninge flows over 0MQ](https://github.com/mattdavey/EuronextClone/blob/master/docs/Fix%20to%20Matching%20Cluster%20Architecture.md) 17 | 18 | ## History 19 | The project initially started in the distant past as a Proof Of Concept (PoC) (see below) mainly aimed at attempting to model the data flows of a matching server in Java. 20 | 21 | Over time, work and thought has been put in to add ancillary services around the core order book with the aim of moving towards the high level architecture detailed below, offering Master/Slave with heart-beating from a resiliance perspective. 22 | 23 | It is hoped that at some point in the future performance numbers can be provided on suitable hardware to allow appropriate tuning and improve the architecture from a latency perspective. 24 | 25 | ## Proof Of Concept (PoC) 26 | Primarily this PoC architecture is aimed at joining all the dots together to ensure a client can submit an FIX order, and receive appropriate ExecutionReports 27 | 28 | ![Basic](https://github.com/mattdavey/EuronextClone/raw/master/assets/basic.jpg) 29 | 30 | ## High Level Architecture 31 | The architecture below is based on the [LMAX](http://martinfowler.com/articles/lmax.html) architecture, but leveraging ZeroMQ instead of [Informatica](http://www.informatica.com/us/products/messaging/) Ultra Messaging. Further reading available [here](http://mdavey.wordpress.com/2012/08/01/financial-messaging-zeromq-random-reading/) 32 | 33 | ![Basic](https://github.com/mattdavey/EuronextClone/raw/master/assets/complex.jpg). 34 | -------------------------------------------------------------------------------- /assets/FIXToMatchingEngineFlows0MQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdavey/EuronextClone/0ae7d79a2c3832b8404d3f09709074c3f0df1657/assets/FIXToMatchingEngineFlows0MQ.png -------------------------------------------------------------------------------- /assets/basic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdavey/EuronextClone/0ae7d79a2c3832b8404d3f09709074c3f0df1657/assets/basic.jpg -------------------------------------------------------------------------------- /assets/complex.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdavey/EuronextClone/0ae7d79a2c3832b8404d3f09709074c3f0df1657/assets/complex.jpg -------------------------------------------------------------------------------- /docs/Fix to Matching Cluster Architecture.md: -------------------------------------------------------------------------------- 1 | # FIX to Matching Engine Cluster Architecture 2 | 3 | ## Notes 4 | * The following architecture diagram does not capture high availability and/or failover aspect of the overall design; only the main messaging flow is presented 5 | * FIX server communicates with the clients over standard FIX protocol (clients not shown on the diagram) 6 | * Each FIX server is responsoble for a configured subset of clients (brokers) 7 | * When orders come in over FIX from brokers they are published using multicast with the instrument name as the topic (routing key) 8 | * Each FIX server subscribes for execution updates related to its subset of brokers and relays them over FIX to the clients 9 | * Each matching engine (ME) is responsible for its range of instruments (configured statically or dynamically) 10 | * Each ME subscribes for order events related to its range of instruments on a multicast channel, published to by the FIX servers. Orders and other instructions are then routed (based on instrument) to their respective matching unit (MU) for execution/processing. 11 | * When trades and other events are generated by MU they are published by ME using multicast with the affected broker as the topic (routing key) 12 | 13 | ## Diagram 14 | ![FIX to Matching Engine Flows over 0MQ](https://github.com/mattdavey/EuronextClone/raw/master/assets/FIXToMatchingEngineFlows0MQ.png) -------------------------------------------------------------------------------- /docs/SettingUpDevEnvironment.md: -------------------------------------------------------------------------------- 1 | # Setting up development environment 2 | 3 | ## Prerequisites 4 | The only prerequisite required is that your system has [0MQ library](http://www.zeromq.org/) built and installed. 0MQ is a native dependecy used for messaging and is not automatically resolved by Maven build. 5 | 6 | Detailed instructions on how to build and install 0MQ on your system are available on [ZeroMQ site](http://www.zeromq.org/intro:get-the-software). 7 | 8 | ## Build 9 | First use [git](http://git-scm.com/) to clone this repo: 10 | 11 | git clone https://github.com/mattdavey/EuronextClone.git 12 | cd EuronextClone 13 | 14 | EuronextClone is built with [Maven](http://maven.apache.org/). 15 | 16 | mvn clean install 17 | 18 | The above will build the prototype and run all unit tests (cucumber-jvm over JUnit runner). 19 | 20 | ## Run 21 | 22 | There are currently two application available in the prototype: [FIX Server](#fix-server) and [FIX client](#fix-client). You can run a sinle copy of FIX server and one or more copies of FIX Client. 23 | This is changing soon though, so check this page often. We are moving to architecture where multiple FIX Servers may run, and we are moving to architecture where matching engine (ME) 24 | is a separate from FIX Server, and you can run multiple copies of those too. 25 | 26 | ### FIX Server 27 | 28 | FIX Server entry point is located at [FixServerApp](../src/main/java/com/euronextclone/fix/server/FixServerApp.java). FIX Server is preconfigured to accept connections from BROKER-A and BROKER-B clients. 29 | (See [FixServer.cfg](../src/main/resources/FixServer.cfg)) 30 | 31 | To run FIX Server using Maven: 32 | 33 | mvn exec:exec -Prun-fix-server 34 | 35 | This assumes that native 0MQ library is available at **/usr/local/lib** unless you have overriden its location using **zmq.library.path** environment variable. 36 | 37 | If you are running FIX Server manually or from your favorite IDE make sure that **-Djava.library.path=...** is set to the location where you have installed 0MQ library to. 38 | 39 | ### FIX Client 40 | 41 | FIX Client entry point is located at [FixClientApp](../src/main/java/com/euronextclone/fix/client/FixClientApp.java). 42 | 43 | To run FIX Client using Maven (as broker A): 44 | 45 | mvn exec:exec -Prun-fix-client-a 46 | 47 | To run FIX Client using Maven (as broker B): 48 | 49 | mvn exec:exec -Prun-fix-client-b 50 | 51 | If you running FIX Client manually or from your favorite IDE then you can pass the following command line arguments: 52 | 53 | **--broker=BROKER** 54 | 55 | The broker argument configures the FIX client for a specific broker configuration. 56 | For example, passing **--broker=A** uses [FixBrokerA.cfg](../src/main/resources/FixBrokerA.cfg) configuration. And passing **--broker=B** uses [FixBrokerB.cfg](../src/main/resources/FixBrokerB.cfg) configuration. 57 | 58 | You can start multiple copies of FIX Client, with different **"--broker"** configured. When the FIX client starts it is ready to accept user's input. 59 | 60 | * typing "**q**" terminates the FIX Client 61 | * typing "**h**" lists all available commands 62 | 63 | The following commands are currently supported: 64 | 65 | #### Place Limit Order 66 | 67 | buy MSFT 10@34 68 | 69 | #### Place Market Order 70 | 71 | sell MSFT 5 72 | -------------------------------------------------------------------------------- /libs/jzmq/jzmq/3.3.0/jzmq-3.3.0-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdavey/EuronextClone/0ae7d79a2c3832b8404d3f09709074c3f0df1657/libs/jzmq/jzmq/3.3.0/jzmq-3.3.0-sources.jar -------------------------------------------------------------------------------- /libs/jzmq/jzmq/3.3.0/jzmq-3.3.0-sources.jar.md5: -------------------------------------------------------------------------------- 1 | c1b94f9cc737fa3d24890efc0bc6e309 -------------------------------------------------------------------------------- /libs/jzmq/jzmq/3.3.0/jzmq-3.3.0-sources.jar.sha1: -------------------------------------------------------------------------------- 1 | 4fe79f286ae37ea360d54adf965371b521166b4d -------------------------------------------------------------------------------- /libs/jzmq/jzmq/3.3.0/jzmq-3.3.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdavey/EuronextClone/0ae7d79a2c3832b8404d3f09709074c3f0df1657/libs/jzmq/jzmq/3.3.0/jzmq-3.3.0.jar -------------------------------------------------------------------------------- /libs/jzmq/jzmq/3.3.0/jzmq-3.3.0.jar.md5: -------------------------------------------------------------------------------- 1 | 4d856c82d70062f3b1bece3963b93f1d -------------------------------------------------------------------------------- /libs/jzmq/jzmq/3.3.0/jzmq-3.3.0.jar.sha1: -------------------------------------------------------------------------------- 1 | d6df4bda6ade31111fa4de7d2002b6f2a3115a8d -------------------------------------------------------------------------------- /libs/jzmq/jzmq/3.3.0/jzmq-3.3.0.pom: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | jzmq 6 | jzmq 7 | 3.3.0 8 | POM was created from install:install-file 9 | 10 | -------------------------------------------------------------------------------- /libs/jzmq/jzmq/3.3.0/jzmq-3.3.0.pom.md5: -------------------------------------------------------------------------------- 1 | 954a7689ea6b8f1d836336f4ca8523f2 -------------------------------------------------------------------------------- /libs/jzmq/jzmq/3.3.0/jzmq-3.3.0.pom.sha1: -------------------------------------------------------------------------------- 1 | fab405161cd464cb338dc4b392dba34f0079a8ea -------------------------------------------------------------------------------- /libs/org/quickfixj/quickfixj-core/1.5.2/quickfixj-core-1.5.2-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdavey/EuronextClone/0ae7d79a2c3832b8404d3f09709074c3f0df1657/libs/org/quickfixj/quickfixj-core/1.5.2/quickfixj-core-1.5.2-sources.jar -------------------------------------------------------------------------------- /libs/org/quickfixj/quickfixj-core/1.5.2/quickfixj-core-1.5.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdavey/EuronextClone/0ae7d79a2c3832b8404d3f09709074c3f0df1657/libs/org/quickfixj/quickfixj-core/1.5.2/quickfixj-core-1.5.2.jar -------------------------------------------------------------------------------- /libs/org/quickfixj/quickfixj-core/1.5.2/quickfixj-core-1.5.2.pom: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | org.quickfixj 6 | quickfixj-core 7 | 1.5.2 8 | POM was created from install:install-file 9 | 10 | -------------------------------------------------------------------------------- /libs/org/quickfixj/quickfixj-core/maven-metadata-local.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.quickfixj 4 | quickfixj-core 5 | 6 | 1.5.2 7 | 8 | 1.5.2 9 | 10 | 20120325210829 11 | 12 | 13 | -------------------------------------------------------------------------------- /libs/org/quickfixj/quickfixj-msg-fix42/1.5.2/quickfixj-msg-fix42-1.5.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdavey/EuronextClone/0ae7d79a2c3832b8404d3f09709074c3f0df1657/libs/org/quickfixj/quickfixj-msg-fix42/1.5.2/quickfixj-msg-fix42-1.5.2.jar -------------------------------------------------------------------------------- /libs/org/quickfixj/quickfixj-msg-fix42/1.5.2/quickfixj-msg-fix42-1.5.2.pom: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | org.quickfixj 6 | quickfixj-msg-fix42 7 | 1.5.2 8 | POM was created from install:install-file 9 | 10 | -------------------------------------------------------------------------------- /libs/org/quickfixj/quickfixj-msg-fix42/maven-metadata-local.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.quickfixj 4 | quickfixj-msg-fix42 5 | 6 | 1.5.2 7 | 8 | 1.5.2 9 | 10 | 20120325210948 11 | 12 | 13 | -------------------------------------------------------------------------------- /libs/reactive4java/reactive4java/0.96.2/_maven.repositories: -------------------------------------------------------------------------------- 1 | #NOTE: This is an internal implementation file, its format can be changed without prior notice. 2 | #Tue Jul 10 17:18:32 EDT 2012 3 | reactive4java-0.96.2.jar>= 4 | reactive4java-0.96.2.pom>= 5 | -------------------------------------------------------------------------------- /libs/reactive4java/reactive4java/0.96.2/reactive4java-0.96.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdavey/EuronextClone/0ae7d79a2c3832b8404d3f09709074c3f0df1657/libs/reactive4java/reactive4java/0.96.2/reactive4java-0.96.2.jar -------------------------------------------------------------------------------- /libs/reactive4java/reactive4java/0.96.2/reactive4java-0.96.2.pom: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | reactive4java 6 | reactive4java 7 | 0.96.2 8 | POM was created from install:install-file 9 | 10 | -------------------------------------------------------------------------------- /libs/reactive4java/reactive4java/maven-metadata-local.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | reactive4java 4 | reactive4java 5 | 6 | 0.96.2 7 | 8 | 0.96.2 9 | 10 | 20120710211832 11 | 12 | 13 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 4.0.0 6 | 7 | com.mattdavey.matching 8 | EuronextClone 9 | 1.0-SNAPSHOT 10 | jar 11 | Euronext Clone 12 | 13 | 14 | 1.0.11 15 | 16 | env.zmq.library.path 17 | 18 | 19 | 20 | 21 | reactive4java 22 | reactive4java 23 | 0.96.2 24 | 25 | 26 | com.google.guava 27 | guava 28 | 12.0.1 29 | 30 | 31 | 32 | info.cukes 33 | cucumber-java 34 | ${cucumber-jvm.version} 35 | test 36 | 37 | 38 | info.cukes 39 | cucumber-picocontainer 40 | ${cucumber-jvm.version} 41 | test 42 | 43 | 44 | info.cukes 45 | cucumber-junit 46 | ${cucumber-jvm.version} 47 | test 48 | 49 | 50 | junit 51 | junit 52 | 4.13.1 53 | test 54 | 55 | 56 | org.hamcrest 57 | hamcrest-all 58 | 1.3 59 | test 60 | 61 | 62 | 63 | 64 | org.quickfixj 65 | quickfixj-core 66 | 1.5.2 67 | 68 | 69 | org.quickfixj 70 | quickfixj-msg-fix42 71 | 1.5.2 72 | 73 | 74 | org.apache.mina 75 | mina-core 76 | 1.1.0 77 | 78 | 79 | 80 | 81 | org.slf4j 82 | slf4j-api 83 | 1.6.3 84 | 85 | 86 | ch.qos.logback 87 | logback-classic 88 | 1.0.0 89 | runtime 90 | 91 | 92 | 93 | 94 | com.googlecode.disruptor 95 | disruptor 96 | 2.10.1 97 | 98 | 99 | 100 | 101 | jzmq 102 | jzmq 103 | 3.3.0 104 | 105 | 106 | 107 | 108 | com.dyuproject.protostuff 109 | protostuff-runtime 110 | 1.0.7 111 | 112 | 113 | com.dyuproject.protostuff 114 | protostuff-core 115 | 1.0.7 116 | 117 | 118 | 119 | 120 | 121 | 122 | local-libs 123 | file://${basedir}/libs 124 | 125 | 126 | 127 | 128 | 129 | 130 | default-zmq-library-path 131 | 132 | 133 | !zmq.library.path 134 | 135 | 136 | 137 | 138 | /usr/local/lib 139 | 140 | 141 | 142 | 143 | run-fix-server 144 | 145 | 146 | 147 | org.codehaus.mojo 148 | exec-maven-plugin 149 | 1.2.1 150 | 151 | 152 | 153 | exec 154 | 155 | 156 | 157 | 158 | java 159 | 160 | -classpath 161 | 162 | -Djava.library.path=${zmq.library.path} 163 | com.euronextclone.fix.server.FixServerApp 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | run-fix-client-a 174 | 175 | 176 | 177 | org.codehaus.mojo 178 | exec-maven-plugin 179 | 1.2.1 180 | 181 | 182 | 183 | exec 184 | 185 | 186 | 187 | 188 | java 189 | 190 | -classpath 191 | 192 | com.euronextclone.fix.client.FixClientApp 193 | --broker=A 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | run-fix-client-b 203 | 204 | 205 | 206 | org.codehaus.mojo 207 | exec-maven-plugin 208 | 1.2.1 209 | 210 | 211 | 212 | exec 213 | 214 | 215 | 216 | 217 | java 218 | 219 | -classpath 220 | 221 | com.euronextclone.fix.client.FixClientApp 222 | --broker=B 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | -------------------------------------------------------------------------------- /script/rt-linux/build-rt-kernel: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | EC2_KEY=~/.ssh/[your_key].pem 4 | 5 | . ./build-utils 6 | 7 | echo 'Spawning new ec2 instance...' 8 | EC2_HOST=`spawn-instance "matching-engine" 20 m1.xlarge` 9 | [ $? != 0 ] && echo 'spawning ec2 instance failed' && exit 1 10 | 11 | echo "Copying installation scripts to ${EC2_HOST}..." 12 | scp -i ${EC2_KEY} * .config root@${EC2_HOST}:/root/ >> /dev/null 2>&1 13 | 14 | echo "Installing ${EC2_HOST}..." 15 | ssh -i ${EC2_KEY} root@${EC2_HOST} './install-rt-kernel' 16 | [ $? != 0 ] && echo 'installing ec2 instance failed' && exit 1 17 | 18 | echo 'Build is done.' 19 | -------------------------------------------------------------------------------- /script/rt-linux/build-utils: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function create-group { 4 | GROUP=$1 5 | DESCRIPTION=$2 6 | 7 | ec2-create-group "${GROUP}" -d "${DESCRIPTION}" 8 | ec2-authorize "${GROUP}" -P tcp -p 22 -s 0.0.0.0/0 9 | } 10 | 11 | function spawn-instance { 12 | SECURITY_GROUPS=$1 13 | for group in ${SECURITY_GROUPS}; do 14 | GRP_OPTIONS="${GRP_OPTIONS} -g ${group}" 15 | done 16 | ROOT_DEVICE_SIZE=$2 17 | INSTANCE_TYPE=${3:-t1.micro} 18 | 19 | RESERVATION=`ec2-run-instances ami-41d00528 ${GRP_OPTIONS} -t ${INSTANCE_TYPE} -k gsmarquee -b /dev/sda1=:${ROOT_DEVICE_SIZE}:true \ 20 | | grep INSTANCE` 21 | [ $? != 0 ] && echo 'ec2-run-instances failed' >&2 && return 1 22 | INSTANCE=`echo "${RESERVATION}" | awk '{print $2}'` 23 | 24 | while [[ -z "${EC2_HOST}" ]]; do 25 | EC2_HOST=`ec2-describe-instances ${INSTANCE} --filter instance-state-name=running \ 26 | | grep '^INSTANCE' | awk '{print $4}'` 27 | [ $? != 0 ] && echo 'ec2-describe-instances failed' >&2 && return 1 28 | done 29 | 30 | while [[ true ]]; do 31 | ssh -o StrictHostKeyChecking=no -i ~/.ssh/gsmarquee.pem root@${EC2_HOST} 'echo OK >> /dev/null' >> /dev/null 2>&1 32 | [ $? = 0 ] && break 33 | sleep 1 34 | done 35 | 36 | echo "${EC2_HOST}" 37 | return 0 38 | } 39 | 40 | function get-instance-field { 41 | GROUP_NAME=$1 42 | FIELD_POS=$2 43 | 44 | ec2-describe-instances --show-empty-fields \ 45 | --filter instance-state-name=running \ 46 | --filter group-name=${GROUP_NAME} \ 47 | | grep INSTANCE | cut -f ${FIELD_POS} 48 | } 49 | 50 | function get-public-dns { 51 | SECURITY_GROUPS=$1 52 | for group in ${SECURITY_GROUPS}; do 53 | get-instance-field ${group} 4 54 | done | sort -u 55 | } 56 | 57 | function get-private-dns { 58 | SECURITY_GROUPS=$1 59 | for group in ${SECURITY_GROUPS}; do 60 | get-instance-field ${group} 5 61 | done | sort -u 62 | } 63 | 64 | function build-uri { 65 | HOSTS=$1 66 | PORT=$2 67 | PROTOCOL=$3 68 | URI='' 69 | 70 | for host in ${HOSTS}; do 71 | if [[ -n ${URI} ]]; then URI=${URI}", "; fi 72 | URI=$URI$PROTOCOL${host} 73 | if [[ -n ${PORT} ]]; then URI=${URI}:${PORT}; fi 74 | done 75 | 76 | echo ${URI} 77 | } 78 | -------------------------------------------------------------------------------- /script/rt-linux/install-rt-kernel: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | LOG=/var/log/matching-engine-install-rt-kernel.log 4 | KERNEL_VERSION='2.6.33.9' 5 | KERNEL_SOURCE="linux-${KERNEL_VERSION}" 6 | RT_PATCH_REV='31' 7 | 8 | echo 'Resizing the root device...' 9 | resize2fs /dev/xvde1 >> $LOG 2>&1 10 | [ $? != 0 ] && echo 'resizing root device failed' && exit 1 11 | 12 | echo 'Installing gcc...' 13 | yum -y install gcc >> $LOG 2>&1 14 | [ $? != 0 ] && echo 'yum gcc failed' && exit 1 15 | 16 | echo 'Downloding kernel...' 17 | wget http://www.kernel.org/pub/linux/kernel/v2.6/longterm/v2.6.33/linux-${KERNEL_VERSION}.tar.bz2 >> $LOG 2>&1 18 | [ $? != 0 ] && echo 'downloading linux kernel sources failed' && exit 1 19 | 20 | echo 'Downloding RT kernel patch...' 21 | wget http://www.kernel.org/pub/linux/kernel/projects/rt/2.6.33/patch-${KERNEL_VERSION}-rt${RT_PATCH_REV}.bz2 >> $LOG 2>&1 22 | [ $? != 0 ] && echo 'downloading linux kernel RT patch failed' && exit 1 23 | 24 | echo 'Patching the kernel...' 25 | tar xfj linux-${KERNEL_VERSION}.tar.bz2 >> $LOG 2>&1 26 | [ $? != 0 ] && echo 'un-tar on linux kernel sources failed' && exit 1 27 | cd ${KERNEL_SOURCE} >> $LOG 2>&1 28 | [ $? != 0 ] && echo 'cd to linux kernel sources failed' && exit 1 29 | bzcat ../patch-${KERNEL_VERSION}-rt${RT_PATCH_REV}.bz2 | patch -p1 >> $LOG 2>&1 30 | [ $? != 0 ] && echo 'patching linux kernel sources with RT failed' && exit 1 31 | 32 | echo 'Deploying kernel config...' 33 | cp ../.config . >> $LOG 2>&1 34 | [ $? != 0 ] && echo 'Deploying kernel config failed' && exit 1 35 | 36 | echo 'Building and installing the kernel...' 37 | make >> $LOG 2>&1 38 | [ $? != 0 ] && echo 'Building the kernel failed' && exit 1 39 | make modules >> $LOG 2>&1 40 | [ $? != 0 ] && echo 'Building the modules failed' && exit 1 41 | make modules_install >> $LOG 2>&1 42 | [ $? != 0 ] && echo 'Installing the modules failed' && exit 1 43 | make install >> $LOG 2>&1 44 | [ $? != 0 ] && echo 'Installing the kernel failed' && exit 1 45 | cp .config /boot/config-${KERNEL_VERSION}-rt${RT_PATCH_REV} >> $LOG 2>&1 46 | [ $? != 0 ] && echo 'Installing the kernel config failed' && exit 1 47 | 48 | echo 'Done installing.' 49 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/IndicativeMatchPriceCalculator.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class IndicativeMatchPriceCalculator 7 | { 8 | private final Double referencePrice; 9 | private final OrderBook sellBook; 10 | private final OrderBook buyBook; 11 | 12 | public IndicativeMatchPriceCalculator(final Double referencePrice, 13 | final OrderBook sellBook, 14 | final OrderBook buyBook) 15 | { 16 | this.referencePrice = referencePrice; 17 | this.sellBook = sellBook; 18 | this.buyBook = buyBook; 19 | } 20 | 21 | public final Double get() 22 | { 23 | List calculations = getMaximumExecutableVolume(); 24 | if (calculations == null) 25 | { 26 | return null; 27 | } 28 | if (calculations.size() == 1) 29 | { 30 | return calculations.get(0).getPrice(); 31 | } 32 | 33 | calculations = getMinimumSurplus(calculations); 34 | if (calculations == null) 35 | { 36 | return null; 37 | } 38 | if (calculations.size() == 1) 39 | { 40 | return calculations.get(0).getPrice(); 41 | } 42 | 43 | calculations = getMarketPressure(calculations); 44 | if (calculations == null) 45 | { 46 | return null; 47 | } 48 | if (calculations.size() == 1) 49 | { 50 | return calculations.get(0).getPrice(); 51 | } 52 | 53 | return getReferencePrice(calculations); 54 | } 55 | 56 | 57 | private List getMaximumExecutableVolume() 58 | { 59 | return new ArrayList(); 60 | } 61 | 62 | private List getMinimumSurplus(List calculations) 63 | { 64 | int minSurplus = Integer.MAX_VALUE; 65 | final List filtered = new ArrayList(); 66 | for (final CalculationInfo ci : calculations) 67 | { 68 | final int localMinSurplus = Math.abs(ci.getMinimumSurplus()); 69 | if (localMinSurplus < minSurplus) 70 | { 71 | minSurplus = localMinSurplus; 72 | filtered.clear(); 73 | filtered.add(ci); 74 | } 75 | else if (localMinSurplus == minSurplus) 76 | { 77 | filtered.add(ci); 78 | } 79 | } 80 | return filtered; 81 | } 82 | 83 | private List getMarketPressure(List calculations) 84 | { 85 | CalculationInfo buyCalculationInfo = null; 86 | CalculationInfo sellCalculationInfo = null; 87 | 88 | for (final CalculationInfo ci : calculations) 89 | { 90 | final int surplus = ci.getMinimumSurplus(); 91 | if (surplus > 0) 92 | { 93 | if (buyCalculationInfo == null || (buyCalculationInfo.getPrice() < ci.getPrice())) 94 | { 95 | buyCalculationInfo = ci; 96 | } 97 | } 98 | else if (surplus < 0) 99 | { 100 | if (buyCalculationInfo == null || (sellCalculationInfo.getPrice() > ci.getPrice())) 101 | { 102 | sellCalculationInfo = ci; 103 | } 104 | } 105 | //return earlier if we have market pressure on both sides (buy/sell) 106 | if (buyCalculationInfo != null && sellCalculationInfo != null) 107 | { 108 | return calculations; 109 | } 110 | } 111 | 112 | if (buyCalculationInfo != null) 113 | { 114 | final ArrayList c = new ArrayList(); 115 | c.add(buyCalculationInfo); 116 | return c; 117 | } 118 | 119 | if (sellCalculationInfo != null) 120 | { 121 | final ArrayList c = new ArrayList(); 122 | c.add(sellCalculationInfo); 123 | return c; 124 | } 125 | return calculations; 126 | } 127 | 128 | private double getReferencePrice(List calculations) 129 | { 130 | //get two prices 131 | CalculationInfo[] twoPrices = getTwoPrices(calculations); 132 | 133 | 134 | double max = Math.max(twoPrices[0].getPrice(), twoPrices[1].getPrice()); 135 | double min = Math.min(twoPrices[0].getPrice(), twoPrices[1].getPrice()); 136 | 137 | if (referencePrice == null) 138 | { 139 | return min; 140 | } 141 | 142 | if (referencePrice >= max) 143 | { 144 | return max; 145 | } 146 | if (referencePrice <= min) 147 | { 148 | return min; 149 | } 150 | return referencePrice; 151 | } 152 | 153 | private CalculationInfo[] getTwoPrices(final List calculations) 154 | { 155 | assert (calculations.size() > 1); 156 | //check all prices have minimum surplus zero 157 | boolean allZero = true; 158 | for (final CalculationInfo ci : calculations) 159 | { 160 | allZero = ci.getMinimumSurplus() == 0; 161 | if (!allZero) 162 | { 163 | break; 164 | } 165 | } 166 | if (allZero) 167 | { 168 | return new CalculationInfo[]{calculations.get(0), 169 | calculations.get(calculations.size() - 1)}; 170 | } 171 | 172 | CalculationInfo first = calculations.get(0); 173 | CalculationInfo second = null; 174 | for (int i = 1; i < calculations.size(); ++i) 175 | { 176 | final CalculationInfo temp = calculations.get(i); 177 | //the items are orderd and all the items minimum surplus are the same value 178 | if (first.getMinimumSurplus() != temp.getMinimumSurplus()) 179 | { 180 | second = temp; 181 | break; 182 | } 183 | first = temp; 184 | } 185 | return new CalculationInfo[]{first, second}; 186 | } 187 | 188 | private final class CalculationInfo 189 | { 190 | 191 | private final double price; 192 | private final int cumulativeBuyQuantity; 193 | private final int cumulativeSellQuantity; 194 | private final int maxExecutableVolume; 195 | 196 | private CalculationInfo(final double price, 197 | final int cumulativeBuyQuantity, 198 | final int cumulativeSellQuantity, 199 | final int maxExecutableVolume) 200 | { 201 | this.price = price; 202 | this.cumulativeBuyQuantity = cumulativeBuyQuantity; 203 | this.cumulativeSellQuantity = cumulativeSellQuantity; 204 | this.maxExecutableVolume = maxExecutableVolume; 205 | } 206 | 207 | 208 | public double getPrice() 209 | { 210 | return price; 211 | } 212 | 213 | public int getCumulativeBuyQuantity() 214 | { 215 | return cumulativeBuyQuantity; 216 | } 217 | 218 | public int getCumulativeSellQuantity() 219 | { 220 | return cumulativeSellQuantity; 221 | } 222 | 223 | public int getMaxExecutableVolume() 224 | { 225 | return maxExecutableVolume; 226 | } 227 | 228 | public int getMinimumSurplus() 229 | { 230 | return cumulativeBuyQuantity - cumulativeSellQuantity; 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/MatchingUnit.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone; 2 | 3 | import com.euronextclone.ordertypes.Limit; 4 | import com.google.common.base.Function; 5 | import com.google.common.base.Optional; 6 | import com.google.common.base.Predicate; 7 | import com.google.common.collect.FluentIterable; 8 | import com.google.common.collect.Lists; 9 | import hu.akarnokd.reactive4java.reactive.DefaultObservable; 10 | import hu.akarnokd.reactive4java.reactive.Observable; 11 | import hu.akarnokd.reactive4java.reactive.Observer; 12 | 13 | import javax.annotation.Nonnull; 14 | import java.io.Closeable; 15 | import java.util.*; 16 | 17 | public class MatchingUnit implements Observable { 18 | 19 | private final OrderBook buyOrderBook; 20 | private final OrderBook sellOrderBook; 21 | private TradingPhase tradingPhase = TradingPhase.CoreContinuous; 22 | private TradingMode tradingMode = TradingMode.Continuous; 23 | private Double referencePrice; 24 | private Double indicativeMatchingPrice; 25 | 26 | /** 27 | * The observable helper. 28 | */ 29 | private final DefaultObservable notifier = new DefaultObservable(); 30 | private final String instrument; 31 | 32 | public MatchingUnit(String instrument) { 33 | this.instrument = instrument; 34 | buyOrderBook = new OrderBook(); 35 | sellOrderBook = new OrderBook(); 36 | } 37 | 38 | public void setReferencePrice(Double referencePrice) { 39 | this.referencePrice = referencePrice; 40 | } 41 | 42 | public Double getIndicativeMatchingPrice() { 43 | 44 | final List eligiblePrices = getListOfEligiblePrices(); 45 | final List cumulativeBuy = getCumulativeQuantity(eligiblePrices, buyOrderBook, OrderSide.Buy); 46 | final List cumulativeSell = getCumulativeQuantity(eligiblePrices, sellOrderBook, OrderSide.Sell); 47 | final List totalTradeableVolume = getTotalTradeableVolume(eligiblePrices, cumulativeBuy, cumulativeSell); 48 | 49 | final List maximumExecutableVolume = determineMaximumExecutableValue(totalTradeableVolume); 50 | if (maximumExecutableVolume.size() == 1) { 51 | return maximumExecutableVolume.get(0).price; 52 | } 53 | 54 | final List minimumSurplus = establishMinimumSurplus(maximumExecutableVolume); 55 | if (minimumSurplus.size() == 1) { 56 | return minimumSurplus.get(0).price; 57 | } 58 | 59 | final Optional pressurePrice = ascertainWhereMarketPressureExists(minimumSurplus); 60 | if (pressurePrice.isPresent()) { 61 | return pressurePrice.get(); 62 | } 63 | 64 | return consultReferencePrice(minimumSurplus); 65 | } 66 | 67 | private double consultReferencePrice(List minimumSurplus) { 68 | final FluentIterable minimumSurplusIterable = FluentIterable.from(minimumSurplus); 69 | 70 | if (!minimumSurplus.isEmpty()) { 71 | double minPotentialPrice; 72 | double maxPotentialPrice; 73 | 74 | if (minimumSurplusIterable.allMatch(VolumeAtPrice.NO_PRESSURE)) { 75 | minPotentialPrice = minimumSurplusIterable.first().get().price; 76 | maxPotentialPrice = minimumSurplusIterable.last().get().price; 77 | } else { 78 | minPotentialPrice = minimumSurplusIterable.filter(VolumeAtPrice.BUYING_PRESSURE).last().get().price; 79 | maxPotentialPrice = minimumSurplusIterable.filter(VolumeAtPrice.SELLING_PRESSURE).first().get().price; 80 | } 81 | 82 | if (referencePrice == null) { 83 | return minPotentialPrice; 84 | } 85 | 86 | if (referencePrice >= maxPotentialPrice) { 87 | return maxPotentialPrice; 88 | } 89 | 90 | if (referencePrice <= minPotentialPrice) { 91 | return minPotentialPrice; 92 | } 93 | } 94 | 95 | return referencePrice; 96 | } 97 | 98 | private List establishMinimumSurplus(List maximumExecutableVolume) { 99 | 100 | if (maximumExecutableVolume.isEmpty()) { 101 | return maximumExecutableVolume; 102 | } 103 | 104 | final int minSurplus = Collections.min(maximumExecutableVolume, VolumeAtPrice.ABSOLUTE_SURPLUS_COMPARATOR).getAbsoluteSurplus(); 105 | 106 | FluentIterable minSurplusOnly = FluentIterable.from(maximumExecutableVolume).filter(new Predicate() { 107 | @Override 108 | public boolean apply(VolumeAtPrice input) { 109 | return input.getAbsoluteSurplus() == minSurplus; 110 | } 111 | }); 112 | 113 | return minSurplusOnly.toImmutableList(); 114 | } 115 | 116 | private List determineMaximumExecutableValue(List totalTradeableVolume) { 117 | 118 | if (totalTradeableVolume.isEmpty()) { 119 | return totalTradeableVolume; 120 | } 121 | 122 | final int maxVolume = Collections.max(totalTradeableVolume, VolumeAtPrice.TRADEABLE_VOLUME_COMPARATOR).getTradeableVolume(); 123 | 124 | FluentIterable maxVolumeOnly = FluentIterable.from(totalTradeableVolume).filter(new Predicate() { 125 | @Override 126 | public boolean apply(VolumeAtPrice input) { 127 | return input.getTradeableVolume() == maxVolume; 128 | } 129 | }); 130 | 131 | return maxVolumeOnly.toImmutableList(); 132 | } 133 | 134 | public void setTradingMode(TradingMode tradingMode) { 135 | this.tradingMode = tradingMode; 136 | } 137 | 138 | public void setTradingPhase(TradingPhase tradingPhase) { 139 | this.tradingPhase = tradingPhase; 140 | } 141 | 142 | private static class VolumeAtPrice { 143 | private double price; 144 | private int buyVolume; 145 | private int sellVolume; 146 | 147 | public VolumeAtPrice(double price, int buyVolume, int sellVolume) { 148 | this.price = price; 149 | this.buyVolume = buyVolume; 150 | this.sellVolume = sellVolume; 151 | } 152 | 153 | public int getTradeableVolume() { 154 | return Math.min(buyVolume, sellVolume); 155 | } 156 | 157 | public int getSurplus() { 158 | return buyVolume - sellVolume; 159 | } 160 | 161 | public int getAbsoluteSurplus() { 162 | return Math.abs(getSurplus()); 163 | } 164 | 165 | public static final Comparator TRADEABLE_VOLUME_COMPARATOR = new Comparator() { 166 | 167 | @Override 168 | public int compare(VolumeAtPrice volumeAtPrice1, VolumeAtPrice volumeAtPrice2) { 169 | Integer tradeableVolume1 = volumeAtPrice1.getTradeableVolume(); 170 | Integer tradeableVolume2 = volumeAtPrice2.getTradeableVolume(); 171 | return tradeableVolume1.compareTo(tradeableVolume2); 172 | } 173 | }; 174 | 175 | public static final Comparator ABSOLUTE_SURPLUS_COMPARATOR = new Comparator() { 176 | 177 | @Override 178 | public int compare(VolumeAtPrice volumeAtPrice1, VolumeAtPrice volumeAtPrice2) { 179 | Integer surplus1 = volumeAtPrice1.getAbsoluteSurplus(); 180 | Integer surplus2 = volumeAtPrice2.getAbsoluteSurplus(); 181 | return surplus1.compareTo(surplus2); 182 | } 183 | }; 184 | 185 | public static Predicate BUYING_PRESSURE = new Predicate() { 186 | @Override 187 | public boolean apply(final VolumeAtPrice input) { 188 | return input.getSurplus() > 0; 189 | } 190 | }; 191 | 192 | public static Predicate SELLING_PRESSURE = new Predicate() { 193 | @Override 194 | public boolean apply(final VolumeAtPrice input) { 195 | return input.getSurplus() < 0; 196 | } 197 | }; 198 | 199 | public static Predicate NO_PRESSURE = new Predicate() { 200 | @Override 201 | public boolean apply(final VolumeAtPrice input) { 202 | return input.getSurplus() == 0; 203 | } 204 | }; 205 | } 206 | 207 | private Optional ascertainWhereMarketPressureExists(List minimumSurplus) { 208 | 209 | if (!minimumSurplus.isEmpty()) { 210 | final FluentIterable minimumSurplusIterable = FluentIterable.from(minimumSurplus); 211 | 212 | final boolean buyingPressure = minimumSurplusIterable.allMatch(VolumeAtPrice.BUYING_PRESSURE); 213 | if (buyingPressure) { 214 | return Optional.of(minimumSurplusIterable.last().get().price); 215 | } 216 | 217 | final boolean sellingPressure = minimumSurplusIterable.allMatch(VolumeAtPrice.SELLING_PRESSURE); 218 | if (sellingPressure) { 219 | return Optional.of(minimumSurplusIterable.first().get().price); 220 | } 221 | } 222 | 223 | return Optional.absent(); 224 | } 225 | 226 | private List getCumulativeQuantity( 227 | final List eligiblePrices, 228 | final OrderBook book, 229 | final OrderSide side) { 230 | 231 | final List quantities = new ArrayList(eligiblePrices.size()); 232 | 233 | final Iterable priceIterable = side == OrderSide.Sell ? 234 | eligiblePrices : 235 | Lists.reverse(eligiblePrices); 236 | final ListIterator current = book.getOrders().listIterator(); 237 | int cumulative = 0; 238 | 239 | for (final double price : priceIterable) { 240 | while (current.hasNext()) { 241 | final Order order = current.next(); 242 | final OrderType orderType = order.getOrderType(); 243 | 244 | Double orderPrice = orderType.price(side, book.getBestLimit()); 245 | if (canTrade(side, orderPrice, price)) { 246 | cumulative += order.getQuantity(); 247 | } else { 248 | current.previous(); 249 | break; 250 | } 251 | } 252 | quantities.add(cumulative); 253 | } 254 | 255 | return side == OrderSide.Sell ? quantities : Lists.reverse(quantities); 256 | } 257 | 258 | private boolean canTrade(OrderSide orderSide, Double orderPrice, double testPrice) { 259 | 260 | if (orderPrice == null) { 261 | return true; 262 | } 263 | 264 | if (orderSide == OrderSide.Buy) { 265 | return testPrice <= orderPrice; 266 | } 267 | 268 | return testPrice >= orderPrice; 269 | } 270 | 271 | private List getTotalTradeableVolume( 272 | final List eligiblePrices, 273 | final List cumulativeBuy, 274 | final List cumulativeSell) { 275 | 276 | final List tradeableVolume = new ArrayList(); 277 | final Iterator priceIterator = eligiblePrices.iterator(); 278 | final Iterator buy = cumulativeBuy.iterator(); 279 | final Iterator sell = cumulativeSell.iterator(); 280 | 281 | while (priceIterator.hasNext()) { 282 | final double price = priceIterator.next(); 283 | final int buyVolume = buy.next(); 284 | final int sellVolume = sell.next(); 285 | tradeableVolume.add(new VolumeAtPrice(price, buyVolume, sellVolume)); 286 | } 287 | 288 | return tradeableVolume; 289 | } 290 | 291 | private List getListOfEligiblePrices() { 292 | 293 | final TreeSet prices = new TreeSet(); 294 | if (referencePrice != null) { 295 | prices.add(referencePrice); 296 | } 297 | prices.addAll(getLimitPrices(buyOrderBook)); 298 | prices.addAll(getLimitPrices(sellOrderBook)); 299 | 300 | return new ArrayList(prices); 301 | } 302 | 303 | private Collection getLimitPrices(final OrderBook book) { 304 | 305 | return FluentIterable.from(book.getOrders()).filter(new Predicate() { 306 | @Override 307 | public boolean apply(Order input) { 308 | return input.getOrderType().providesLimit(); 309 | } 310 | }).transform(new Function() { 311 | @Override 312 | public Double apply(Order input) { 313 | return input.getOrderType().getLimit(); 314 | } 315 | }).toImmutableSet(); 316 | } 317 | 318 | public void auction() { 319 | tradingPhase = TradingPhase.CoreAuction; 320 | indicativeMatchingPrice = getIndicativeMatchingPrice(); 321 | 322 | while (!buyOrderBook.getOrders().isEmpty()) { 323 | if (!tryMatchOrder(buyOrderBook.getOrders().get(0))) { 324 | break; 325 | } 326 | } 327 | 328 | upgradeMarketToLimitOrders(buyOrderBook); 329 | upgradeMarketToLimitOrders(sellOrderBook); 330 | } 331 | 332 | private void upgradeMarketToLimitOrders(OrderBook orderBook) { 333 | 334 | if (tradingMode == TradingMode.Continuous) { 335 | 336 | List orders = orderBook.getOrders(); 337 | List mtlOrders = FluentIterable.from(orders).filter(new Predicate() { 338 | @Override 339 | public boolean apply(Order input) { 340 | return input.getOrderType().convertsToLimit(); 341 | } 342 | }).toImmutableList(); 343 | orders.removeAll(mtlOrders); 344 | 345 | for (Order mtlOrder : mtlOrders) { 346 | Order limit = mtlOrder.convertTo(new Limit(indicativeMatchingPrice)); 347 | orderBook.add(limit); 348 | } 349 | } 350 | } 351 | 352 | public List getOrders(final OrderSide side) { 353 | return getBook(side).getOrders(); 354 | } 355 | 356 | public void addOrder(final OrderEntry orderEntry) { 357 | final Order order = new Order(orderEntry.getOrderId(), orderEntry.getBroker(), orderEntry.getQuantity(), orderEntry.getOrderType(), orderEntry.getSide()); 358 | final OrderSide side = orderEntry.getSide(); 359 | boolean topOfTheBook = getBook(side).add(order) == 0; 360 | 361 | if (tradingPhase == TradingPhase.CoreContinuous && topOfTheBook) { 362 | tryMatchOrder(order); 363 | } 364 | } 365 | 366 | private boolean tryMatchOrder(final Order newOrder) { 367 | final OrderSide side = newOrder.getSide(); 368 | final OrderBook book = getBook(side); 369 | final int startQuantity = newOrder.getQuantity(); 370 | final OrderBook counterBook = getCounterBook(side); 371 | 372 | Double newOrderPrice = newOrder.getOrderType().providesLimit() ? newOrder.getOrderType().getLimit() : null; 373 | 374 | List toRemove = new ArrayList(); 375 | List toAdd = new ArrayList(); 376 | 377 | Order currentOrder = newOrder; 378 | 379 | for (final Order order : counterBook.getOrders()) { 380 | 381 | // Determine the price at which the trade happens 382 | final Double bookOrderPrice = order.getOrderType().price(order.getSide(), counterBook.getBestLimit()); 383 | 384 | Double tradePrice = determineTradePrice(newOrderPrice, bookOrderPrice, order.getSide(), counterBook.getBestLimit()); 385 | if (tradePrice == null) { 386 | break; 387 | } 388 | 389 | if (tradingPhase == TradingPhase.CoreAuction) { 390 | tradePrice = indicativeMatchingPrice; 391 | } 392 | 393 | // Determine the amount to trade 394 | int tradeQuantity = determineTradeQuantity(currentOrder, order); 395 | 396 | // Trade 397 | currentOrder.decrementQuantity(tradeQuantity); 398 | order.decrementQuantity(tradeQuantity); 399 | generateTrade(currentOrder, order, tradeQuantity, tradePrice); 400 | 401 | if (order.getQuantity() == 0) { 402 | toRemove.add(order); 403 | } else { 404 | if (order.getOrderType().convertsToLimit()) { 405 | toRemove.add(order); 406 | toAdd.add(order.convertTo(new Limit(tradePrice))); 407 | } 408 | break; 409 | } 410 | 411 | if (currentOrder.getQuantity() == 0) { 412 | break; 413 | } 414 | 415 | if (currentOrder.getOrderType().convertsToLimit()) { 416 | book.remove(currentOrder); 417 | currentOrder = currentOrder.convertTo(new Limit(tradePrice)); 418 | book.add(currentOrder); 419 | newOrderPrice = tradePrice; 420 | } 421 | } 422 | 423 | counterBook.removeAll(toRemove); 424 | for (Order order : toAdd) { 425 | counterBook.add(order); 426 | } 427 | 428 | if (currentOrder.getQuantity() == 0) { 429 | book.remove(currentOrder); 430 | } 431 | 432 | return startQuantity != currentOrder.getQuantity(); 433 | } 434 | 435 | private void generateTrade(final Order newOrder, final Order order, final int tradeQuantity, final double price) { 436 | Order buyOrder = newOrder.getSide() == OrderSide.Buy ? newOrder : order; 437 | Order sellOrder = newOrder == buyOrder ? order : newOrder; 438 | 439 | notifier.next(new Trade( 440 | buyOrder.getOrderId(), buyOrder.getBroker(), 441 | sellOrder.getOrderId(), sellOrder.getBroker(), 442 | tradeQuantity, price)); 443 | } 444 | 445 | private int determineTradeQuantity(Order newOrder, Order order) { 446 | return Math.min(newOrder.getQuantity(), order.getQuantity()); 447 | } 448 | 449 | private Double determineTradePrice(Double newOrderPrice, Double counterBookOrderPrice, OrderSide counterBookSide, Double counterBookBestLimit) { 450 | 451 | if (newOrderPrice == null && counterBookOrderPrice == null) { 452 | if (counterBookBestLimit != null) { 453 | return counterBookBestLimit; 454 | } 455 | 456 | return referencePrice; 457 | } 458 | 459 | if (newOrderPrice == null) { 460 | return counterBookOrderPrice; 461 | } 462 | 463 | if (counterBookOrderPrice == null) { 464 | return tryImprovePrice(newOrderPrice, counterBookSide, counterBookBestLimit); 465 | } 466 | 467 | if (counterBookSide == OrderSide.Buy && counterBookOrderPrice >= newOrderPrice) { 468 | return counterBookOrderPrice; 469 | } 470 | 471 | if (counterBookSide == OrderSide.Sell && counterBookOrderPrice <= newOrderPrice) { 472 | return counterBookOrderPrice; 473 | } 474 | 475 | return null; // Can't trade 476 | } 477 | 478 | private double tryImprovePrice(double price, OrderSide counterBookSide, Double counterBookBestLimit) { 479 | if (counterBookBestLimit == null) { 480 | return price; // can't improve, not best limit in counter book 481 | } 482 | 483 | if (counterBookSide == OrderSide.Buy && counterBookBestLimit > price) { 484 | return counterBookBestLimit; 485 | } 486 | if (counterBookSide == OrderSide.Sell && counterBookBestLimit < price) { 487 | return counterBookBestLimit; 488 | } 489 | 490 | return price; // can't improve 491 | } 492 | 493 | public Double getBestLimit(final OrderSide side) { 494 | return side != OrderSide.Buy ? sellOrderBook.getBestLimit() : buyOrderBook.getBestLimit(); 495 | } 496 | 497 | @Nonnull 498 | public Closeable register(@Nonnull Observer observer) { 499 | return notifier.register(observer); 500 | } 501 | 502 | private OrderBook getBook(final OrderSide side) { 503 | return side != OrderSide.Buy ? sellOrderBook : buyOrderBook; 504 | } 505 | 506 | private OrderBook getCounterBook(final OrderSide side) { 507 | return side != OrderSide.Buy ? buyOrderBook : sellOrderBook; 508 | } 509 | } -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/Order.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | 5 | public class Order { 6 | private static AtomicInteger c = new AtomicInteger(0); 7 | private String orderId; 8 | private final String broker; 9 | private int quantity; 10 | private final OrderType orderType; 11 | private final OrderSide side; 12 | private final int id; 13 | 14 | public int compareTo(final Order anOrder, final Double bestLimit) { 15 | final int BEFORE = -1; 16 | final int EQUAL = 0; 17 | final int AFTER = 1; 18 | 19 | if (getId() == anOrder.getId()) 20 | return EQUAL; 21 | 22 | final int compareOrderType = compareOrderType(anOrder); 23 | if (compareOrderType != EQUAL) { 24 | return compareOrderType; 25 | } 26 | 27 | final int comparePrice = comparePrice(anOrder, bestLimit); 28 | if (comparePrice == EQUAL) { 29 | // Time after orderType compare 30 | if (getId() > anOrder.getId()) { 31 | return BEFORE; 32 | } else if (getId() < anOrder.getId()) { 33 | return AFTER; 34 | } 35 | } 36 | 37 | return comparePrice; 38 | } 39 | 40 | private int compareOrderType(Order other) { 41 | final int BEFORE = -1; 42 | final int EQUAL = 0; 43 | final int AFTER = 1; 44 | 45 | if (this.orderType.acceptsMarketPrice() && !other.orderType.acceptsMarketPrice()) { 46 | return AFTER; 47 | } 48 | if (!this.orderType.acceptsMarketPrice() && other.orderType.acceptsMarketPrice()) { 49 | return BEFORE; 50 | } 51 | 52 | return EQUAL; 53 | } 54 | 55 | private int comparePrice(final Order anOrder, final Double bestLimit) { 56 | final int BEFORE = -1; 57 | final int EQUAL = 0; 58 | final int AFTER = 1; 59 | 60 | final Double thisOrderLimit = getOrderType().price(side, bestLimit); 61 | final Double anOrderLimit = anOrder.getOrderType().price(side, bestLimit); 62 | 63 | if (thisOrderLimit == null && anOrderLimit == null) { 64 | return EQUAL; 65 | } 66 | 67 | switch (side) { 68 | case Buy: 69 | if (thisOrderLimit == null || thisOrderLimit > anOrderLimit) { 70 | return AFTER; 71 | } else if (thisOrderLimit < anOrderLimit) { 72 | return BEFORE; 73 | } 74 | break; 75 | case Sell: 76 | if (thisOrderLimit == null || thisOrderLimit < anOrderLimit) { 77 | return AFTER; 78 | } else if (thisOrderLimit > anOrderLimit) { 79 | return BEFORE; 80 | } 81 | break; 82 | } 83 | 84 | return EQUAL; 85 | } 86 | 87 | public Order(final String orderId, final String broker, final int quantity, final OrderType orderType, final OrderSide side) { 88 | id = c.incrementAndGet(); 89 | this.orderId = orderId; 90 | this.orderType = orderType; 91 | this.side = side; 92 | this.broker = broker; 93 | this.quantity = quantity; 94 | } 95 | 96 | public int getQuantity() { 97 | return quantity; 98 | } 99 | 100 | public String getOrderId() { 101 | return orderId; 102 | } 103 | 104 | public String getBroker() { 105 | return broker; 106 | } 107 | 108 | public OrderSide getSide() { 109 | return side; 110 | } 111 | 112 | public int getId() { 113 | return id; 114 | } 115 | 116 | public void decrementQuantity(final int quantity) { 117 | this.quantity -= quantity; 118 | } 119 | 120 | public OrderType getOrderType() { 121 | return orderType; 122 | } 123 | 124 | public Order convertTo(OrderType orderType) { 125 | return new Order(orderId, broker, quantity, orderType, side); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/OrderBook.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone; 2 | 3 | import com.google.common.base.Predicate; 4 | import com.google.common.collect.FluentIterable; 5 | 6 | import java.util.Collection; 7 | import java.util.LinkedList; 8 | import java.util.List; 9 | 10 | public class OrderBook { 11 | private final LinkedList orders = new LinkedList(); 12 | private Double bestLimit = null; 13 | 14 | public Double getBestLimit() { 15 | return bestLimit; 16 | } 17 | 18 | 19 | public void removeAll(Collection ordersToRemove) { 20 | orders.removeAll(ordersToRemove); 21 | validatePegOrderPositions(); 22 | } 23 | 24 | public void remove(Order order) { 25 | orders.remove(order); 26 | validatePegOrderPositions(); 27 | } 28 | 29 | public int add(final Order order) { 30 | int position = placeOrderInBook(order); 31 | validatePegOrderPositions(); 32 | return position; 33 | } 34 | 35 | private int placeOrderInBook(final Order newOrder) { 36 | int count = 0; 37 | for (final Order order : orders) { 38 | 39 | final int compare = order.compareTo(newOrder, bestLimit); 40 | if (compare < 0) { 41 | break; 42 | } 43 | 44 | count++; 45 | } 46 | 47 | orders.add(count, newOrder); 48 | return count; 49 | } 50 | 51 | private void validatePegOrderPositions() { 52 | if (orders.size() == 0) 53 | return; 54 | 55 | Double oldBestLimit = bestLimit; 56 | bestLimit = calculateBestLimit(); 57 | boolean changed = oldBestLimit != null ? !oldBestLimit.equals(bestLimit) : bestLimit != null; 58 | 59 | if (changed) { 60 | List pegged = FluentIterable.from(orders) 61 | .filter(new Predicate() { 62 | @Override 63 | public boolean apply(final Order input) { 64 | return input.getOrderType().canPegLimit(); 65 | } 66 | }).toImmutableList(); 67 | 68 | orders.removeAll(pegged); 69 | for (Order order : pegged) { 70 | placeOrderInBook(order.convertTo(order.getOrderType())); 71 | } 72 | } 73 | 74 | // Cheat if the order book only has a PEG left 75 | if (orders.size() > 0 && orders.get(0).getOrderType().canPegLimit()) 76 | orders.clear(); 77 | 78 | } 79 | 80 | private Double calculateBestLimit() { 81 | // Order book should be good, just reset best 82 | for (final Order order : orders) { 83 | if (order.getOrderType().providesLimit()) { 84 | return order.getOrderType().getLimit(); 85 | } 86 | } 87 | 88 | return bestLimit; 89 | } 90 | 91 | public int orderBookDepth() { 92 | return orders.size(); 93 | } 94 | 95 | public List getOrders() { 96 | return orders; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/OrderEntry.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone; 2 | 3 | import java.util.UUID; 4 | 5 | /** 6 | * Created with IntelliJ IDEA. 7 | * User: eprystupa 8 | * Date: 7/26/12 9 | * Time: 5:19 PM 10 | */ 11 | public class OrderEntry { 12 | private final String orderId; 13 | private final OrderSide side; 14 | private final String broker; 15 | private final int quantity; 16 | private final OrderType orderType; 17 | 18 | 19 | public OrderEntry(OrderSide side, String broker, int quantity, OrderType orderType) { 20 | this.orderId = UUID.randomUUID().toString(); 21 | this.side = side; 22 | this.broker = broker; 23 | this.quantity = quantity; 24 | this.orderType = orderType; 25 | } 26 | 27 | public String getOrderId() { 28 | return orderId; 29 | } 30 | 31 | public OrderSide getSide() { 32 | return side; 33 | } 34 | 35 | public String getBroker() { 36 | return broker; 37 | } 38 | 39 | public int getQuantity() { 40 | return quantity; 41 | } 42 | 43 | public OrderType getOrderType() { 44 | return orderType; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/OrderSide.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone; 2 | 3 | /** 4 | * Created with IntelliJ IDEA. 5 | * User: eprystupa 6 | * Date: 7/22/12 7 | * Time: 10:19 AM 8 | */ 9 | public enum OrderSide { 10 | Buy, Sell 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/OrderType.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone; 2 | 3 | public interface OrderType { 4 | 5 | boolean acceptsMarketPrice(); 6 | 7 | boolean providesLimit(); 8 | 9 | double getLimit(); 10 | 11 | boolean canPegLimit(); 12 | 13 | boolean convertsToLimit(); 14 | 15 | Double price(OrderSide side, final Double bestLimit); 16 | 17 | String displayPrice(Double price); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/Trade.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone; 2 | 3 | public class Trade { 4 | private final String buyOrderId; 5 | private final String buyBroker; 6 | private final String sellOrderId; 7 | private final String sellBroker; 8 | private final int quantity; 9 | private final double price; 10 | 11 | 12 | 13 | public Trade(String buyOrderId, final String buyBroker, String sellOrderId, final String sellBroker, final int quantity, final double price) { 14 | this.buyOrderId = buyOrderId; 15 | this.buyBroker = buyBroker; 16 | this.sellOrderId = sellOrderId; 17 | this.sellBroker = sellBroker; 18 | this.price = price; 19 | this.quantity = quantity; 20 | } 21 | 22 | public String getSellBroker() { 23 | return sellBroker; 24 | } 25 | 26 | public String getBuyBroker() { 27 | return buyBroker; 28 | } 29 | 30 | public int getQuantity() { 31 | return quantity; 32 | } 33 | 34 | public double getPrice() { 35 | return price; 36 | } 37 | 38 | public String getBuyOrderId() { 39 | return buyOrderId; 40 | } 41 | 42 | public String getSellOrderId() { 43 | return sellOrderId; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/TradingMode.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone; 2 | 3 | /** 4 | * Created with IntelliJ IDEA. 5 | * User: eprystupa 6 | * Date: 7/19/12 7 | * Time: 7:24 AM 8 | */ 9 | public enum TradingMode { 10 | ByAuction, 11 | Continuous 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/TradingPhase.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone; 2 | 3 | /** 4 | * Created with IntelliJ IDEA. 5 | * User: eprystupa 6 | * Date: 7/19/12 7 | * Time: 7:12 AM 8 | */ 9 | public enum TradingPhase { 10 | // PreOpeningPhase, 11 | CoreCall, 12 | CoreAuction, 13 | CoreContinuous, 14 | // PreCloseingPhase, ClosingAuction, TradingAtLastPhase, AfterHoursTrading 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/engine/MatchingEngine.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.engine; 2 | 3 | import com.euronextclone.MatchingUnit; 4 | import com.euronextclone.OrderEntry; 5 | import com.euronextclone.OrderSide; 6 | import com.euronextclone.ordertypes.Limit; 7 | import com.lmax.disruptor.*; 8 | import com.lmax.disruptor.dsl.Disruptor; 9 | 10 | import java.util.Scanner; 11 | import java.util.concurrent.ExecutorService; 12 | import java.util.concurrent.Executors; 13 | 14 | /** 15 | * Matching Engine will contain 1 or more matching units. 16 | * 17 | * Engine is responsible for listing to incoming requests, journal the requests (orderEntry), 18 | * replicate to slave Matching engine and then submitting request to Matching Unit for processing 19 | * 20 | * Engine also needs to heat-beat so slave can promote to master if required 21 | * 22 | * Engine will also handle networking - ZeroMQ 23 | */ 24 | public class MatchingEngine { 25 | private final static int RING_SIZE = 1024 * 8; 26 | private final ExecutorService EXECUTOR = Executors.newFixedThreadPool(3); 27 | private final RingBuffer ringBuffer; 28 | private final MatchingUnit matchingUnit; 29 | 30 | final EventHandler journalHandler = new EventHandler() 31 | { 32 | public void onEvent(final ValueEvent event, final long sequence, final boolean endOfBatch) throws Exception 33 | { 34 | System.out.println(String.format("Journal %s", event.getValue())); 35 | } 36 | }; 37 | final EventHandler replicatorHandler = new EventHandler() 38 | { 39 | public void onEvent(final ValueEvent event, final long sequence, final boolean endOfBatch) throws Exception 40 | { 41 | System.out.println(String.format("Replicator %s", event.getValue())); 42 | } 43 | }; 44 | final EventHandler matchingUnitHandler = new EventHandler() 45 | { 46 | public void onEvent(final ValueEvent event, final long sequence, final boolean endOfBatch) throws Exception 47 | { 48 | System.out.println(String.format("Process %s", event.getValue())); 49 | matchingUnit.addOrder(event.getValue()); 50 | } 51 | }; 52 | 53 | private final Disruptor disruptor; 54 | 55 | public MatchingEngine(final String instrument) { 56 | matchingUnit = new MatchingUnit(instrument); 57 | 58 | disruptor = new Disruptor(ValueEvent.EVENT_FACTORY, EXECUTOR, 59 | new SingleThreadedClaimStrategy(RING_SIZE), 60 | new SleepingWaitStrategy()); 61 | disruptor.handleEventsWith(journalHandler, replicatorHandler).then(matchingUnitHandler); 62 | ringBuffer = disruptor.start(); 63 | } 64 | 65 | public void publish(final int num) { 66 | // Publishers claim events in sequence 67 | long sequence = ringBuffer.next(); 68 | ValueEvent event = ringBuffer.get(sequence); 69 | event.setValue(new OrderEntry(OrderSide.Buy, "A", num, new Limit(num))); // this could be more complex with multiple fields 70 | 71 | ringBuffer.publish(sequence); 72 | } 73 | 74 | public static void main(String args[]) { 75 | final MatchingEngine engine = new MatchingEngine("MSFT"); 76 | engine.publish(1234); 77 | engine.publish(1235); 78 | 79 | System.out.println("[Enter] q to exit"); 80 | Scanner scanner = new Scanner(System.in); 81 | while (!scanner.nextLine().trim().equals("q")) System.out.println("[Enter] q to exit"); 82 | 83 | engine.halt(); 84 | System.out.println("Exiting..."); 85 | } 86 | 87 | private void halt() { 88 | disruptor.halt(); 89 | disruptor.shutdown(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/engine/ValueEvent.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.engine; 2 | 3 | import com.euronextclone.OrderEntry; 4 | import com.lmax.disruptor.EventFactory; 5 | 6 | public final class ValueEvent 7 | { 8 | private OrderEntry value; 9 | 10 | public OrderEntry getValue() 11 | { 12 | return value; 13 | } 14 | 15 | public void setValue(final OrderEntry value) 16 | { 17 | this.value = value; 18 | } 19 | 20 | public final static EventFactory EVENT_FACTORY = new EventFactory() 21 | { 22 | public ValueEvent newInstance() 23 | { 24 | return new ValueEvent(); 25 | } 26 | }; 27 | } -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/fix/FixAdapter.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.fix; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import quickfix.*; 6 | import quickfix.fix42.MessageCracker; 7 | 8 | /** 9 | * Created with IntelliJ IDEA. 10 | * User: eprystupa 11 | * Date: 7/24/12 12 | * Time: 10:30 PM 13 | */ 14 | public abstract class FixAdapter extends MessageCracker implements Application { 15 | 16 | private static Logger logger = LoggerFactory.getLogger(FixAdapter.class); 17 | 18 | @Override 19 | public void onLogon(SessionID sessionId) { 20 | logger.debug("Logon for session: {}", sessionId); 21 | } 22 | 23 | @Override 24 | public void onLogout(SessionID sessionId) { 25 | logger.debug("Logout for session: {}", sessionId); 26 | } 27 | 28 | @Override 29 | public void toAdmin(Message message, SessionID sessionId) { 30 | logger.debug("toAdmin for session: {}, message: {}", sessionId, message); 31 | } 32 | 33 | @Override 34 | public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon { 35 | logger.debug("fromAdmin for session: {}, message: {}", sessionId, message); 36 | } 37 | 38 | @Override 39 | public void toApp(Message message, SessionID sessionId) throws DoNotSend { 40 | logger.debug("toApp for session: {}, message: {}", sessionId, message); 41 | } 42 | 43 | @Override 44 | public void fromApp(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType { 45 | logger.debug("fromApp for session: {}, message: {}", sessionId, message); 46 | crack(message, sessionId); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/fix/client/FixClient.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.fix.client; 2 | 3 | import com.euronextclone.fix.FixAdapter; 4 | import com.lmax.disruptor.EventHandler; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import quickfix.*; 8 | import quickfix.fix42.ExecutionReport; 9 | import quickfix.fix42.NewOrderSingle; 10 | 11 | 12 | /** 13 | * Created with IntelliJ IDEA. 14 | * User: eprystupa 15 | * Date: 7/25/12 16 | * Time: 8:28 PM 17 | */ 18 | public class FixClient extends FixAdapter { 19 | 20 | private static Logger logger = LoggerFactory.getLogger(FixClient.class); 21 | private final SocketInitiator socketInitiator; 22 | private SessionID sessionId; 23 | private EventHandler executionReportEventHandler; 24 | 25 | public FixClient(final SessionSettings settings) throws ConfigError { 26 | 27 | MessageStoreFactory messageStoreFactory = new FileStoreFactory(settings); 28 | SLF4JLogFactory logFactory = new SLF4JLogFactory(settings); 29 | MessageFactory messageFactory = new DefaultMessageFactory(); 30 | socketInitiator = new SocketInitiator(this, messageStoreFactory, settings, logFactory, messageFactory); 31 | } 32 | 33 | @Override 34 | public void onCreate(SessionID sessionId) { 35 | logger.info("Session created: {}", sessionId); 36 | 37 | this.sessionId = sessionId; 38 | Session.lookupSession(sessionId).logon(); 39 | } 40 | 41 | @Override 42 | public void onMessage(ExecutionReport message, SessionID sessionID) throws FieldNotFound, UnsupportedMessageType, IncorrectTagValue { 43 | try { 44 | executionReportEventHandler.onEvent(message, 0L, true); 45 | } catch (Exception e) { 46 | logger.error(e.getMessage(), e); 47 | } 48 | } 49 | 50 | public void start() throws ConfigError { 51 | socketInitiator.start(); 52 | } 53 | 54 | public void stop() { 55 | socketInitiator.stop(); 56 | } 57 | 58 | public void submitOrder(NewOrderSingle order) throws SessionNotFound { 59 | Session.sendToTarget(order, sessionId); 60 | } 61 | 62 | public void handleExecutions(EventHandler handler) { 63 | executionReportEventHandler = handler; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/fix/client/FixClientApp.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.fix.client; 2 | 3 | import com.euronextclone.fix.client.commands.ClientCommand; 4 | import com.euronextclone.fix.client.commands.PlaceLimitOrder; 5 | import com.euronextclone.fix.client.commands.PlaceMarketOrder; 6 | import com.lmax.disruptor.EventHandler; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import quickfix.ConfigError; 10 | import quickfix.SessionNotFound; 11 | import quickfix.SessionSettings; 12 | import quickfix.field.ExecType; 13 | import quickfix.field.Side; 14 | import quickfix.fix42.ExecutionReport; 15 | 16 | import java.io.InputStream; 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | import java.util.Scanner; 20 | import java.util.regex.Matcher; 21 | import java.util.regex.Pattern; 22 | 23 | /** 24 | * Created with IntelliJ IDEA. 25 | * User: eprystupa 26 | * Date: 7/24/12 27 | * Time: 8:24 PM 28 | */ 29 | public class FixClientApp { 30 | private static Logger logger = LoggerFactory.getLogger(FixClientApp.class); 31 | private static List commands = new ArrayList(); 32 | 33 | public static void main(String args[]) { 34 | 35 | commands.add(new PlaceLimitOrder()); 36 | commands.add(new PlaceMarketOrder()); 37 | 38 | try { 39 | final String broker = getBroker(args, "A"); 40 | final FixClient client = createClient(broker); 41 | 42 | Scanner scanner = new Scanner(System.in); 43 | System.out.println("Enter [q] to exit, or [h] to list commands"); 44 | while (true) { 45 | System.out.print("Broker " + broker + "> "); 46 | final String command = scanner.nextLine().trim(); 47 | if (command.equals("q")) { 48 | break; 49 | } 50 | 51 | if (command.equals("h")) { 52 | listCommands(); 53 | continue; 54 | } 55 | 56 | for (ClientCommand clientCommand : commands) { 57 | final Matcher matcher = clientCommand.pattern().matcher(command); 58 | if (matcher.matches()) { 59 | clientCommand.execute(client, matcher); 60 | break; 61 | } 62 | } 63 | } 64 | 65 | System.out.println("Exiting..."); 66 | 67 | client.stop(); 68 | 69 | } catch (ConfigError configError) { 70 | logger.error(configError.getMessage(), configError); 71 | } catch (SessionNotFound sessionNotFound) { 72 | logger.error(sessionNotFound.getMessage(), sessionNotFound); 73 | } 74 | } 75 | 76 | private static String getBroker(String[] args, String defaultBroker) { 77 | // TODO: replace with some Java library for command arg parsing 78 | String result = defaultBroker; 79 | for (String arg : args) { 80 | final Matcher matcher = Pattern.compile("--broker=(\\S)").matcher(arg); 81 | if (matcher.matches()) { 82 | result = matcher.group(1); 83 | } 84 | } 85 | return result; 86 | } 87 | 88 | private static void listCommands() { 89 | for (ClientCommand command : commands) { 90 | System.out.println(command.name() + ":\t" + command.pattern().pattern()); 91 | } 92 | } 93 | 94 | private static FixClient createClient(final String broker) throws ConfigError { 95 | final InputStream config = FixClientApp.class.getClassLoader().getResourceAsStream("FixBroker" + broker + ".cfg"); 96 | final FixClient client = new FixClient(new SessionSettings(config)); 97 | client.start(); 98 | 99 | client.handleExecutions(new EventHandler() { 100 | 101 | @Override 102 | public void onEvent(ExecutionReport report, long sequence, boolean endOfBatch) throws Exception { 103 | String orderId = report.getOrderID().getValue(); 104 | String side = report.getSide().getValue() == Side.BUY ? "bought" : "sold"; 105 | ExecType execType = report.getExecType(); 106 | String symbol = report.getSymbol().getValue(); 107 | 108 | if (execType.getValue() == ExecType.NEW) { 109 | logger.debug("Order {} accepted", orderId); 110 | } else { 111 | double tradeQty = report.getLastShares().getValue(); 112 | double tradePrice = report.getLastPx().getValue(); 113 | logger.debug("Broker {} {} {} shares of {} at {}", new Object[]{broker, side, tradeQty, symbol, tradePrice}); 114 | } 115 | } 116 | }); 117 | return client; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/fix/client/OrderBuilder.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.fix.client; 2 | 3 | import quickfix.field.*; 4 | import quickfix.fix42.NewOrderSingle; 5 | 6 | import java.util.Date; 7 | import java.util.UUID; 8 | 9 | /** 10 | * Created with IntelliJ IDEA. 11 | * User: eprystupa 12 | * Date: 7/25/12 13 | * Time: 10:02 PM 14 | */ 15 | public class OrderBuilder { 16 | private String symbol; 17 | private char orderType; 18 | private double quantity; 19 | private char side; 20 | private double price; 21 | 22 | public OrderBuilder withSymbol(String symbol) { 23 | this.symbol = symbol; 24 | return this; 25 | } 26 | 27 | public OrderBuilder withOrderType(char orderType) { 28 | this.orderType = orderType; 29 | return this; 30 | } 31 | 32 | public OrderBuilder withQuantity(double quantity) { 33 | this.quantity = quantity; 34 | return this; 35 | } 36 | 37 | public OrderBuilder at(double price) { 38 | this.price = price; 39 | return this; 40 | } 41 | 42 | public NewOrderSingle buy() { 43 | this.side = Side.BUY; 44 | return build(); 45 | } 46 | 47 | public NewOrderSingle sell() { 48 | this.side = Side.SELL; 49 | return build(); 50 | } 51 | 52 | private NewOrderSingle build() { 53 | final NewOrderSingle order = new NewOrderSingle( 54 | new ClOrdID(UUID.randomUUID().toString()), 55 | new HandlInst(HandlInst.AUTOMATED_EXECUTION_ORDER_PUBLIC), 56 | new Symbol(symbol), 57 | new Side(side), 58 | new TransactTime(new Date()), 59 | new OrdType(orderType)); 60 | 61 | order.set(new OrderQty(quantity)); 62 | order.set(new Price(price)); 63 | return order; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/fix/client/commands/ClientCommand.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.fix.client.commands; 2 | 3 | import com.euronextclone.fix.client.FixClient; 4 | import quickfix.SessionNotFound; 5 | 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | /** 10 | * Created with IntelliJ IDEA. 11 | * User: eprystupa 12 | * Date: 8/1/12 13 | * Time: 7:34 PM 14 | */ 15 | public interface ClientCommand { 16 | String name(); 17 | 18 | Pattern pattern(); 19 | 20 | void execute(FixClient client, Matcher input) throws SessionNotFound; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/fix/client/commands/PlaceLimitOrder.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.fix.client.commands; 2 | 3 | import com.euronextclone.fix.client.FixClient; 4 | import com.euronextclone.fix.client.OrderBuilder; 5 | import quickfix.SessionNotFound; 6 | import quickfix.field.OrdType; 7 | import quickfix.fix42.NewOrderSingle; 8 | 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | /** 13 | * Created with IntelliJ IDEA. 14 | * User: eprystupa 15 | * Date: 8/1/12 16 | * Time: 7:38 PM 17 | */ 18 | public class PlaceLimitOrder implements ClientCommand { 19 | 20 | private static final Pattern INSTRUCTION = Pattern.compile("^(Buy|Sell) (\\S+) (\\d+)@(\\d+)$", Pattern.CASE_INSENSITIVE); 21 | 22 | @Override 23 | public String name() { 24 | return "Place limit order"; 25 | } 26 | 27 | @Override 28 | public Pattern pattern() { 29 | return INSTRUCTION; 30 | } 31 | 32 | @Override 33 | public void execute(final FixClient client, final Matcher input) throws SessionNotFound { 34 | 35 | if (!input.matches()) { 36 | throw new RuntimeException("Bad input for place limit order command"); 37 | } 38 | final String side = input.group(1); 39 | final String symbol = input.group(2); 40 | final String quantity = input.group(3); 41 | final String price = input.group(4); 42 | 43 | final OrderBuilder orderBuilder = new OrderBuilder() 44 | .withSymbol(symbol) 45 | .withOrderType(OrdType.LIMIT) 46 | .withQuantity(Double.parseDouble(quantity)) 47 | .at(Double.parseDouble(price)); 48 | final NewOrderSingle order = side.compareToIgnoreCase("Buy") == 0 ? orderBuilder.buy() : orderBuilder.sell(); 49 | client.submitOrder(order); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/fix/client/commands/PlaceMarketOrder.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.fix.client.commands; 2 | 3 | import com.euronextclone.fix.client.FixClient; 4 | import com.euronextclone.fix.client.OrderBuilder; 5 | import quickfix.SessionNotFound; 6 | import quickfix.field.OrdType; 7 | import quickfix.fix42.NewOrderSingle; 8 | 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | /** 13 | * Created with IntelliJ IDEA. 14 | * User: eprystupa 15 | * Date: 8/1/12 16 | * Time: 10:22 PM 17 | */ 18 | public class PlaceMarketOrder implements ClientCommand { 19 | 20 | private static final Pattern INSTRUCTION = Pattern.compile("^(Buy|Sell) (\\S+) (\\d+)$", Pattern.CASE_INSENSITIVE); 21 | 22 | @Override 23 | public String name() { 24 | return "Place market order"; 25 | } 26 | 27 | @Override 28 | public Pattern pattern() { 29 | return INSTRUCTION; 30 | } 31 | 32 | @Override 33 | public void execute(FixClient client, Matcher input) throws SessionNotFound { 34 | if (!input.matches()) { 35 | throw new RuntimeException("Bad input for place market order command"); 36 | } 37 | final String side = input.group(1); 38 | final String symbol = input.group(2); 39 | final String quantity = input.group(3); 40 | 41 | final OrderBuilder orderBuilder = new OrderBuilder() 42 | .withSymbol(symbol) 43 | .withOrderType(OrdType.MARKET) 44 | .withQuantity(Double.parseDouble(quantity)); 45 | final NewOrderSingle order = side.compareToIgnoreCase("Buy") == 0 ? orderBuilder.buy() : orderBuilder.sell(); 46 | client.submitOrder(order); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/fix/server/FixServer.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.fix.server; 2 | 3 | import com.euronextclone.*; 4 | import com.euronextclone.fix.FixAdapter; 5 | import com.euronextclone.messaging.Publisher; 6 | import com.euronextclone.ordertypes.Limit; 7 | import com.euronextclone.ordertypes.Market; 8 | import hu.akarnokd.reactive4java.reactive.Observer; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import quickfix.*; 12 | import quickfix.field.*; 13 | import quickfix.fix42.ExecutionReport; 14 | import quickfix.fix42.NewOrderSingle; 15 | 16 | import javax.annotation.Nonnull; 17 | import java.util.HashMap; 18 | import java.util.Map; 19 | import java.util.UUID; 20 | 21 | public class FixServer extends FixAdapter implements Observer { 22 | 23 | private static Logger logger = LoggerFactory.getLogger(FixServer.class); 24 | 25 | private final SocketAcceptor socketAcceptor; 26 | private Map sessionByBroker; 27 | // TODO: this is temporary. Matching units will be outside of FIX server, likely in a different process 28 | private final MatchingUnit matchingUnit; 29 | // TODO: this is temporary until symbol is added to OrderEntry 30 | private final Symbol symbol = new Symbol("MSFT"); 31 | private final Publisher orderPublisher; 32 | 33 | 34 | // TODO: to correctly route execution reports to relevant party looks like I need: 35 | // a) a mapping from OrderId to Broker (TargetCompID) 36 | // b) a mapping from Broker (TargetCompId) to SessionID 37 | 38 | public FixServer(final SessionSettings settings, final Publisher orderPublisher) throws ConfigError { 39 | this.orderPublisher = orderPublisher; 40 | MessageStoreFactory messageStoreFactory = new FileStoreFactory(settings); 41 | SLF4JLogFactory logFactory = new SLF4JLogFactory(settings); 42 | MessageFactory messageFactory = new DefaultMessageFactory(); 43 | 44 | socketAcceptor = new SocketAcceptor(this, messageStoreFactory, settings, logFactory, messageFactory); 45 | sessionByBroker = new HashMap(); 46 | 47 | matchingUnit = new MatchingUnit("MSFT"); 48 | matchingUnit.register(this); 49 | matchingUnit.setTradingMode(TradingMode.Continuous); 50 | matchingUnit.setTradingPhase(TradingPhase.CoreContinuous); 51 | } 52 | 53 | public void start() throws ConfigError { 54 | socketAcceptor.start(); 55 | } 56 | 57 | public void stop() { 58 | socketAcceptor.stop(); 59 | } 60 | 61 | @Override 62 | public void onCreate(SessionID sessionId) { 63 | sessionByBroker.put(sessionId.getTargetCompID(), sessionId); 64 | } 65 | 66 | @Override 67 | public void onMessage(NewOrderSingle order, SessionID sessionID) throws FieldNotFound, UnsupportedMessageType, IncorrectTagValue { 68 | 69 | final String broker = sessionID.getTargetCompID(); 70 | OrderEntry orderEntry = convertToOrderEntry(order, broker); 71 | acceptOrder(orderEntry); 72 | 73 | final String symbol = order.getSymbol().getValue(); 74 | orderPublisher.publish(symbol, orderEntry, OrderEntry.class); 75 | 76 | // TODO: this is going away soon when subscribe is ready on the matching engine side 77 | matchingUnit.addOrder(orderEntry); 78 | } 79 | 80 | @Override 81 | public void next(Trade trade) { 82 | try { 83 | sendExecutionReport(trade, new Side(Side.BUY)); 84 | sendExecutionReport(trade, new Side(Side.SELL)); 85 | } catch (SessionNotFound sessionNotFound) { 86 | logger.error(sessionNotFound.getMessage(), sessionNotFound); 87 | } 88 | } 89 | 90 | @Override 91 | public void error(@Nonnull Throwable throwable) { 92 | throw new RuntimeError(throwable); 93 | } 94 | 95 | @Override 96 | public void finish() { 97 | // Nothing to do 98 | } 99 | 100 | private OrderEntry convertToOrderEntry(NewOrderSingle orderSingle, String broker) throws FieldNotFound { 101 | OrderSide side = orderSingle.getSide().getValue() == Side.BUY ? OrderSide.Buy : OrderSide.Sell; 102 | OrderType orderType = null; 103 | switch (orderSingle.getOrdType().getValue()) { 104 | case OrdType.MARKET: 105 | orderType = new Market(); 106 | break; 107 | case OrdType.LIMIT: 108 | orderType = new Limit(orderSingle.getPrice().getValue()); 109 | break; 110 | } 111 | return new OrderEntry( 112 | side, 113 | broker, 114 | (int) orderSingle.getOrderQty().getValue(), 115 | orderType); 116 | } 117 | 118 | private ExecutionReport buildExecutionReport(final String orderId, 119 | final ExecTransType execTransType, 120 | final ExecType execType, 121 | final OrdStatus ordStatus, 122 | final Side side) { 123 | 124 | return new ExecutionReport( 125 | new OrderID(orderId), 126 | generateExecId(), 127 | execTransType, 128 | execType, 129 | ordStatus, 130 | symbol, 131 | side, 132 | new LeavesQty(), 133 | new CumQty(), 134 | new AvgPx()); 135 | } 136 | 137 | private void acceptOrder(final OrderEntry orderEntry) { 138 | final String broker = orderEntry.getBroker(); 139 | final SessionID sessionID = sessionByBroker.get(broker); 140 | 141 | if (sessionID != null) { 142 | final Side side = new Side(orderEntry.getSide() == OrderSide.Buy ? Side.BUY : Side.SELL); 143 | final ExecutionReport report = buildExecutionReport( 144 | orderEntry.getOrderId(), 145 | new ExecTransType(ExecTransType.NEW), 146 | new ExecType(ExecType.NEW), 147 | new OrdStatus(OrdStatus.NEW), 148 | side); 149 | sendToTarget(report, sessionID); 150 | } 151 | } 152 | 153 | private boolean sendToTarget(ExecutionReport report, SessionID sessionID) { 154 | try { 155 | return Session.sendToTarget(report, sessionID); 156 | } catch (SessionNotFound sessionNotFound) { 157 | throw new RuntimeError(sessionNotFound); 158 | } 159 | } 160 | 161 | private void sendExecutionReport(Trade trade, Side side) throws SessionNotFound { 162 | final boolean buy = side.getValue() == Side.BUY; 163 | final String orderId = buy ? trade.getBuyOrderId() : trade.getSellOrderId(); 164 | final String broker = buy ? trade.getBuyBroker() : trade.getSellBroker(); 165 | final SessionID sessionID = sessionByBroker.get(broker); 166 | if (sessionID != null) { 167 | ExecutionReport executionReport = new ExecutionReport( 168 | new OrderID(orderId), 169 | generateExecId(), 170 | new ExecTransType(ExecTransType.STATUS), 171 | new ExecType(ExecType.PARTIAL_FILL), 172 | new OrdStatus(OrdStatus.PARTIALLY_FILLED), 173 | symbol, 174 | side, 175 | new LeavesQty(), 176 | new CumQty(), 177 | new AvgPx()); 178 | 179 | executionReport.set(new LastShares(trade.getQuantity())); 180 | executionReport.set(new LastPx(trade.getPrice())); 181 | sendToTarget(executionReport, sessionID); 182 | } 183 | } 184 | 185 | private ExecID generateExecId() { 186 | return new ExecID(UUID.randomUUID().toString()); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/fix/server/FixServerApp.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.fix.server; 2 | 3 | import com.euronextclone.messaging.Publisher; 4 | import quickfix.ConfigError; 5 | import quickfix.SessionSettings; 6 | 7 | import java.io.InputStream; 8 | import java.util.Scanner; 9 | 10 | /** 11 | * Created with IntelliJ IDEA. 12 | * User: eprystupa 13 | * Date: 7/26/12 14 | * Time: 9:55 PM 15 | */ 16 | public class FixServerApp { 17 | 18 | public static void main(String[] args) throws ConfigError { 19 | 20 | final InputStream config = FixServerApp.class.getClassLoader().getResourceAsStream("FixServer.cfg"); 21 | 22 | FixServer fixServer = new FixServer( 23 | new SessionSettings(config), 24 | new Publisher("tcp://*:5555")); 25 | fixServer.start(); 26 | 27 | Scanner scanner = new Scanner(System.in); 28 | System.out.println("[Enter] q to exit"); 29 | while (!scanner.nextLine().trim().equals("q")) System.out.println("[Enter] q to exit"); 30 | 31 | System.out.println("Exiting..."); 32 | fixServer.stop(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/framework/Action.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.framework; 2 | 3 | /** 4 | * Created with IntelliJ IDEA. 5 | * User: eprystupa 6 | * Date: 8/6/12 7 | * Time: 8:39 AM 8 | */ 9 | public interface Action { 10 | void invoke(final T t); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/framework/Factory.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.framework; 2 | 3 | /** 4 | * Created with IntelliJ IDEA. 5 | * User: eprystupa 6 | * Date: 8/6/12 7 | * Time: 8:24 AM 8 | */ 9 | public interface Factory { 10 | T build(); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/messaging/Publisher.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.messaging; 2 | 3 | import com.dyuproject.protostuff.LinkedBuffer; 4 | import com.dyuproject.protostuff.ProtostuffIOUtil; 5 | import com.dyuproject.protostuff.Schema; 6 | import com.dyuproject.protostuff.runtime.RuntimeSchema; 7 | import org.zeromq.ZMQ; 8 | 9 | /** 10 | * Created with IntelliJ IDEA. 11 | * User: eprystupa 12 | * Date: 8/5/12 13 | * Time: 4:54 PM 14 | */ 15 | public class Publisher { 16 | 17 | private final ZMQ.Socket publisher; 18 | 19 | public Publisher(final String endpoint) { 20 | final ZMQ.Context context = ZMQ.context(1); 21 | publisher = context.socket(ZMQ.PUB); 22 | publisher.bind(endpoint); 23 | } 24 | 25 | public boolean publish(final String topic, final T payload, final Class typeClass) { 26 | 27 | publisher.send(topic.getBytes(), ZMQ.SNDMORE); 28 | final Schema schema = RuntimeSchema.getSchema(typeClass); 29 | final LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE); 30 | final byte[] message = ProtostuffIOUtil.toByteArray(payload, schema, buffer); 31 | return publisher.send(message, 0); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/messaging/Subscriber.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.messaging; 2 | 3 | import com.dyuproject.protostuff.ProtobufIOUtil; 4 | import com.dyuproject.protostuff.Schema; 5 | import com.dyuproject.protostuff.runtime.RuntimeSchema; 6 | import com.euronextclone.framework.Action; 7 | import com.euronextclone.framework.Factory; 8 | import org.zeromq.ZMQ; 9 | 10 | import java.io.Closeable; 11 | import java.io.IOException; 12 | 13 | /** 14 | * Created with IntelliJ IDEA. 15 | * User: eprystupa 16 | * Date: 8/6/12 17 | * Time: 7:24 AM 18 | */ 19 | public class Subscriber { 20 | 21 | private final ZMQ.Socket subscriber; 22 | 23 | public Subscriber(final String endpoint) { 24 | final ZMQ.Context context = ZMQ.context(1); 25 | subscriber = context.socket(ZMQ.SUB); 26 | subscriber.connect(endpoint); 27 | } 28 | 29 | public Closeable subscribe(final String topic) { 30 | final byte[] topicBytes = topic.getBytes(); 31 | subscriber.subscribe(topicBytes); 32 | 33 | return new Closeable() { 34 | @Override 35 | public void close() throws IOException { 36 | subscriber.unsubscribe(topicBytes); 37 | } 38 | }; 39 | } 40 | 41 | public void run(final Action handler, final Factory factory, final Class typeClass) { 42 | final Schema schema = RuntimeSchema.getSchema(typeClass); 43 | 44 | while (true) { 45 | final byte[] message = subscriber.recv(0); 46 | T event = factory.build(); 47 | ProtobufIOUtil.mergeFrom(message, event, schema); 48 | handler.invoke(event); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/ordertypes/Limit.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.ordertypes; 2 | 3 | import com.euronextclone.OrderSide; 4 | import com.euronextclone.OrderType; 5 | 6 | import java.text.DecimalFormat; 7 | 8 | /** 9 | * Created with IntelliJ IDEA. 10 | * User: eprystupa 11 | * Date: 7/20/12 12 | * Time: 2:32 PM 13 | */ 14 | public class Limit implements OrderType { 15 | 16 | private final double limit; 17 | 18 | public Limit(final double limit) { 19 | this.limit = limit; 20 | } 21 | 22 | @Override 23 | public boolean acceptsMarketPrice() { 24 | return false; 25 | } 26 | 27 | @Override 28 | public boolean providesLimit() { 29 | return true; 30 | } 31 | 32 | @Override 33 | public double getLimit() { 34 | return limit; 35 | } 36 | 37 | @Override 38 | public boolean canPegLimit() { 39 | return false; 40 | } 41 | 42 | @Override 43 | public boolean convertsToLimit() { 44 | return false; 45 | } 46 | 47 | @Override 48 | public Double price(OrderSide side, Double bestLimit) { 49 | return limit; 50 | } 51 | 52 | @Override 53 | public String displayPrice(Double price) { 54 | return new DecimalFormat("#.##").format(price); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/ordertypes/Market.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.ordertypes; 2 | 3 | import com.euronextclone.OrderSide; 4 | import com.euronextclone.OrderType; 5 | 6 | /** 7 | * Created with IntelliJ IDEA. 8 | * User: eprystupa 9 | * Date: 7/20/12 10 | * Time: 2:08 PM 11 | */ 12 | public class Market implements OrderType { 13 | 14 | @Override 15 | public boolean acceptsMarketPrice() { 16 | return true; 17 | } 18 | 19 | @Override 20 | public boolean providesLimit() { 21 | return false; 22 | } 23 | 24 | @Override 25 | public double getLimit() { 26 | throw new RuntimeException("Market orders do not provide limit"); 27 | } 28 | 29 | @Override 30 | public boolean canPegLimit() { 31 | return false; 32 | } 33 | 34 | @Override 35 | public boolean convertsToLimit() { 36 | return false; 37 | } 38 | 39 | @Override 40 | public Double price(OrderSide side, Double bestLimit) { 41 | return null; 42 | } 43 | 44 | @Override 45 | public String displayPrice(Double price) { 46 | return "MO"; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/ordertypes/MarketToLimit.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.ordertypes; 2 | 3 | import com.euronextclone.OrderSide; 4 | import com.euronextclone.OrderType; 5 | 6 | /** 7 | * Created with IntelliJ IDEA. 8 | * User: eprystupa 9 | * Date: 7/20/12 10 | * Time: 2:33 PM 11 | */ 12 | public class MarketToLimit implements OrderType { 13 | 14 | @Override 15 | public boolean acceptsMarketPrice() { 16 | return true; 17 | } 18 | 19 | @Override 20 | public boolean providesLimit() { 21 | return false; 22 | } 23 | 24 | @Override 25 | public double getLimit() { 26 | throw new RuntimeException("MTL orders do not provide limit"); 27 | } 28 | 29 | @Override 30 | public boolean canPegLimit() { 31 | return false; 32 | } 33 | 34 | @Override 35 | public boolean convertsToLimit() { 36 | return true; 37 | } 38 | 39 | @Override 40 | public Double price(OrderSide side, Double bestLimit) { 41 | return null; 42 | } 43 | 44 | @Override 45 | public String displayPrice(Double price) { 46 | return "MTL"; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/ordertypes/Peg.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.ordertypes; 2 | 3 | import com.euronextclone.OrderSide; 4 | import com.euronextclone.OrderType; 5 | 6 | import java.text.DecimalFormat; 7 | 8 | /** 9 | * Created with IntelliJ IDEA. 10 | * User: eprystupa 11 | * Date: 7/20/12 12 | * Time: 2:35 PM 13 | */ 14 | public class Peg implements OrderType { 15 | 16 | private final static DecimalFormat priceFormat = new DecimalFormat("#.##"); 17 | 18 | @Override 19 | public boolean acceptsMarketPrice() { 20 | return false; 21 | } 22 | 23 | @Override 24 | public boolean providesLimit() { 25 | return false; 26 | } 27 | 28 | @Override 29 | public double getLimit() { 30 | throw new RuntimeException("Peg orders do not provide limit"); 31 | } 32 | 33 | @Override 34 | public boolean canPegLimit() { 35 | return true; 36 | } 37 | 38 | @Override 39 | public boolean convertsToLimit() { 40 | return false; 41 | } 42 | 43 | @Override 44 | public Double price(OrderSide side, Double bestLimit) { 45 | return bestLimit; 46 | } 47 | 48 | @Override 49 | public String displayPrice(Double price) { 50 | return String.format("Peg(%s)", priceFormat.format(price)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/euronextclone/ordertypes/PegWithLimit.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone.ordertypes; 2 | 3 | import com.euronextclone.OrderSide; 4 | import com.euronextclone.OrderType; 5 | 6 | import java.text.DecimalFormat; 7 | 8 | /** 9 | * Created with IntelliJ IDEA. 10 | * User: eprystupa 11 | * Date: 7/20/12 12 | * Time: 2:36 PM 13 | */ 14 | public class PegWithLimit implements OrderType { 15 | 16 | private final static DecimalFormat priceFormat = new DecimalFormat("#.##"); 17 | private final double limit; 18 | 19 | public PegWithLimit(final double limit) { 20 | this.limit = limit; 21 | } 22 | 23 | @Override 24 | public boolean acceptsMarketPrice() { 25 | return false; 26 | } 27 | 28 | @Override 29 | public boolean providesLimit() { 30 | return false; 31 | } 32 | 33 | @Override 34 | public double getLimit() { 35 | return limit; 36 | } 37 | 38 | @Override 39 | public boolean canPegLimit() { 40 | return true; 41 | } 42 | 43 | @Override 44 | public boolean convertsToLimit() { 45 | return false; 46 | } 47 | 48 | @Override 49 | public Double price(OrderSide side, Double bestLimit) { 50 | if (side == OrderSide.Buy && bestLimit <= limit) { 51 | return bestLimit; 52 | } else if (side == OrderSide.Sell && bestLimit >= limit) { 53 | return bestLimit; 54 | } else { 55 | return limit; 56 | } 57 | } 58 | 59 | @Override 60 | public String displayPrice(Double price) { 61 | return String.format("Peg(%s)[%s]", priceFormat.format(price), priceFormat.format(limit)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/resources/FixBrokerA.cfg: -------------------------------------------------------------------------------- 1 | [default] 2 | FileStorePath=target/data/fix/broker-a 3 | ConnectionType=initiator 4 | SenderCompID=BROKER-A 5 | TargetCompID=EXCHANGE 6 | SocketConnectHost=localhost 7 | StartTime=00:00:00 8 | EndTime=00:00:00 9 | HeartBtInt=30 10 | ReconnectInterval=5 11 | 12 | [session] 13 | BeginString=FIX.4.2 14 | SocketConnectPort=9878 15 | -------------------------------------------------------------------------------- /src/main/resources/FixBrokerB.cfg: -------------------------------------------------------------------------------- 1 | [default] 2 | FileStorePath=target/data/fix/broker-b 3 | ConnectionType=initiator 4 | SenderCompID=BROKER-B 5 | TargetCompID=EXCHANGE 6 | SocketConnectHost=localhost 7 | StartTime=00:00:00 8 | EndTime=00:00:00 9 | HeartBtInt=30 10 | ReconnectInterval=5 11 | 12 | [session] 13 | BeginString=FIX.4.2 14 | SocketConnectPort=9879 15 | -------------------------------------------------------------------------------- /src/main/resources/FixServer.cfg: -------------------------------------------------------------------------------- 1 | [default] 2 | FileStorePath=target/data/fix/exchange 3 | ConnectionType=acceptor 4 | StartTime=00:00:00 5 | EndTime=00:00:00 6 | HeartBtInt=30 7 | ValidOrderTypes=1,2,F 8 | SenderCompID=EXCHANGE 9 | UseDataDictionary=Y 10 | DefaultMarketPrice=12.30 11 | 12 | [session] 13 | BeginString=FIX.4.2 14 | TargetCompID=BROKER-A 15 | SocketAcceptPort=9878 16 | 17 | [session] 18 | BeginString=FIX.4.2 19 | TargetCompID=BROKER-B 20 | SocketAcceptPort=9879 21 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %date{ISO8601} %-5level[%logger{0}]- %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/test/java/com/euronextclone/ContinuousMatchingStepDefinitions.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone; 2 | 3 | import cucumber.annotation.en.Then; 4 | import cucumber.table.DataTable; 5 | 6 | import java.util.List; 7 | 8 | import static org.hamcrest.Matchers.is; 9 | import static org.junit.Assert.assertThat; 10 | 11 | public class ContinuousMatchingStepDefinitions { 12 | 13 | private final MatchingUnit matchingUnit; 14 | 15 | public ContinuousMatchingStepDefinitions(World world) { 16 | this.matchingUnit = world.getMatchingUnit(); 17 | } 18 | 19 | @Then("^best limits are:$") 20 | public void best_limits_are(DataTable limitsTable) throws Throwable { 21 | final List bestLimitRows = limitsTable.asList(BestLimitRow.class); 22 | for (BestLimitRow bestLimitRow : bestLimitRows) { 23 | assertThat(matchingUnit.getBestLimit(bestLimitRow.side), is(bestLimitRow.limit)); 24 | } 25 | } 26 | 27 | private static class BestLimitRow { 28 | private OrderSide side; 29 | private Double limit; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com/euronextclone/MatchingUnitStepDefinitions.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone; 2 | 3 | import com.euronextclone.ordertypes.*; 4 | import com.google.common.base.Function; 5 | import com.google.common.base.Predicate; 6 | import com.google.common.collect.FluentIterable; 7 | import cucumber.annotation.en.Given; 8 | import cucumber.annotation.en.Then; 9 | import cucumber.annotation.en.When; 10 | import cucumber.table.DataTable; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.regex.Matcher; 15 | import java.util.regex.Pattern; 16 | 17 | import static junit.framework.Assert.assertEquals; 18 | import static org.hamcrest.CoreMatchers.is; 19 | import static org.junit.Assert.assertThat; 20 | 21 | public class MatchingUnitStepDefinitions { 22 | 23 | private final MatchingUnit matchingUnit; 24 | private final List generatedTrades; 25 | 26 | public MatchingUnitStepDefinitions(World world) { 27 | matchingUnit = world.getMatchingUnit(); 28 | generatedTrades = world.getGeneratedTrades(); 29 | } 30 | 31 | @Given("^that trading mode for security is \"([^\"]*)\" and phase is \"([^\"]*)\"$") 32 | public void that_trading_mode_for_security_is_and_phase_is(TradingMode tradingMode, TradingPhase phase) throws Throwable { 33 | matchingUnit.setTradingMode(tradingMode); 34 | matchingUnit.setTradingPhase(phase); 35 | } 36 | 37 | @Given("^that reference price is ([0-9]*\\.?[0-9]+)$") 38 | public void that_reference_price_is_(double price) throws Throwable { 39 | matchingUnit.setReferencePrice(price); 40 | } 41 | 42 | @Given("^the following orders are submitted in this order:$") 43 | public void the_following_orders_are_submitted_in_this_order(DataTable orderTable) throws Throwable { 44 | 45 | final List orderRows = orderTable.asList(OrderEntryRow.class); 46 | for (OrderEntryRow orderRow : orderRows) { 47 | matchingUnit.addOrder(new OrderEntry(orderRow.side, orderRow.broker, orderRow.quantity, orderRow.getOrderType())); 48 | } 49 | } 50 | 51 | @When("^class auction completes$") 52 | public void class_auction_completes() throws Throwable { 53 | matchingUnit.auction(); 54 | } 55 | 56 | @Then("^the calculated IMP is:$") 57 | public void the_calculated_IMP_is(List imp) { 58 | assertThat(matchingUnit.getIndicativeMatchingPrice(), is(imp.get(0))); 59 | } 60 | 61 | @Then("^the following trades are generated:$") 62 | public void the_following_trades_are_generated(DataTable expectedTradesTable) throws Throwable { 63 | final List expectedTrades = expectedTradesTable.asList(TradeRow.class); 64 | final List actualTrades = FluentIterable.from(generatedTrades).transform(TradeRow.FROM_TRADE).toImmutableList(); 65 | assertEquals(expectedTrades, actualTrades); 66 | 67 | generatedTrades.clear(); 68 | } 69 | 70 | @Then("^no trades are generated$") 71 | public void no_trades_are_generated() throws Throwable { 72 | List actualTrades = FluentIterable.from(generatedTrades).transform(TradeRow.FROM_TRADE).toImmutableList(); 73 | List empty = new ArrayList(); 74 | assertEquals(empty, actualTrades); 75 | } 76 | 77 | @Then("^the book is empty$") 78 | public void the_book_is_empty() throws Throwable { 79 | 80 | final List actualBuy = FluentIterable.from(matchingUnit.getOrders(OrderSide.Buy)).transform(OrderBookRow.FROM_Order(matchingUnit)).toImmutableList(); 81 | final List actualSell = FluentIterable.from(matchingUnit.getOrders(OrderSide.Sell)).transform(OrderBookRow.FROM_Order(matchingUnit)).toImmutableList(); 82 | 83 | assertEquals(new ArrayList(), actualBuy); 84 | assertEquals(new ArrayList(), actualSell); 85 | } 86 | 87 | @Then("^the book looks like:$") 88 | public void the_book_looks_like(DataTable expectedBooks) throws Throwable { 89 | 90 | final List headerColumns = new ArrayList(expectedBooks.raw().get(0)); 91 | final int columns = headerColumns.size(); 92 | final int sideSize = columns / 2; 93 | for (int i = 0; i < columns; i++) { 94 | headerColumns.set(i, (i < sideSize ? "Buy " : "Sell ") + headerColumns.get(i)); 95 | } 96 | 97 | final List> raw = new ArrayList>(); 98 | raw.add(headerColumns); 99 | final List> body = expectedBooks.raw().subList(1, expectedBooks.raw().size()); 100 | raw.addAll(body); 101 | 102 | final DataTable montageTable = expectedBooks.toTable(raw); 103 | final List rows = montageTable.asList(MontageRow.class); 104 | final List expectedBids = FluentIterable.from(rows).filter(MontageRow.NON_EMPTY_BID).transform(MontageRow.TO_TEST_BID).toImmutableList(); 105 | final List expectedAsks = FluentIterable.from(rows).filter(MontageRow.NON_EMPTY_ASK).transform(MontageRow.TO_TEST_ASK).toImmutableList(); 106 | 107 | final List actualBuy = FluentIterable.from(matchingUnit.getOrders(OrderSide.Buy)).transform(OrderBookRow.FROM_Order(matchingUnit)).toImmutableList(); 108 | final List actualSell = FluentIterable.from(matchingUnit.getOrders(OrderSide.Sell)).transform(OrderBookRow.FROM_Order(matchingUnit)).toImmutableList(); 109 | 110 | assertEquals(expectedBids, actualBuy); 111 | assertEquals(expectedAsks, actualSell); 112 | } 113 | 114 | private static class MontageRow { 115 | private String buyBroker; 116 | private Integer buyQuantity; 117 | private String buyPrice; 118 | private String sellBroker; 119 | private Integer sellQuantity; 120 | private String sellPrice; 121 | 122 | public static final Predicate NON_EMPTY_BID = new Predicate() { 123 | @Override 124 | public boolean apply(final MontageRow input) { 125 | return input.buyBroker != null && !"".equals(input.buyBroker); 126 | } 127 | }; 128 | 129 | public static final Function TO_TEST_BID = new Function() { 130 | @Override 131 | public OrderBookRow apply(final MontageRow input) { 132 | final OrderBookRow orderRow = new OrderBookRow(); 133 | orderRow.side = OrderSide.Buy; 134 | orderRow.broker = input.buyBroker; 135 | orderRow.price = input.buyPrice; 136 | orderRow.quantity = input.buyQuantity; 137 | return orderRow; 138 | } 139 | }; 140 | 141 | public static final Function TO_TEST_ASK = new Function() { 142 | @Override 143 | public OrderBookRow apply(final MontageRow input) { 144 | final OrderBookRow orderRow = new OrderBookRow(); 145 | orderRow.side = OrderSide.Sell; 146 | orderRow.broker = input.sellBroker; 147 | orderRow.price = input.sellPrice; 148 | orderRow.quantity = input.sellQuantity; 149 | return orderRow; 150 | } 151 | }; 152 | 153 | public static final Predicate NON_EMPTY_ASK = new Predicate() { 154 | @Override 155 | public boolean apply(final MontageRow input) { 156 | return input.sellBroker != null && !"".equals(input.sellBroker); 157 | } 158 | }; 159 | } 160 | 161 | 162 | private static class TradeRow { 163 | private String buyingBroker; 164 | private String sellingBroker; 165 | private int quantity; 166 | private double price; 167 | 168 | public static final Function FROM_TRADE = new Function() { 169 | @Override 170 | public TradeRow apply(Trade input) { 171 | TradeRow tradeRow = new TradeRow(); 172 | tradeRow.setBuyingBroker(input.getBuyBroker()); 173 | tradeRow.setPrice(input.getPrice()); 174 | tradeRow.setQuantity(input.getQuantity()); 175 | tradeRow.setSellingBroker(input.getSellBroker()); 176 | return tradeRow; 177 | } 178 | }; 179 | 180 | @Override 181 | public boolean equals(Object o) { 182 | if (this == o) return true; 183 | if (o == null || getClass() != o.getClass()) return false; 184 | 185 | TradeRow tradeRow = (TradeRow) o; 186 | 187 | if (Double.compare(tradeRow.price, price) != 0) return false; 188 | if (quantity != tradeRow.quantity) return false; 189 | if (!buyingBroker.equals(tradeRow.buyingBroker)) return false; 190 | if (!sellingBroker.equals(tradeRow.sellingBroker)) return false; 191 | 192 | return true; 193 | } 194 | 195 | @Override 196 | public int hashCode() { 197 | int result; 198 | long temp; 199 | result = buyingBroker.hashCode(); 200 | result = 31 * result + sellingBroker.hashCode(); 201 | result = 31 * result + quantity; 202 | temp = price != +0.0d ? Double.doubleToLongBits(price) : 0L; 203 | result = 31 * result + (int) (temp ^ (temp >>> 32)); 204 | return result; 205 | } 206 | 207 | @Override 208 | public String toString() { 209 | return "TradeRow{" + 210 | "buyingBroker='" + buyingBroker + '\'' + 211 | ", sellingBroker='" + sellingBroker + '\'' + 212 | ", quantity=" + quantity + 213 | ", price=" + price + 214 | '}'; 215 | } 216 | 217 | public void setBuyingBroker(String buyingBroker) { 218 | this.buyingBroker = buyingBroker; 219 | } 220 | 221 | public void setSellingBroker(String sellingBroker) { 222 | this.sellingBroker = sellingBroker; 223 | } 224 | 225 | public void setQuantity(int quantity) { 226 | this.quantity = quantity; 227 | } 228 | 229 | public void setPrice(double price) { 230 | this.price = price; 231 | } 232 | } 233 | 234 | private static class OrderEntryRow { 235 | private static final Pattern PEG = Pattern.compile("Peg(?:\\[(.+)\\])?", Pattern.CASE_INSENSITIVE); 236 | private String broker; 237 | private OrderSide side; 238 | private int quantity; 239 | private String price; 240 | 241 | public OrderType getOrderType() { 242 | 243 | Matcher peg = PEG.matcher(price); 244 | if (peg.matches()) { 245 | String limit = peg.group(1); 246 | return limit != null ? new PegWithLimit(Double.parseDouble(limit)) : new Peg(); 247 | } 248 | if ("MTL".compareToIgnoreCase(price) == 0) { 249 | return new MarketToLimit(); 250 | } 251 | if ("MO".compareToIgnoreCase(price) == 0) { 252 | return new Market(); 253 | } 254 | 255 | return new Limit(Double.parseDouble(price)); 256 | } 257 | } 258 | 259 | private static class OrderBookRow { 260 | private String broker; 261 | private OrderSide side; 262 | private int quantity; 263 | private String price; 264 | 265 | public static Function FROM_Order(final MatchingUnit matchingUnit) { 266 | return new Function() { 267 | @Override 268 | public OrderBookRow apply(final Order order) { 269 | final OrderSide side = order.getSide(); 270 | final OrderType orderType = order.getOrderType(); 271 | final Double bestLimit = matchingUnit.getBestLimit(side); 272 | final OrderBookRow orderRow = new OrderBookRow(); 273 | 274 | orderRow.broker = order.getBroker(); 275 | orderRow.side = order.getSide(); 276 | Double price = orderType.price(side, matchingUnit.getBestLimit(side)); 277 | orderRow.price = orderType.displayPrice(price); 278 | orderRow.quantity = order.getQuantity(); 279 | return orderRow; 280 | } 281 | }; 282 | } 283 | 284 | @Override 285 | public boolean equals(Object o) { 286 | if (this == o) return true; 287 | if (o == null || getClass() != o.getClass()) return false; 288 | 289 | OrderBookRow orderRow = (OrderBookRow) o; 290 | 291 | if (quantity != orderRow.quantity) return false; 292 | if (!broker.equals(orderRow.broker)) return false; 293 | if (price != null ? !price.equals(orderRow.price) : orderRow.price != null) return false; 294 | if (side != orderRow.side) return false; 295 | 296 | return true; 297 | } 298 | 299 | @Override 300 | public int hashCode() { 301 | int result = broker.hashCode(); 302 | result = 31 * result + side.hashCode(); 303 | result = 31 * result + quantity; 304 | result = 31 * result + (price != null ? price.hashCode() : 0); 305 | return result; 306 | } 307 | 308 | @Override 309 | public String toString() { 310 | return "OrderBookRow{" + 311 | "broker='" + broker + '\'' + 312 | ", side=" + side + 313 | ", quantity=" + quantity + 314 | ", price=" + price + 315 | '}'; 316 | } 317 | } 318 | } -------------------------------------------------------------------------------- /src/test/java/com/euronextclone/RunAllCukesTest.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone; 2 | 3 | import cucumber.junit.Cucumber; 4 | import org.junit.runner.RunWith; 5 | 6 | @RunWith(Cucumber.class) 7 | public class RunAllCukesTest { 8 | } 9 | -------------------------------------------------------------------------------- /src/test/java/com/euronextclone/RunFocusCukesTest.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone; 2 | 3 | import cucumber.junit.Cucumber; 4 | import org.junit.runner.RunWith; 5 | 6 | @RunWith(Cucumber.class) 7 | @Cucumber.Options(tags = "@focus") 8 | public class RunFocusCukesTest { 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/com/euronextclone/World.java: -------------------------------------------------------------------------------- 1 | package com.euronextclone; 2 | 3 | import cucumber.annotation.Before; 4 | import hu.akarnokd.reactive4java.reactive.Observer; 5 | 6 | import javax.annotation.Nonnull; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class World { 11 | 12 | private final List generatedTrades = new ArrayList(); 13 | private final MatchingUnit matchingUnit = new MatchingUnit("MSFT"); 14 | 15 | public List getGeneratedTrades() { 16 | return generatedTrades; 17 | } 18 | 19 | public MatchingUnit getMatchingUnit() { 20 | return matchingUnit; 21 | } 22 | 23 | @Before 24 | public void setUp() { 25 | matchingUnit.register(new Observer() { 26 | @Override 27 | public void next(Trade trade) { 28 | generatedTrades.add(trade); 29 | } 30 | 31 | @Override 32 | public void error(@Nonnull Throwable throwable) { 33 | } 34 | 35 | @Override 36 | public void finish() { 37 | } 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/resources/com/euronextclone/Euronext - market-to-limit-orders.feature: -------------------------------------------------------------------------------- 1 | Feature: Examples from the Euronext Market-to-Limit PDF 2 | 3 | Scenario: On a class of Securities traded by auction - Example 1 - Only Market to Limit orders in the order book 4 | Given that trading mode for security is "ByAuction" and phase is "CoreCall" 5 | And that reference price is 10 6 | And the following orders are submitted in this order: 7 | | Broker | Side | Quantity | Price | 8 | | A | Buy | 50 | MTL | 9 | | B | Sell | 40 | MTL | 10 | Then the calculated IMP is: 11 | | 10 | 12 | When class auction completes 13 | Then the following trades are generated: 14 | | Buying broker | Selling broker | Quantity | Price | 15 | | A | B | 40 | 10 | 16 | And the book looks like: 17 | | Broker | Quantity | Price | Price | Quantity | Broker | 18 | | A | 10 | 10 | | | | 19 | 20 | 21 | Scenario: On a class of Securities traded by auction - Example 2 - There are Market to limits, Pure Market orders and limited orders 22 | Given that trading mode for security is "ByAuction" and phase is "CoreCall" 23 | And that reference price is 10 24 | And the following orders are submitted in this order: 25 | | Broker | Side | Quantity | Price | 26 | | A | Buy | 10 | MTL | 27 | | B | Buy | 10 | MO | 28 | | C | Buy | 10 | MTL | 29 | | D | Sell | 10 | 10 | 30 | Then the calculated IMP is: 31 | | 10 | 32 | When class auction completes 33 | Then the following trades are generated: 34 | | Buying broker | Selling broker | Quantity | Price | 35 | | A | D | 10 | 10 | 36 | And the book looks like: 37 | | Broker | Quantity | Price | Price | Quantity | Broker | 38 | | B | 10 | MO | | | | 39 | | C | 10 | MTL | | | | 40 | 41 | 42 | Scenario: On a class of Securities traded by auction - Example 3 - There are Market to limits and Pure Market orders 43 | Given that trading mode for security is "ByAuction" and phase is "CoreCall" 44 | And that reference price is 10 45 | And the following orders are submitted in this order: 46 | | Broker | Side | Quantity | Price | 47 | | A | Buy | 10 | MTL | 48 | | B | Buy | 10 | MO | 49 | | C | Buy | 10 | MTL | 50 | | D | Sell | 5 | 10 | 51 | Then the calculated IMP is: 52 | | 10 | 53 | When class auction completes 54 | Then the following trades are generated: 55 | | Buying broker | Selling broker | Quantity | Price | 56 | | A | D | 5 | 10 | 57 | And the book looks like: 58 | | Broker | Quantity | Price | Price | Quantity | Broker | 59 | | B | 10 | MO | | | | 60 | | C | 10 | MTL | | | | 61 | | A | 5 | 10 | | | | 62 | 63 | 64 | Scenario: On a class of Securities traded on a continuous mode – Call phase - Example 1 - Only Market to Limit orders in the order book 65 | Given that trading mode for security is "Continuous" and phase is "CoreCall" 66 | And that reference price is 10 67 | And the following orders are submitted in this order: 68 | | Broker | Side | Quantity | Price | 69 | | A | Buy | 50 | MTL | 70 | | B | Sell | 40 | MTL | 71 | Then the calculated IMP is: 72 | | 10 | 73 | When class auction completes 74 | Then the following trades are generated: 75 | | Buying broker | Selling broker | Quantity | Price | 76 | | A | B | 40 | 10 | 77 | And the book looks like: 78 | | Broker | Quantity | Price | Price | Quantity | Broker | 79 | | A | 10 | 10 | | | | 80 | 81 | 82 | Scenario: On a class of Securities traded on a continuous mode – Call phase - Example 2 - There are Market to limits and Pure Market orders 83 | Given that trading mode for security is "Continuous" and phase is "CoreCall" 84 | And that reference price is 10 85 | And the following orders are submitted in this order: 86 | | Broker | Side | Quantity | Price | 87 | | A | Buy | 10 | MTL | 88 | | B | Buy | 10 | MO | 89 | | C | Buy | 10 | MTL | 90 | | D | Sell | 10 | 10 | 91 | Then the calculated IMP is: 92 | | 10 | 93 | And the book looks like: 94 | | Broker | Quantity | Price | Price | Quantity | Broker | 95 | | A | 10 | MTL | 10 | 10 | D | 96 | | B | 10 | MO | | | | 97 | | C | 10 | MTL | | | | 98 | When class auction completes 99 | Then the following trades are generated: 100 | | Buying broker | Selling broker | Quantity | Price | 101 | | A | D | 10 | 10 | 102 | And the book looks like: 103 | | Broker | Quantity | Price | Price | Quantity | Broker | 104 | | B | 10 | MO | | | | 105 | | C | 10 | 10 | | | | 106 | 107 | 108 | Scenario: On a class of Securities traded on a continuous mode – Call phase - Example 3 - There are Market to limits, Pure Market orders and Limited orders 109 | Given that trading mode for security is "Continuous" and phase is "CoreCall" 110 | And that reference price is 10 111 | And the following orders are submitted in this order: 112 | | Broker | Side | Quantity | Price | 113 | | A | Buy | 10 | MTL | 114 | | B | Buy | 10 | MO | 115 | | C | Buy | 10 | MTL | 116 | | D | Sell | 5 | 10 | 117 | Then the calculated IMP is: 118 | | 10 | 119 | And the book looks like: 120 | | Broker | Quantity | Price | Price | Quantity | Broker | 121 | | A | 10 | MTL | 10 | 5 | D | 122 | | B | 10 | MO | | | | 123 | | C | 10 | MTL | | | | 124 | When class auction completes 125 | Then the following trades are generated: 126 | | Buying broker | Selling broker | Quantity | Price | 127 | | A | D | 5 | 10 | 128 | And the book looks like: 129 | | Broker | Quantity | Price | Price | Quantity | Broker | 130 | | B | 10 | MO | | | | 131 | | A | 5 | 10 | | | | 132 | | C | 10 | 10 | | | | 133 | 134 | 135 | Scenario: On a class of Securities traded on a continuous mode – Continuous phase - Example 1 136 | There are limit orders in the order book 137 | Given that trading mode for security is "Continuous" and phase is "CoreContinuous" 138 | And that reference price is 15 139 | And the following orders are submitted in this order: 140 | | Broker | Side | Quantity | Price | 141 | | A | Buy | 10 | 15 | 142 | | B | Buy | 10 | 12 | 143 | | C | Buy | 10 | 10 | 144 | | D | Sell | 25 | MTL | 145 | Then the following trades are generated: 146 | | Buying broker | Selling broker | Quantity | Price | 147 | | A | D | 10 | 15 | 148 | And the book looks like: 149 | | Broker | Quantity | Price | Price | Quantity | Broker | 150 | | B | 10 | 12 | 15 | 15 | D | 151 | | C | 10 | 10 | | | | 152 | 153 | 154 | Scenario: On a class of Securities traded on a continuous mode – Continuous phase - Example 2 155 | There are limit orders and Pure Market orders in the order book 156 | Given that trading mode for security is "Continuous" and phase is "CoreContinuous" 157 | And that reference price is 12 158 | And the following orders are submitted in this order: 159 | | Broker | Side | Quantity | Price | 160 | | A | Buy | 10 | MO | 161 | | B | Buy | 10 | 12 | 162 | | C | Buy | 10 | 9 | 163 | | D | Sell | 25 | MTL | 164 | And the book looks like: 165 | | Broker | Quantity | Price | Price | Quantity | Broker | 166 | | C | 10 | 9 | 12 | 5 | D | 167 | -------------------------------------------------------------------------------- /src/test/resources/com/euronextclone/Euronext - pure market order.feature: -------------------------------------------------------------------------------- 1 | Feature: Examples from the Euronext Pure Market Order PDF 2 | 3 | Scenario: Call Phase - Example 1 - the Market order is totally filled 4 | Given that trading mode for security is "Continuous" and phase is "CoreCall" 5 | And that reference price is 10 6 | And the following orders are submitted in this order: 7 | | Broker | Side | Quantity | Price | 8 | | A | Buy | 50 | MTL | 9 | | B | Buy | 90 | 10.1 | 10 | | C | Buy | 10 | 9.9 | 11 | | D | Sell | 40 | MTL | 12 | | E | Sell | 100 | 10.08 | 13 | | F | Sell | 60 | 10.15 | 14 | Then the calculated IMP is: 15 | | 10.08 | 16 | And the book looks like: 17 | | Broker | Quantity | Price | Price | Quantity | Broker | 18 | | A | 50 | MTL | MTL | 40 | D | 19 | | B | 90 | 10.1 | 10.08 | 100 | E | 20 | | C | 10 | 9.9 | 10.15 | 60 | F | 21 | When the following orders are submitted in this order: 22 | | Broker | Side | Quantity | Price | 23 | | G | Buy | 20 | MO | 24 | Then the calculated IMP is: 25 | | 10.1 | 26 | And the book looks like: 27 | | Broker | Quantity | Price | Price | Quantity | Broker | 28 | | A | 50 | MTL | MTL | 40 | D | 29 | | G | 20 | MO | 10.08 | 100 | E | 30 | | B | 90 | 10.1 | 10.15 | 60 | F | 31 | | C | 10 | 9.9 | | | | 32 | When class auction completes 33 | Then the following trades are generated: 34 | | Buying broker | Selling broker | Quantity | Price | 35 | | A | D | 40 | 10.1 | 36 | | A | E | 10 | 10.1 | 37 | | G | E | 20 | 10.1 | 38 | | B | E | 70 | 10.1 | 39 | And the book looks like: 40 | | Broker | Quantity | Price | Price | Quantity | Broker | 41 | | B | 20 | 10.1 | 10.15 | 60 | F | 42 | | C | 10 | 9.9 | | | | 43 | 44 | 45 | Scenario: Call Phase - Example 2 - the market order is partially filled 46 | Broker G enters a Market Order for the purchase of 20 shares 47 | Given that trading mode for security is "Continuous" and phase is "CoreCall" 48 | And that reference price is 10 49 | And the following orders are submitted in this order: 50 | | Broker | Side | Quantity | Price | 51 | | A | Buy | 40 | MTL | 52 | | D | Sell | 45 | MO | 53 | Then the book looks like: 54 | | Broker | Quantity | Price | Price | Quantity | Broker | 55 | | A | 40 | MTL | MO | 45 | D | 56 | Then the calculated IMP is: 57 | | 10 | 58 | And the following orders are submitted in this order: 59 | | Broker | Side | Quantity | Price | 60 | | G | Buy | 20 | MO | 61 | Then the calculated IMP is: 62 | | 10 | 63 | And the book looks like: 64 | | Broker | Quantity | Price | Price | Quantity | Broker | 65 | | A | 40 | MTL | MO | 45 | D | 66 | | G | 20 | MO | | | | 67 | When class auction completes 68 | Then the following trades are generated: 69 | | Buying broker | Selling broker | Quantity | Price | 70 | | A | D | 40 | 10 | 71 | | G | D | 5 | 10 | 72 | And the book looks like: 73 | | Broker | Quantity | Price | Price | Quantity | Broker | 74 | | G | 15 | MO | | | | 75 | 76 | 77 | Scenario: Call Phase - Example 3 - the Indicative Matching Price is higher than the best limit & equal to the reference price 78 | Broker G enters a sell Limit order at 9,98€ for the sale of 40 shares 79 | Since the number of shares on the bid side fully covers the number on offer, the Indicative 80 | Matching Price is 10€, this being the closest to the closing price on the previous day. 81 | Given that trading mode for security is "Continuous" and phase is "CoreCall" 82 | And that reference price is 10 83 | And the following orders are submitted in this order: 84 | | Broker | Side | Quantity | Price | 85 | | A | Buy | 40 | MO | 86 | | G | Sell | 40 | 9.98 | 87 | Then the calculated IMP is: 88 | | 10 | 89 | And the book looks like: 90 | | Broker | Quantity | Price | Price | Quantity | Broker | 91 | | A | 40 | MO | 9.98 | 40 | G | 92 | 93 | 94 | Scenario: Call Phase - Example 4 - the Indicative Matching Price is equal to the best limit & lower to the reference price Reference price 95 | Broker G enters a sell Limit order at 9,98€ for the purchase of 41 shares 96 | Since the number of shares on the bid side partly covers the number on the ask side, the 97 | Indicative Matching Price adopted is 9.98€. 98 | Given that trading mode for security is "Continuous" and phase is "CoreCall" 99 | And that reference price is 10 100 | And the following orders are submitted in this order: 101 | | Broker | Side | Quantity | Price | 102 | | A | Buy | 40 | MO | 103 | | G | Sell | 41 | 9.98 | 104 | Then the calculated IMP is: 105 | | 9.98 | 106 | And the book looks like: 107 | | Broker | Quantity | Price | Price | Quantity | Broker | 108 | | A | 40 | MO | 9.98 | 41 | G | 109 | 110 | 111 | Scenario: Call Phase - Example 5 - the Indicative Matching Price is equal to the best limit & higher to the reference price Reference price 112 | Broker G enters a sell Limit order at 10,02€ for the purchase of 40 shares 113 | The Indicative Matching Price adopted within the range [10,02;+∞] is 10.02€, since this is the closest to the reference 114 | price of 10€ and the only price allowing 80 shares to be traded. 115 | Given that trading mode for security is "Continuous" and phase is "CoreCall" 116 | And that reference price is 10 117 | And the following orders are submitted in this order: 118 | | Broker | Side | Quantity | Price | 119 | | A | Buy | 40 | MO | 120 | | G | Sell | 40 | 10.02 | 121 | Then the calculated IMP is: 122 | | 10.02 | 123 | And the book looks like: 124 | | Broker | Quantity | Price | Price | Quantity | Broker | 125 | | A | 40 | MO | 10.02 | 40 | G | 126 | 127 | 128 | Scenario: Trading session Phase - Example 1 - the Market order is totally executed upon entry 129 | Broker C enters a Market order for the purchase of 110 shares 130 | The order is executed with 100 shares at 10,2€ and 10 shares at 10.3€ (in compliance with the 131 | collars). 132 | Given that trading mode for security is "Continuous" and phase is "CoreContinuous" 133 | And that reference price is 10 134 | And the following orders are submitted in this order: 135 | | Broker | Side | Quantity | Price | 136 | | A | Sell | 100 | 10.2 | 137 | | B | Sell | 60 | 10.3 | 138 | When the following orders are submitted in this order: 139 | | Broker | Side | Quantity | Price | 140 | | C | Buy | 110 | MO | 141 | Then the following trades are generated: 142 | | Buying broker | Selling broker | Quantity | Price | 143 | | C | A | 100 | 10.2 | 144 | | C | B | 10 | 10.3 | 145 | And the book looks like: 146 | | Broker | Quantity | Price | Price | Quantity | Broker | 147 | | | | | 10.3 | 50 | B | 148 | 149 | 150 | Scenario: Trading session Phase - Example 2 - the Market order is partially executed upon entry 151 | Broker C enters a Market order for the purchase of 200 shares 152 | The order is partly executed with 100 shares at 10.2€ and 60 at 10.3€ (in compliance with the 153 | collars) 154 | Given that trading mode for security is "Continuous" and phase is "CoreContinuous" 155 | And that reference price is 10 156 | And the following orders are submitted in this order: 157 | | Broker | Side | Quantity | Price | 158 | | A | Sell | 100 | 10.2 | 159 | | B | Sell | 60 | 10.3 | 160 | When the following orders are submitted in this order: 161 | | Broker | Side | Quantity | Price | 162 | | C | Buy | 200 | MO | 163 | Then the following trades are generated: 164 | | Buying broker | Selling broker | Quantity | Price | 165 | | C | A | 100 | 10.2 | 166 | | C | B | 60 | 10.3 | 167 | And the book looks like: 168 | | Broker | Quantity | Price | Price | Quantity | Broker | 169 | | C | 40 | MO | | | | 170 | 171 | 172 | Scenario: Trading session Phase - Example 3 - there are only Market orders in the order book, trade price is the last traded price 173 | Broker C enters a Market order for the sale of 170 shares 174 | The order is partly executed, with 100 shares at 10€ each, representing the reference price witch 175 | is the last price. 176 | Priority of execution for market orders on the bid side is based on the rule of “First Come, first 177 | served”. The un-executed portion of the ask market order, representing 70 shares, remains on the 178 | order book. 179 | Given that trading mode for security is "Continuous" and phase is "CoreContinuous" 180 | And that reference price is 10 181 | And the following orders are submitted in this order: 182 | | Broker | Side | Quantity | Price | 183 | | A | Buy | 90 | MO | 184 | | B | Buy | 10 | MO | 185 | Then the book looks like: 186 | | Broker | Quantity | Price | Price | Quantity | Broker | 187 | | A | 90 | MO | | | | 188 | | B | 10 | MO | | | | 189 | When the following orders are submitted in this order: 190 | | Broker | Side | Quantity | Price | 191 | | C | Sell | 170 | MO | 192 | Then the following trades are generated: 193 | | Buying broker | Selling broker | Quantity | Price | 194 | | A | C | 90 | 10 | 195 | | B | C | 10 | 10 | 196 | 197 | 198 | Scenario: Trading session Phase - Example 4 - the best limit is higher that the last traded price 199 | Broker D enters a limit order at 10€ for the sale of 120 shares 200 | The order is executed, with 100 shares at 10.1€ (90 representing the Market Order and 10 the 201 | limit order at 10.1€) and 20 at 10.08€. 202 | Given that trading mode for security is "Continuous" and phase is "CoreContinuous" 203 | And that reference price is 10 204 | And the following orders are submitted in this order: 205 | | Broker | Side | Quantity | Price | 206 | | A | Buy | 90 | MO | 207 | | B | Buy | 10 | 10.1 | 208 | | C | Buy | 20 | 10.08 | 209 | Then the book looks like: 210 | | Broker | Quantity | Price | Price | Quantity | Broker | 211 | | A | 90 | MO | | | | 212 | | B | 10 | 10.1 | | | | 213 | | C | 20 | 10.08 | | | | 214 | When the following orders are submitted in this order: 215 | | Broker | Side | Quantity | Price | 216 | | D | Sell | 120 | 10 | 217 | Then the following trades are generated: 218 | | Buying broker | Selling broker | Quantity | Price | 219 | | A | D | 90 | 10.1 | 220 | | B | D | 10 | 10.1 | 221 | | C | D | 20 | 10.08 | 222 | And the book is empty 223 | 224 | 225 | Scenario: Trading session Phase - Example 5 - the best limit is lower that the last traded price 226 | Broker D enters a limit order at 10€ for the sale of 120 shares 227 | The order is executed, with 90 shares at 10.3€, 10 shares at 10.1€ and 20 at 10.08€. 228 | Given that trading mode for security is "Continuous" and phase is "CoreContinuous" 229 | And that reference price is 10.3 230 | And the following orders are submitted in this order: 231 | | Broker | Side | Quantity | Price | 232 | | A | Buy | 90 | MO | 233 | | B | Buy | 10 | 10.1 | 234 | | C | Buy | 20 | 10.08 | 235 | Then the book looks like: 236 | | Broker | Quantity | Price | Price | Quantity | Broker | 237 | | A | 90 | MO | | | | 238 | | B | 10 | 10.1 | | | | 239 | | C | 20 | 10.08 | | | | 240 | When the following orders are submitted in this order: 241 | | Broker | Side | Quantity | Price | 242 | | D | Sell | 120 | 10 | 243 | Then the following trades are generated: 244 | | Buying broker | Selling broker | Quantity | Price | 245 | | A | D | 90 | 10.1 | 246 | | B | D | 10 | 10.1 | 247 | | C | D | 20 | 10.08 | 248 | And the book is empty 249 | -------------------------------------------------------------------------------- /src/test/resources/com/euronextclone/Euronext - the peg orders.feature: -------------------------------------------------------------------------------- 1 | Feature: Examples from the Euronext the Peg Orders PDF 2 | See http://www.euronext.com/fic/000/041/609/416094.pdf 3 | 4 | Background: 5 | Given that trading mode for security is "Continuous" and phase is "CoreContinuous" 6 | 7 | Scenario: Example 1 8 | Given that reference price is 10 9 | And the following orders are submitted in this order: 10 | | Broker | Side | Quantity | Price | 11 | | A | Buy | 200 | 10.5 | 12 | | B | Buy | 150 | Peg | 13 | | B | Buy | 70 | Peg | 14 | | B | Buy | 125 | 10.5 | 15 | | C | Sell | 130 | 10.9 | 16 | | C | Sell | 350 | 10.9 | 17 | | D | Sell | 275 | 11 | 18 | And the book looks like: 19 | | Broker | Quantity | Price | Price | Quantity | Broker | 20 | | A | 200 | 10.5 | 10.9 | 130 | C | 21 | | B | 150 | Peg(10.5) | 10.9 | 350 | C | 22 | | B | 70 | Peg(10.5) | 11 | 275 | D | 23 | | B | 125 | 10.5 | | | | 24 | Given the following orders are submitted in this order: 25 | | Broker | Side | Quantity | Price | 26 | | E | Buy | 200 | 10.8 | 27 | Then no trades are generated 28 | And the book looks like: 29 | | Broker | Quantity | Price | Price | Quantity | Broker | 30 | | E | 200 | 10.8 | 10.9 | 130 | C | 31 | | B | 150 | Peg(10.8) | 10.9 | 350 | C | 32 | | B | 70 | Peg(10.8) | 11 | 275 | D | 33 | | A | 200 | 10.5 | | | | 34 | | B | 125 | 10.5 | | | | 35 | Given the following orders are submitted in this order: 36 | | Broker | Side | Quantity | Price | 37 | | G | Buy | 100 | 10.9 | 38 | Then the following trades are generated: 39 | | Buying broker | Selling broker | Quantity | Price | 40 | | G | C | 100 | 10.9 | 41 | And the book looks like: 42 | | Broker | Quantity | Price | Price | Quantity | Broker | 43 | | E | 200 | 10.8 | 10.9 | 30 | C | 44 | | B | 150 | Peg(10.8) | 10.9 | 350 | C | 45 | | B | 70 | Peg(10.8) | 11 | 275 | D | 46 | | A | 200 | 10.5 | | | | 47 | | B | 125 | 10.5 | | | | 48 | Given the following orders are submitted in this order: 49 | | Broker | Side | Quantity | Price | 50 | | G | Sell | 250 | 10.8 | 51 | Then the following trades are generated: 52 | | Buying broker | Selling broker | Quantity | Price | 53 | | E | G | 200 | 10.8 | 54 | | B | G | 50 | 10.8 | 55 | And the book looks like: 56 | | Broker | Quantity | Price | Price | Quantity | Broker | 57 | | A | 200 | 10.5 | 10.9 | 30 | C | 58 | | B | 125 | 10.5 | 10.9 | 350 | C | 59 | | B | 100 | Peg(10.5) | 11 | 275 | D | 60 | | B | 70 | Peg(10.5) | | | | 61 | 62 | Scenario: Example 2 63 | Given that reference price is 10 64 | And the following orders are submitted in this order: 65 | | Broker | Side | Quantity | Price | 66 | | A | Buy | 200 | 11.5 | 67 | | B | Buy | 150 | Peg[11.6] | 68 | | B | Buy | 70 | Peg | 69 | | B | Buy | 125 | 10.5 | 70 | | C | Sell | 130 | 11.8 | 71 | | C | Sell | 350 | 11.9 | 72 | | D | Sell | 275 | 12 | 73 | And the book looks like: 74 | | Broker | Quantity | Price | Price | Quantity | Broker | 75 | | A | 200 | 11.5 | 11.8 | 130 | C | 76 | | B | 150 | Peg(11.5)[11.6] | 11.9 | 350 | C | 77 | | B | 70 | Peg(11.5) | 12 | 275 | D | 78 | | B | 125 | 10.5 | | | | 79 | And the following orders are submitted in this order: 80 | | Broker | Side | Quantity | Price | 81 | | E | Buy | 200 | 11.7 | 82 | And the book looks like: 83 | | Broker | Quantity | Price | Price | Quantity | Broker | 84 | | E | 200 | 11.7 | 11.8 | 130 | C | 85 | | B | 70 | Peg(11.7) | 11.9 | 350 | C | 86 | | B | 150 | Peg(11.6)[11.6] | 12 | 275 | D | 87 | | A | 200 | 11.5 | | | | 88 | | B | 125 | 10.5 | | | | 89 | When the following orders are submitted in this order: 90 | | Broker | Side | Quantity | Price | 91 | | A | Sell | 270 | 11.7 | 92 | Then the following trades are generated: 93 | | Buying broker | Selling broker | Quantity | Price | 94 | | E | A | 200 | 11.7 | 95 | | B | A | 70 | 11.7 | 96 | And the book looks like: 97 | | Broker | Quantity | Price | Price | Quantity | Broker | 98 | | A | 200 | 11.5 | 11.8 | 130 | C | 99 | | B | 150 | Peg(11.5)[11.6] | 11.9 | 350 | C | 100 | | B | 125 | 10.5 | 12 | 275 | D | 101 | 102 | Scenario: Example 3 103 | Given that reference price is 10 104 | And the following orders are submitted in this order: 105 | | Broker | Side | Quantity | Price | 106 | | A | Buy | 200 | 11.5 | 107 | | B | Buy | 150 | Peg[11.6] | 108 | Then the book looks like: 109 | | Broker | Quantity | Price | Price | Quantity | Broker | 110 | | A | 200 | 11.5 | | | | 111 | | B | 150 | Peg(11.5)[11.6] | | | | 112 | And the following orders are submitted in this order: 113 | | Broker | Side | Quantity | Price | 114 | | C | Sell | 200 | 11.5 | 115 | Then the following trades are generated: 116 | | Buying broker | Selling broker | Quantity | Price | 117 | | A | C | 200 | 11.5 | 118 | And the book is empty -------------------------------------------------------------------------------- /src/test/resources/com/euronextclone/_intellij-support.feature: -------------------------------------------------------------------------------- 1 | Feature: Cucumber-JVM support in IntelliJ 2 | 3 | @focus 4 | Scenario: Running only scenarios tagged with @focus 5 | This scenario ensures that there is always at least one cuke marked with @focus -------------------------------------------------------------------------------- /src/test/resources/com/euronextclone/auction-market-order.feature: -------------------------------------------------------------------------------- 1 | Feature: Auction Phase Matching Rules 2 | 3 | Background: 4 | Given that trading mode for security is "ByAuction" and phase is "CoreCall" 5 | And that reference price is 10 6 | 7 | Scenario: Simplest single trade full match on offsetting limit orders 8 | Given the following orders are submitted in this order: 9 | | Broker | Side | Quantity | Price | 10 | | A | Buy | 50 | 10 | 11 | | B | Sell | 50 | 10 | 12 | When class auction completes 13 | Then the following trades are generated: 14 | | Buying broker | Selling broker | Quantity | Price | 15 | | A | B | 50 | 10 | 16 | 17 | Scenario: Simplest single trade partial match on offsetting limit orders 18 | Given the following orders are submitted in this order: 19 | | Broker | Side | Quantity | Price | 20 | | A | Buy | 30 | 10 | 21 | | B | Sell | 50 | 10 | 22 | When class auction completes 23 | Then the following trades are generated: 24 | | Buying broker | Selling broker | Quantity | Price | 25 | | A | B | 30 | 10 | 26 | 27 | Scenario: Simple multi trade full match on offsetting orders (multiple fills due to to sell side) 28 | Given the following orders are submitted in this order: 29 | | Broker | Side | Quantity | Price | 30 | | A | Buy | 30 | 10 | 31 | | B | Sell | 20 | 10 | 32 | | C | Sell | 10 | 10 | 33 | When class auction completes 34 | Then the following trades are generated: 35 | | Buying Broker | Selling Broker | Quantity | Price | 36 | | A | B | 20 | 10 | 37 | | A | C | 10 | 10 | 38 | 39 | Scenario: Simple multi trade full match on offsetting orders (multiple fills due to to buy side) 40 | Given the following orders are submitted in this order: 41 | | Broker | Side | Quantity | Price | 42 | | A | Buy | 30 | 10 | 43 | | B | Buy | 50 | 10 | 44 | | C | Sell | 80 | 10 | 45 | When class auction completes 46 | Then the following trades are generated: 47 | | Buying Broker | Selling Broker | Quantity | Price | 48 | | A | C | 30 | 10 | 49 | | B | C | 50 | 10 | 50 | -------------------------------------------------------------------------------- /src/test/resources/com/euronextclone/basic-order-book-construction.feature: -------------------------------------------------------------------------------- 1 | Feature: Basic order book construction 2 | 3 | Background: 4 | Given that trading mode for security is "ByAuction" and phase is "CoreCall" 5 | 6 | Scenario: Limit buy orders are sorted from high to low 7 | Given the following orders are submitted in this order: 8 | | Broker | Side | Quantity | Price | 9 | | A | Buy | 100 | 10.2 | 10 | | B | Buy | 100 | 10.3 | 11 | | C | Buy | 100 | 10.1 | 12 | | D | Buy | 100 | 10.5 | 13 | Then the book looks like: 14 | | Broker | Quantity | Price | Price | Quantity | Broker | 15 | | D | 100 | 10.5 | | | | 16 | | B | 100 | 10.3 | | | | 17 | | A | 100 | 10.2 | | | | 18 | | C | 100 | 10.1 | | | | 19 | 20 | Scenario: Limit sell orders are sorted from low to high 21 | Given the following orders are submitted in this order: 22 | | Broker | Side | Quantity | Price | 23 | | A | Sell | 100 | 10.2 | 24 | | B | Sell | 100 | 10.3 | 25 | | C | Sell | 100 | 10.1 | 26 | | D | Sell | 100 | 10.5 | 27 | Then the book looks like: 28 | | Broker | Quantity | Price | Price | Quantity | Broker | 29 | | | | | 10.1 | 100 | C | 30 | | | | | 10.2 | 100 | A | 31 | | | | | 10.3 | 100 | B | 32 | | | | | 10.5 | 100 | D | 33 | 34 | 35 | Scenario: Equal limit orders are sorted by their arrival 36 | Given the following orders are submitted in this order: 37 | | Broker | Side | Quantity | Price | 38 | | A | Buy | 100 | 10.2 | 39 | | B | Buy | 100 | 10.2 | 40 | | C | Sell | 100 | 10.3 | 41 | | D | Sell | 100 | 10.3 | 42 | Then the book looks like: 43 | | Broker | Quantity | Price | Price | Quantity | Broker | 44 | | A | 100 | 10.2 | 10.3 | 100 | C | 45 | | B | 100 | 10.2 | 10.3 | 100 | D | 46 | 47 | 48 | Scenario: Market orders are sorted by their arrival 49 | Given the following orders are submitted in this order: 50 | | Broker | Side | Quantity | Price | 51 | | A | Buy | 100 | MO | 52 | | B | Buy | 100 | MTL | 53 | | C | Buy | 100 | MO | 54 | | D | Sell | 100 | MO | 55 | | E | Sell | 100 | MTL | 56 | | F | Sell | 100 | MO | 57 | Then the book looks like: 58 | | Broker | Quantity | Price | Price | Quantity | Broker | 59 | | A | 100 | MO | MO | 100 | D | 60 | | B | 100 | MTL | MTL | 100 | E | 61 | | C | 100 | MO | MO | 100 | F | 62 | -------------------------------------------------------------------------------- /src/test/resources/com/euronextclone/indicative-market-price.feature: -------------------------------------------------------------------------------- 1 | Feature: Calculating Indicative Market Price Used in Auction Phase 2 | 3 | Background: 4 | Given that trading mode for security is "Continuous" and phase is "CoreAuction" 5 | 6 | Scenario: - The Indicative Matching Price should be the one clearing most shares in the presence of multiple sell limit prices 7 | Given that reference price is 10 8 | And the following orders are submitted in this order: 9 | | Broker | Side | Quantity | Price | 10 | | A | Buy | 80 | MO | 11 | | G | Sell | 40 | 10.02 | 12 | | H | Sell | 40 | 10.04 | 13 | Then the calculated IMP is: 14 | | 10.04 | 15 | 16 | Scenario: - The Indicative Matching Price should be the one clearing most shares in the presence of multiple buy limit prices 17 | Given that trading mode for security is "ByAuction" and phase is "CoreCall" 18 | And that reference price is 10 19 | And the following orders are submitted in this order: 20 | | Broker | Side | Quantity | Price | 21 | | A | Buy | 40 | 9.98 | 22 | | B | Buy | 40 | 9.96 | 23 | | G | Sell | 80 | MO | 24 | Then the calculated IMP is: 25 | | 9.96 | 26 | -------------------------------------------------------------------------------- /src/test/resources/com/euronextclone/pure-market-orders.feature: -------------------------------------------------------------------------------- 1 | Feature: Pure market orders 2 | 3 | Scenario: Resting market order is partially filled by incoming limit order 4 | Given the following orders are submitted in this order: 5 | | Broker | Side | Quantity | Price | 6 | | A | Buy | 100 | MO | 7 | | B | Sell | 60 | 10.3 | 8 | Then the following trades are generated: 9 | | Buying broker | Selling broker | Quantity | Price | 10 | | A | B | 60 | 10.3 | 11 | And the book looks like: 12 | | Broker | Quantity | Price | Price | Quantity | Broker | 13 | | A | 40 | MO | | | | 14 | --------------------------------------------------------------------------------