├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── net │ │ └── quedex │ │ └── api │ │ ├── common │ │ ├── CommunicationException.java │ │ ├── Config.java │ │ ├── DisconnectedException.java │ │ ├── ListenerException.java │ │ ├── MaintenanceException.java │ │ ├── MessageReceiver.java │ │ ├── StreamFailureListener.java │ │ └── WebsocketStream.java │ │ ├── market │ │ ├── Instrument.java │ │ ├── InstrumentsListener.java │ │ ├── MarketMessageReceiver.java │ │ ├── MarketStream.java │ │ ├── OrderBook.java │ │ ├── OrderBookListener.java │ │ ├── PriceQuantity.java │ │ ├── Quotes.java │ │ ├── QuotesListener.java │ │ ├── Registration.java │ │ ├── SessionState.java │ │ ├── SessionStateListener.java │ │ ├── SpotDataListener.java │ │ ├── SpotDataWrapper.java │ │ ├── Trade.java │ │ ├── TradeListener.java │ │ └── WebsocketMarketStream.java │ │ ├── pgp │ │ ├── BcDecryptor.java │ │ ├── BcEncryptor.java │ │ ├── BcPrivateKey.java │ │ ├── BcPublicKey.java │ │ ├── BcSignatureVerifier.java │ │ ├── PGPDecryptionException.java │ │ ├── PGPEncryptionException.java │ │ ├── PGPExceptionBase.java │ │ ├── PGPInvalidSignatureException.java │ │ ├── PGPKeyInitialisationException.java │ │ ├── PGPKeyNotFoundException.java │ │ ├── PGPSignatureVerificationException.java │ │ └── PGPUnknownRecipientException.java │ │ └── user │ │ ├── AccountState.java │ │ ├── AccountStateListener.java │ │ ├── CancelAllOrdersFailed.java │ │ ├── CancelAllOrdersSpec.java │ │ ├── InternalTransfer.java │ │ ├── InternalTransferExecuted.java │ │ ├── InternalTransferListener.java │ │ ├── InternalTransferReceived.java │ │ ├── InternalTransferRejected.java │ │ ├── LimitOrderSpec.java │ │ ├── LiquidationOrderCancelled.java │ │ ├── LiquidationOrderFilled.java │ │ ├── LiquidationOrderPlaced.java │ │ ├── OpenPosition.java │ │ ├── OpenPositionForcefullyClosed.java │ │ ├── OpenPositionListener.java │ │ ├── OrderCancelFailed.java │ │ ├── OrderCancelSpec.java │ │ ├── OrderCancelled.java │ │ ├── OrderFilled.java │ │ ├── OrderForcefullyCancelled.java │ │ ├── OrderListener.java │ │ ├── OrderModificationFailed.java │ │ ├── OrderModificationSpec.java │ │ ├── OrderModified.java │ │ ├── OrderPlaceFailed.java │ │ ├── OrderPlaced.java │ │ ├── OrderSide.java │ │ ├── OrderSpec.java │ │ ├── OrderType.java │ │ ├── TimerAdded.java │ │ ├── TimerCancelFailed.java │ │ ├── TimerCancelled.java │ │ ├── TimerExpired.java │ │ ├── TimerListener.java │ │ ├── TimerRejected.java │ │ ├── TimerTriggered.java │ │ ├── TimerUpdateFailed.java │ │ ├── TimerUpdated.java │ │ ├── UserMessageReceiver.java │ │ ├── UserMessageSender.java │ │ ├── UserStream.java │ │ └── WebsocketUserStream.java └── resources │ └── qdxConfig.properties.example └── test ├── java └── net │ └── quedex │ └── api │ ├── market │ ├── Fixtures.java │ ├── MarketMessageReceiverTest.java │ └── WebsocketMarketStreamIT.java │ ├── testcommons │ ├── Keys.java │ └── Utils.java │ └── user │ ├── UserMessageReceiverTest.java │ ├── UserMessageSenderTest.java │ └── WebsocketUserStreamIT.java └── resources ├── logback-test.xml └── qdxConfig.properties.example /.gitignore: -------------------------------------------------------------------------------- 1 | # idea 2 | .idea 3 | *.iml 4 | 5 | # gradle 6 | build/ 7 | .gradle/ 8 | out/ 9 | 10 | *.properties -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk8 4 | - openjdk11 5 | - oraclejdk11 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quedex Official Java API 2 | 3 | > The best way to communicate with [Quedex Bitcoin Derivatives Exchange](https://quedex.net) 4 | using Java. 5 | 6 | ## Important! 7 | 8 | * Next to this documentation, please read the [general documentation](https://quedex.net/doc/api) of our WebSocket API. 9 | * Quedex Exchange uses an innovative [schedule of session states][faq-session-schedule]. Some 10 | session states employ different order matching model - namely, [Auction][faq-what-is-auction]. 11 | Please consider this when placing orders. 12 | 13 | ## Getting the API 14 | 15 | Include in your project as a Maven dependency: 16 | 17 | ``` 18 | 19 | net.quedex 20 | java-api 21 | 0.7.0 22 | 23 | 24 | ``` 25 | 26 | ## Using the API: 27 | 28 | To use the API you need to provide a configuration - the default way to do that is via a `.properties` file. An example 29 | may be found in [qdxConfig.properties.example][example-config] - rename this file to `qdxConfig.propertie`, place on 30 | your classpath and fill in the following (the rest of the properties is done): 31 | * `net.quedex.client.api.accountId` 32 | * `net.quedex.client.api.userPrivateKey` 33 | 34 | You may find your account id and encrypted private key in our web application - on the trading dashboard select the 35 | dropdown menu with your email address in the upper right corner and go to User Profile (equivalent to visiting 36 | https://quedex.app/profile when logged in). 37 | 38 | Now you are ready to start hacking: 39 | 40 | ```java 41 | char[] pwd = ... // read private key passphrase 42 | Config qdxConfig = Config.fromResource(pwd); // initialise the config from qdxConfig.properties using one of the factory methods 43 | 44 | MarketStream marketStream = new WebsocketMarketStream(qdxConfig); 45 | UserStream userStream = new WebsocketUserStream(qdxConfig); 46 | 47 | // register stream failure listeners 48 | marketStream.registerStreamFailureListener(...); 49 | userStream.registerStreamFailureListener(...) 50 | 51 | // start streams 52 | marketStream.start(); 53 | userStream.start(); 54 | 55 | // receive tradable instruments 56 | marketStream.registerInstrumentsListener(instruments -> { 57 | 58 | // register and subscribe other market stream listeners 59 | marketStream.registerQuotesListener(...).subscribe(instruments.keySet()); // to subscribe all instruments 60 | marketStream.register*(...).subscribe(...); 61 | }); 62 | 63 | // register user stream listeners 64 | userStream.registerOpenPositionListener(...); 65 | userStream.register*(...); 66 | 67 | // subscribe user stream listeners; see Javadoc for details 68 | userStream.subscribeListeners(); 69 | 70 | // play with the streams: receive events, place orders and so on 71 | userStream.placeOrder(...); 72 | userStream.batch() 73 | .placeOrder(...) 74 | ... 75 | .send(); 76 | ... 77 | 78 | // once finished, stop the streams 79 | userStream.stop(); 80 | marketStream.stop(); 81 | ``` 82 | 83 | ## Contributing Guide 84 | 85 | Default channel for submitting **questions regarding the API** is [opening new issues][new-issue]. 86 | In cases when information disclosure is not possible, you can contact us at support@quedex.net. 87 | 88 | In case you need to add a feature to the API, please [submit an issue][new-issue] 89 | containing change proposal before submitting a PR. 90 | 91 | Pull requests containing bugfixes are very welcome! 92 | 93 | ## License 94 | 95 | Copyright © 2017-2019 Quedex Ltd. API is released under [Apache License Version 2.0](LICENSE). 96 | 97 | [inverse-notation-docs]: https://quedex.net/doc/inverse_notation 98 | [faq-session-schedule]: https://quedex.net/faq#session_schedule 99 | [faq-what-is-auction]: https://quedex.net/faq#what_is_auction 100 | [example-config]: src/main/resources/qdxConfig.properties.example 101 | [new-issue]: https://github.com/quedexnet/python-api/issues/new 102 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'maven' 3 | apply plugin: 'signing' 4 | 5 | group 'net.quedex' 6 | version '0.9.0' 7 | ext.isReleaseVersion = !version.endsWith("SNAPSHOT") 8 | 9 | ext { 10 | if (!project.hasProperty('ossrhUsername')) { 11 | ossrhUsername = '' 12 | } 13 | 14 | if (!project.hasProperty('ossrhPassword')) { 15 | ossrhPassword = '' 16 | } 17 | } 18 | 19 | sourceCompatibility = 1.8 20 | targetCompatibility = 1.8 21 | 22 | repositories { 23 | mavenCentral() 24 | } 25 | 26 | dependencies { 27 | compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.21' 28 | compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.8.0' 29 | compile group: 'org.bouncycastle', name: 'bcpg-jdk15on', version: '1.54' 30 | compile group: 'com.google.guava', name: 'guava', version: '19.0' 31 | compile group: 'org.java-websocket', name: 'Java-WebSocket', version: '1.3.0' 32 | 33 | testCompile group: 'org.testng', name: 'testng', version: '6.9.10' 34 | testCompile group: 'ch.qos.logback', name: 'logback-classic', version: '1.1.7' 35 | testCompile group: 'org.mockito', name: 'mockito-all', version: '1.10.19' 36 | testCompile group: 'org.assertj', name: 'assertj-core', version: '3.5.2' 37 | testCompile group: 'junit', name: 'junit', version: '4.12' // mockito depends on it 38 | } 39 | 40 | test { 41 | useTestNG() 42 | 43 | testLogging.showStandardStreams = true 44 | } 45 | 46 | task javadocJar(type: Jar) { 47 | classifier = 'javadoc' 48 | from javadoc 49 | } 50 | 51 | task sourcesJar(type: Jar) { 52 | classifier = 'sources' 53 | from sourceSets.main.allSource 54 | } 55 | 56 | signing { 57 | required { isReleaseVersion && gradle.taskGraph.hasTask("uploadArchives") } 58 | sign configurations.archives 59 | } 60 | 61 | artifacts { 62 | archives sourcesJar 63 | archives javadocJar 64 | } 65 | 66 | uploadArchives { 67 | repositories { 68 | mavenDeployer { 69 | beforeDeployment { 70 | MavenDeployment deployment -> signing.signPom(deployment) 71 | } 72 | 73 | repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { 74 | authentication(userName: ossrhUsername, password: ossrhPassword) 75 | } 76 | 77 | snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { 78 | authentication(userName: ossrhUsername, password: ossrhPassword) 79 | } 80 | 81 | pom.project { 82 | name = 'java-api' 83 | packaging = 'pom' 84 | // optionally artifactId can be defined here 85 | description = 'Quedex Java API.' 86 | url = 'https://github.com/quedexnet/java-api' 87 | 88 | scm { 89 | connection = 'scm:git:https://github.com/wgromniak/java-api.git' 90 | developerConnection = 'scm:git:https://github.com/quedexnet/java-api.git' 91 | url = 'https://github.com/quedexnet/java-api' 92 | } 93 | 94 | licenses { 95 | license { 96 | name = 'The MIT License' 97 | url = 'https://opensource.org/licenses/MIT' 98 | } 99 | } 100 | 101 | developers { 102 | developer { 103 | id = 'wgromniak' 104 | name = 'Wiktor Gromniak' 105 | email = 'wgromniak@quedex.net' 106 | url = 'https://github.com/wgromniak' 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } 113 | 114 | wrapper { 115 | gradleVersion '2.14' 116 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quedexnet/java-api/f1a79d142703ead7a300203c48ebf2716b58de55/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Sep 06 16:46:34 CEST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'java-api' 2 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/common/CommunicationException.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.common; 2 | 3 | import java.io.IOException; 4 | 5 | public class CommunicationException extends IOException { 6 | 7 | public CommunicationException() { 8 | } 9 | 10 | public CommunicationException(String message) { 11 | super(message); 12 | } 13 | 14 | public CommunicationException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | 18 | public CommunicationException(Throwable cause) { 19 | super(cause); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/common/Config.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.common; 2 | 3 | import com.google.common.base.MoreObjects; 4 | import com.google.common.base.Objects; 5 | import com.google.common.io.Resources; 6 | import net.quedex.api.pgp.BcPrivateKey; 7 | import net.quedex.api.pgp.BcPublicKey; 8 | import net.quedex.api.pgp.PGPKeyInitialisationException; 9 | 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.util.Properties; 13 | 14 | import static com.google.common.base.Preconditions.checkArgument; 15 | import static com.google.common.base.Preconditions.checkNotNull; 16 | 17 | public class Config { 18 | 19 | private final String marketStreamUrl; 20 | private final String userStreamUrl; 21 | private final BcPublicKey qdxPublicKey; 22 | private final BcPrivateKey userPrivateKey; 23 | private final long accountId; 24 | private final int nonceGroup; 25 | 26 | /** 27 | * @param nonceGroup value between 0 and 9, has to be different for every WebSocket connection opened to the 28 | * exchange (e.g. browser and trading bot); our webapp uses nonce_group=0 29 | */ 30 | public Config(String marketStreamUrl, 31 | String userStreamUrl, 32 | BcPublicKey qdxPublicKey, 33 | BcPrivateKey userPrivateKey, 34 | long accountId, 35 | int nonceGroup) { 36 | checkArgument(!marketStreamUrl.isEmpty(), "Empty marketStreamUrl"); 37 | checkArgument(!userStreamUrl.isEmpty(), "Empty userStreamUrl"); 38 | checkArgument(accountId > 0, "accountId=%s <= 0", accountId); 39 | checkArgument(nonceGroup >= 0, "accountId=%s < 0", nonceGroup); 40 | this.marketStreamUrl = marketStreamUrl + "?keepalive=true"; 41 | this.userStreamUrl = userStreamUrl + "?keepalive=true"; 42 | this.qdxPublicKey = checkNotNull(qdxPublicKey, "null qdxPublicKey"); 43 | this.userPrivateKey = checkNotNull(userPrivateKey, "null userPrivateKey"); 44 | this.accountId = accountId; 45 | this.nonceGroup = nonceGroup; 46 | } 47 | 48 | public static Config fromResource(String resourceName, char[] prvKeyPasspharse) { 49 | try { 50 | return fromInputStream(Resources.getResource(resourceName).openStream(), prvKeyPasspharse); 51 | } catch (IOException e) { 52 | throw new IllegalArgumentException("Error reading resource=" + resourceName, e); 53 | } 54 | } 55 | 56 | public static Config fromResource(char[] prvKeyPasspharse) { 57 | return fromResource("qdxConfig.properties", prvKeyPasspharse); 58 | } 59 | 60 | public static Config fromInputStream(InputStream inputStream, char[] prvKeyPasspharse) { 61 | Properties props = new Properties(); 62 | try { 63 | props.load(inputStream); 64 | } catch (IOException e) { 65 | throw new IllegalStateException("Error reading properties file", e); 66 | } 67 | 68 | try { 69 | return new Config( 70 | props.getProperty("net.quedex.client.api.marketStreamUrl"), 71 | props.getProperty("net.quedex.client.api.userStreamUrl"), 72 | BcPublicKey.fromArmored(props.getProperty("net.quedex.client.api.qdxPublicKey")), 73 | BcPrivateKey.fromArmored(props.getProperty("net.quedex.client.api.userPrivateKey"), prvKeyPasspharse), 74 | Long.parseLong(props.getProperty("net.quedex.client.api.accountId")), 75 | Integer.parseInt(props.getProperty("net.quedex.client.api.nonceGroup")) 76 | ); 77 | } catch (PGPKeyInitialisationException e) { 78 | throw new IllegalArgumentException("Error instantiating keys", e); 79 | } 80 | } 81 | 82 | public String getMarketStreamUrl() { 83 | return marketStreamUrl; 84 | } 85 | 86 | public String getUserStreamUrl() { 87 | return userStreamUrl; 88 | } 89 | 90 | public BcPublicKey getQdxPublicKey() { 91 | return qdxPublicKey; 92 | } 93 | 94 | public BcPrivateKey getUserPrivateKey() { 95 | return userPrivateKey; 96 | } 97 | 98 | public long getAccountId() { 99 | return accountId; 100 | } 101 | 102 | public int getNonceGroup() { 103 | return nonceGroup; 104 | } 105 | 106 | @Override 107 | public boolean equals(Object o) { 108 | if (this == o) { 109 | return true; 110 | } 111 | if (o == null || getClass() != o.getClass()) { 112 | return false; 113 | } 114 | Config config = (Config) o; 115 | return accountId == config.accountId && 116 | nonceGroup == config.nonceGroup && 117 | Objects.equal(marketStreamUrl, config.marketStreamUrl) && 118 | Objects.equal(userStreamUrl, config.userStreamUrl) && 119 | Objects.equal(qdxPublicKey, config.qdxPublicKey) && 120 | Objects.equal(userPrivateKey, config.userPrivateKey); 121 | } 122 | 123 | @Override 124 | public int hashCode() { 125 | return Objects.hashCode( 126 | marketStreamUrl, 127 | userStreamUrl, 128 | qdxPublicKey, 129 | userPrivateKey, 130 | accountId, 131 | nonceGroup 132 | ); 133 | } 134 | 135 | @Override 136 | public String toString() { 137 | return MoreObjects.toStringHelper(this) 138 | .add("marketStreamUrl", marketStreamUrl) 139 | .add("userStreamUrl", userStreamUrl) 140 | .add("qdxPublicKey", qdxPublicKey) 141 | .add("userPrivateKey", userPrivateKey) 142 | .add("accountId", accountId) 143 | .add("nonceGroup", nonceGroup) 144 | .toString(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/common/DisconnectedException.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.common; 2 | 3 | /** 4 | * Indicates lost connection to the exchange in which case a reconnect should be attempted. 5 | */ 6 | public class DisconnectedException extends CommunicationException { 7 | 8 | public DisconnectedException() { 9 | } 10 | 11 | public DisconnectedException(final String message) { 12 | super(message); 13 | } 14 | 15 | public DisconnectedException(final String message, final Throwable cause) { 16 | super(message, cause); 17 | } 18 | 19 | public DisconnectedException(final Throwable cause) { 20 | super(cause); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/common/ListenerException.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.common; 2 | 3 | /** 4 | * Indicates problems with listener implementation provided by user. Listener failed to handle incoming data and threw 5 | * an exception. No listener should throw any exceptions. 6 | */ 7 | public class ListenerException extends Exception { 8 | 9 | public ListenerException() { 10 | } 11 | 12 | public ListenerException(String message) { 13 | super(message); 14 | } 15 | 16 | public ListenerException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | public ListenerException(Throwable cause) { 21 | super(cause); 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/common/MaintenanceException.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.common; 2 | 3 | public class MaintenanceException extends DisconnectedException { 4 | 5 | public MaintenanceException() { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/common/MessageReceiver.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.common; 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import net.quedex.api.pgp.PGPExceptionBase; 7 | import org.slf4j.Logger; 8 | 9 | import java.io.IOException; 10 | 11 | import static com.google.common.base.Preconditions.checkNotNull; 12 | 13 | public abstract class MessageReceiver { 14 | 15 | public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() 16 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 17 | 18 | private final Logger logger; 19 | 20 | private volatile StreamFailureListener streamFailureListener; 21 | 22 | protected MessageReceiver(Logger logger) { 23 | this.logger = checkNotNull(logger, "null logger"); 24 | } 25 | 26 | protected abstract void processData(String data) throws IOException, PGPExceptionBase, ListenerException; 27 | 28 | public final void processMessage(String message) { 29 | 30 | try { 31 | JsonNode metaJson = OBJECT_MAPPER.readTree(message); 32 | 33 | switch (metaJson.get("type").asText()) { 34 | case "data": 35 | processData(metaJson.get("data").asText()); 36 | break; 37 | case "error": 38 | processError(metaJson.get("error_code").asText()); 39 | break; 40 | case "keepalive": 41 | logger.trace("keepalive with server time: {}", metaJson.get("timestamp").asLong()); 42 | break; 43 | default: 44 | // no-op 45 | break; 46 | } 47 | } catch (IOException e) { 48 | onError(new CommunicationException("Error parsing json entity on message=" + message, e)); 49 | } catch (PGPExceptionBase e) { 50 | onError(new CommunicationException("PGP error on message=" + message, e)); 51 | } catch (RuntimeException e) { 52 | onError(new CommunicationException("Error processing message=" + message, e)); 53 | } catch (ListenerException e) { 54 | onError(e); 55 | } 56 | } 57 | 58 | private void processError(String errorCode) { 59 | logger.trace("processError({})", errorCode); 60 | if ("maintenance".equals(errorCode)) { 61 | onError(new MaintenanceException()); 62 | } else { 63 | onError(new CommunicationException("Received server processing error: " + errorCode)); 64 | } 65 | } 66 | 67 | private void onError(Exception e) { 68 | logger.warn("onError({})", e); 69 | StreamFailureListener streamFailureListener = this.streamFailureListener; 70 | if (streamFailureListener != null) { 71 | streamFailureListener.onStreamFailure(e); 72 | } 73 | } 74 | 75 | public final void registerStreamFailureListener(StreamFailureListener streamFailureListener) { 76 | this.streamFailureListener = streamFailureListener; 77 | } 78 | 79 | protected void runListener(Runnable listenerUpdate) throws ListenerException { 80 | try { 81 | listenerUpdate.run(); 82 | } catch (final RuntimeException exception) { 83 | throw new ListenerException("Listener failed to process data", exception); 84 | } 85 | } 86 | 87 | protected void runListenerAndPassExceptionsToFailureListener(Runnable listenerUpdate) { 88 | try { 89 | listenerUpdate.run(); 90 | } catch (final RuntimeException exception) { 91 | onError(new ListenerException("Listener failed to process data", exception)); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/common/StreamFailureListener.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.common; 2 | 3 | @FunctionalInterface 4 | public interface StreamFailureListener { 5 | 6 | /** 7 | * @param exception indicates that an error happened during operations of a stream; 8 | * If this exception is an instance of {@link net.quedex.api.common.DisconnectedException} 9 | * this means that the stream got disconnected (due to network errors, exchange going down 10 | * for maintenance, etc.) and the stream should be reconnected. 11 | * If this exception is an instance of {@link net.quedex.api.common.ListenerException} this means 12 | * that an error from one of listeners provided by user was thrown. 13 | */ 14 | void onStreamFailure(Exception exception); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/common/WebsocketStream.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.common; 2 | 3 | import org.java_websocket.client.DefaultSSLWebSocketClientFactory; 4 | import org.java_websocket.client.WebSocketClient; 5 | import org.java_websocket.drafts.Draft_17; 6 | import org.java_websocket.handshake.ServerHandshake; 7 | import org.slf4j.Logger; 8 | 9 | import javax.net.ssl.SSLContext; 10 | 11 | import java.io.IOException; 12 | import java.net.URI; 13 | import java.security.KeyManagementException; 14 | import java.security.NoSuchAlgorithmException; 15 | import java.util.concurrent.ExecutorService; 16 | import java.util.concurrent.Executors; 17 | 18 | import static com.google.common.base.Preconditions.checkNotNull; 19 | 20 | public class WebsocketStream { 21 | 22 | private final Logger logger; 23 | 24 | protected final WebSocketClient webSocketClient; 25 | private final ExecutorService webSocketClientFactoryExec; 26 | protected final T messageReceiver; 27 | 28 | private volatile StreamFailureListener streamFailureListener; 29 | 30 | protected WebsocketStream( 31 | Logger logger, 32 | String streamUrl, 33 | T messageReceiver 34 | ) { 35 | // TODO: Java-Websockets holds infinitely growing queues 36 | webSocketClient = new WebSocketClient(URI.create(streamUrl), new Draft_17()) { 37 | @Override 38 | public void onOpen(ServerHandshake handshakedata) { 39 | logger.info( 40 | "Websocket opened with url={}, httpStatus={}, httpStatusMessage={}", 41 | streamUrl, handshakedata.getHttpStatus(), handshakedata.getHttpStatusMessage() 42 | ); 43 | } 44 | 45 | @Override 46 | public void onMessage(String message) { 47 | WebsocketStream.this.processMessage(message); 48 | } 49 | 50 | @Override 51 | public void onClose(int code, String reason, boolean remote) { 52 | if (remote) { 53 | WebsocketStream.this.onError( 54 | new DisconnectedException("Websocket closed with code=" + code + ", reason=" + reason) 55 | ); 56 | } else { 57 | logger.info("Websocket closed with code={}, reason={}", code, reason); 58 | } 59 | } 60 | 61 | @Override 62 | public void onError(Exception ex) { 63 | if (ex instanceof IOException) { 64 | // this is a case of ungraceful disconnect 65 | WebsocketStream.this.onError(new DisconnectedException("Websocket error", ex)); 66 | } else { 67 | WebsocketStream.this.onError(new CommunicationException("Websocket error", ex)); 68 | } 69 | } 70 | }; 71 | 72 | webSocketClientFactoryExec = Executors.newSingleThreadExecutor(); 73 | if (streamUrl.startsWith("wss")) { 74 | initSsl(); 75 | } 76 | 77 | this.messageReceiver = checkNotNull(messageReceiver, "null messageReceiver"); 78 | this.logger = checkNotNull(logger, "null logger"); 79 | } 80 | 81 | private void initSsl() { 82 | try { 83 | SSLContext ssl = SSLContext.getInstance("TLS"); 84 | ssl.init(null, null, null); 85 | DefaultSSLWebSocketClientFactory webSocketClientFactory = 86 | new DefaultSSLWebSocketClientFactory(ssl, webSocketClientFactoryExec); 87 | webSocketClient.setWebSocketFactory(webSocketClientFactory); 88 | } catch (NoSuchAlgorithmException | KeyManagementException e) { 89 | throw new IllegalStateException("Error initialising SSL", e); 90 | } 91 | } 92 | 93 | public void registerStreamFailureListener(StreamFailureListener streamFailureListener) { 94 | this.streamFailureListener = streamFailureListener; 95 | messageReceiver.registerStreamFailureListener(streamFailureListener); 96 | } 97 | 98 | public void start() throws CommunicationException { 99 | logger.trace("Starting"); 100 | try { 101 | webSocketClient.connectBlocking(); 102 | } catch (InterruptedException e) { 103 | Thread.interrupted(); 104 | } 105 | logger.info("Started"); 106 | } 107 | 108 | public void stop() throws CommunicationException { 109 | logger.trace("Stopping"); 110 | // has to be closed this way because of incompatibilities in WS protocol 111 | webSocketClient.close(); 112 | webSocketClient.getConnection().closeConnection(1000, ""); 113 | try { 114 | webSocketClient.closeBlocking(); 115 | } catch (InterruptedException e) { 116 | Thread.currentThread().interrupt(); 117 | } 118 | webSocketClientFactoryExec.shutdown(); 119 | logger.info("Stopped"); 120 | } 121 | 122 | private void processMessage(String message) { 123 | messageReceiver.processMessage(message); 124 | } 125 | 126 | private void onError(Exception e) { 127 | StreamFailureListener streamFailureListener = this.streamFailureListener; 128 | if (streamFailureListener != null) { 129 | streamFailureListener.onStreamFailure(e); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/market/Instrument.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.market; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | 7 | import java.math.BigDecimal; 8 | 9 | import static com.google.common.base.Preconditions.checkArgument; 10 | import static com.google.common.base.Preconditions.checkNotNull; 11 | 12 | public final class Instrument { 13 | 14 | public static final int SETTLEMENT_HOUR_UTC_MILLIS = 8 * 60 * 60 * 1000; 15 | 16 | public enum Type { 17 | INVERSE_FUTURES, 18 | INVERSE_OPTION; 19 | 20 | @JsonCreator 21 | private static Type deserialize(String value) { 22 | return valueOf(value.toUpperCase()); 23 | } 24 | } 25 | 26 | public enum OptionType { 27 | CALL_EUROPEAN, 28 | PUT_EUROPEAN; 29 | 30 | @JsonCreator 31 | private static OptionType deserialize(String value) { 32 | return valueOf(value.toUpperCase()); 33 | } 34 | } 35 | 36 | private final int instrumentId; 37 | private final String symbol; 38 | private final Type type; 39 | private final BigDecimal tickSize; 40 | private final long issueDate; 41 | private final long expirationDate; 42 | private final String underlyingSymbol; 43 | private final int notionalAmount; 44 | private final BigDecimal feeFraction; 45 | private final BigDecimal takerToMakerFeeFraction; 46 | private final BigDecimal initialMarginFraction; 47 | private final BigDecimal maintenanceMarginFraction; 48 | private final BigDecimal strike; // nullable, option only 49 | private final OptionType optionType; // nullable, option only 50 | 51 | @JsonCreator 52 | public Instrument( 53 | @JsonProperty("symbol") String symbol, 54 | @JsonProperty("instrument_id") int instrumentId, 55 | @JsonProperty("type") Type type, 56 | @JsonProperty("option_type") OptionType optionType, 57 | @JsonProperty("tick_size") BigDecimal tickSize, 58 | @JsonProperty("issue_date") long issueDate, 59 | @JsonProperty("expiration_date") long expirationDate, 60 | @JsonProperty("underlying_symbol") String underlyingSymbol, 61 | @JsonProperty("notional_amount") int notionalAmount, 62 | @JsonProperty("fee") BigDecimal feeFraction, 63 | @JsonProperty("taker_to_maker") BigDecimal takerToMakerFraction, 64 | @JsonProperty("initial_margin") BigDecimal initialMarginFraction, 65 | @JsonProperty("maintenance_margin") BigDecimal maintenanceMarginFraction, 66 | @JsonProperty("strike") BigDecimal strike 67 | ) { 68 | checkArgument(!symbol.isEmpty(), "Empty symbol"); 69 | checkArgument(tickSize.compareTo(BigDecimal.ZERO) > 0, "tickSize=%s <=0", tickSize); 70 | checkArgument(issueDate > 0, "issueDate=%s <= 0", issueDate); 71 | checkArgument(expirationDate > issueDate, "expirationDate=%s <= %s=issueDate", expirationDate, issueDate); 72 | checkArgument(!underlyingSymbol.isEmpty(), "Empty underlyingSymbol"); 73 | checkArgument(notionalAmount > 0, "notionalAmount=%s <= 0", notionalAmount); 74 | checkArgument(feeFraction.compareTo(BigDecimal.ZERO) >= 0, "feeFraction=%s < 0", takerToMakerFraction); 75 | checkArgument(takerToMakerFraction.compareTo(BigDecimal.ZERO) >= 0, "takerToMaker=%s < 0", takerToMakerFraction); 76 | checkArgument( 77 | initialMarginFraction.compareTo(BigDecimal.ZERO) >= 0, 78 | "initialMarginFraction=%s < 0", initialMarginFraction 79 | ); 80 | checkArgument( 81 | maintenanceMarginFraction.compareTo(BigDecimal.ZERO) >= 0, 82 | "maintenanceMarginFraction=%s < 0", maintenanceMarginFraction 83 | ); 84 | 85 | this.symbol = symbol; 86 | this.instrumentId = instrumentId; 87 | this.type = checkNotNull(type, "null instrumentType"); 88 | this.tickSize = tickSize; 89 | this.issueDate = issueDate; 90 | this.expirationDate = expirationDate; 91 | this.underlyingSymbol = underlyingSymbol; 92 | this.notionalAmount = notionalAmount; 93 | this.feeFraction = feeFraction; 94 | this.takerToMakerFeeFraction = takerToMakerFraction; 95 | this.initialMarginFraction = initialMarginFraction; 96 | this.maintenanceMarginFraction = maintenanceMarginFraction; 97 | 98 | if (this.type == Type.INVERSE_FUTURES) { 99 | checkArgument(strike == null, "Expected null strike"); 100 | checkArgument(optionType == null, "Expected null optionType"); 101 | this.strike = null; 102 | this.optionType = null; 103 | } else { 104 | checkArgument(strike.compareTo(BigDecimal.ZERO) > 0, "strike=%s <= 0", strike); 105 | this.strike = strike; 106 | this.optionType = checkNotNull(optionType, "null optionType"); 107 | } 108 | } 109 | 110 | public String getSymbol() { 111 | return symbol; 112 | } 113 | 114 | public int getInstrumentId() { 115 | return instrumentId; 116 | } 117 | 118 | public Type getType() { 119 | return type; 120 | } 121 | 122 | public BigDecimal getTickSize() { 123 | return tickSize; 124 | } 125 | 126 | public long getIssueDate() { 127 | return issueDate; 128 | } 129 | 130 | public long getExpirationDate() { 131 | return expirationDate; 132 | } 133 | 134 | public String getUnderlyingSymbol() { 135 | return underlyingSymbol; 136 | } 137 | 138 | public int getNotionalAmount() { 139 | return notionalAmount; 140 | } 141 | 142 | public BigDecimal getFeeFraction() { 143 | return feeFraction; 144 | } 145 | 146 | public BigDecimal getTakerToMakerFeeFraction() { 147 | return takerToMakerFeeFraction; 148 | } 149 | 150 | /** 151 | * @return taker fee fraction 152 | */ 153 | public BigDecimal getTakerFeeFraction() { 154 | return feeFraction.add(takerToMakerFeeFraction); 155 | } 156 | 157 | /** 158 | * @return maker fee fraction (may be negative) 159 | */ 160 | public BigDecimal getMakerFeeFraction() { 161 | return feeFraction.subtract(takerToMakerFeeFraction); 162 | } 163 | 164 | public BigDecimal getInitialMarginFraction() { 165 | return initialMarginFraction; 166 | } 167 | 168 | public BigDecimal getMaintenanceMarginFraction() { 169 | return maintenanceMarginFraction; 170 | } 171 | 172 | /** 173 | * @return strike value if the instrument is option, null otherwise 174 | */ 175 | public BigDecimal getStrike() { 176 | return strike; 177 | } 178 | 179 | /** 180 | * @return type of the option if the instrument is option, null otherwise 181 | */ 182 | public OptionType getOptionType() { 183 | return optionType; 184 | } 185 | 186 | public boolean isFutures() { 187 | return type == Type.INVERSE_FUTURES; 188 | } 189 | 190 | public boolean isTraded(long currentTimeMillis) { 191 | return issueDate < currentTimeMillis && currentTimeMillis < expirationDate + SETTLEMENT_HOUR_UTC_MILLIS; 192 | } 193 | 194 | @Override 195 | public boolean equals(Object o) { 196 | if (this == o) return true; 197 | if (o == null || getClass() != o.getClass()) return false; 198 | Instrument that = (Instrument) o; 199 | return instrumentId == that.instrumentId; 200 | } 201 | 202 | @Override 203 | public int hashCode() { 204 | return Integer.hashCode(instrumentId); 205 | } 206 | 207 | @Override 208 | public String toString() { 209 | return MoreObjects.toStringHelper(this) 210 | .add("instrumentId", instrumentId) 211 | .add("symbol", symbol) 212 | .add("instrumentType", type) 213 | .add("tickSize", tickSize) 214 | .add("issueDate", issueDate) 215 | .add("expirationDate", expirationDate) 216 | .add("underlyingSymbol", underlyingSymbol) 217 | .add("notionalAmount", notionalAmount) 218 | .add("feeFraction", feeFraction) 219 | .add("takerToMakerFeeFraction", takerToMakerFeeFraction) 220 | .add("initialMarginFraction", initialMarginFraction) 221 | .add("maintenanceMarginFraction", maintenanceMarginFraction) 222 | .add("strike", strike) 223 | .add("optionType", optionType) 224 | .toString(); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/market/InstrumentsListener.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.market; 2 | 3 | import java.util.Map; 4 | 5 | @FunctionalInterface 6 | public interface InstrumentsListener { 7 | 8 | void onInstruments(Map instruments); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/market/MarketStream.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.market; 2 | 3 | import net.quedex.api.common.CommunicationException; 4 | import net.quedex.api.common.StreamFailureListener; 5 | 6 | /** 7 | * Represents the stream of realtime public trade data streamed from Quedex and allows registering and subscribing for 8 | * particular data types. The registered listeners will be called (in a single thread) for every event that arrives. The 9 | * data come in form of PGP-clearsigned JSON messages - all the verification and deserialization is handled by the 10 | * implementations and the listeners receive Java objects. 11 | *

12 | * The stream gives the following guarantees useful for state initialisation: 13 | *

22 | *

23 | * To handle all errors properly, always {@link #registerStreamFailureListener} before {@link #start}ing the stream. 24 | */ 25 | public interface MarketStream { 26 | 27 | void registerStreamFailureListener(StreamFailureListener streamFailureListener); 28 | 29 | void start() throws CommunicationException; 30 | 31 | void registerInstrumentsListener(InstrumentsListener instrumentsListener); 32 | 33 | Registration registerOrderBookListener(OrderBookListener orderBookListener); 34 | 35 | Registration registerTradeListener(TradeListener tradeListener); 36 | 37 | Registration registerQuotesListener(QuotesListener quotesListener); 38 | 39 | void registerSpotDataListener(SpotDataListener spotDataListener); 40 | 41 | void registerAndSubscribeSessionStateListener(SessionStateListener sessionStateListener); 42 | 43 | void stop() throws CommunicationException; 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/market/OrderBook.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.market; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import java.util.List; 9 | 10 | import static com.google.common.base.Preconditions.checkNotNull; 11 | 12 | public class OrderBook { 13 | 14 | private final int instrumentId; 15 | private final List bids; 16 | private final List asks; 17 | 18 | @JsonCreator 19 | public OrderBook( 20 | @JsonProperty("instrument_id") int instrumentId, 21 | @JsonProperty("bids") List bids, 22 | @JsonProperty("asks") List asks 23 | ) { 24 | this.instrumentId = instrumentId; 25 | this.bids = checkNotNull(bids, "null bids"); 26 | this.asks = checkNotNull(asks, "null asks"); 27 | } 28 | 29 | public int getInstrumentId() { 30 | return instrumentId; 31 | } 32 | 33 | public List getBids() { 34 | return bids; 35 | } 36 | 37 | public List getAsks() { 38 | return asks; 39 | } 40 | 41 | @Override 42 | public boolean equals(Object o) { 43 | if (this == o) return true; 44 | if (o == null || getClass() != o.getClass()) return false; 45 | OrderBook orderBook = (OrderBook) o; 46 | return instrumentId == orderBook.instrumentId && 47 | Objects.equal(bids, orderBook.bids) && 48 | Objects.equal(asks, orderBook.asks); 49 | } 50 | 51 | @Override 52 | public int hashCode() { 53 | return Objects.hashCode(instrumentId, bids, asks); 54 | } 55 | 56 | @Override 57 | public String toString() { 58 | return MoreObjects.toStringHelper(this) 59 | .add("instrumentId", instrumentId) 60 | .add("bids", bids) 61 | .add("asks", asks) 62 | .toString(); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/market/OrderBookListener.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.market; 2 | 3 | @FunctionalInterface 4 | public interface OrderBookListener { 5 | 6 | void onOrderBook(OrderBook orderBook); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/market/PriceQuantity.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.market; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.google.common.base.Objects; 5 | 6 | import java.math.BigDecimal; 7 | 8 | import static com.google.common.base.Preconditions.checkArgument; 9 | import static com.google.common.base.Preconditions.checkNotNull; 10 | 11 | public final class PriceQuantity { 12 | 13 | private final BigDecimal price; // may be null for market order 14 | private final int quantity; 15 | 16 | public PriceQuantity(BigDecimal price, int quantity) { 17 | this.price = checkNotNull(price, "null price"); 18 | this.quantity = quantity; 19 | } 20 | 21 | public PriceQuantity(int quantity) { 22 | this.price = null; 23 | this.quantity = quantity; 24 | } 25 | 26 | @JsonCreator 27 | private PriceQuantity(BigDecimal[] priceQty) { 28 | checkArgument(priceQty.length == 2, "priceQty.length=%s != 2", priceQty.length); 29 | this.price = priceQty[0]; 30 | this.quantity = priceQty[1].intValueExact(); 31 | } 32 | 33 | /** 34 | * @return price, null if absent 35 | */ 36 | public BigDecimal getPrice() { 37 | return price; 38 | } 39 | 40 | public int getQuantity() { 41 | return quantity; 42 | } 43 | 44 | @Override 45 | public boolean equals(Object o) { 46 | if (this == o) return true; 47 | if (o == null || getClass() != o.getClass()) return false; 48 | PriceQuantity priceQuantity = (PriceQuantity) o; 49 | return quantity == priceQuantity.quantity && 50 | Objects.equal(price, priceQuantity.price); 51 | } 52 | 53 | @Override 54 | public int hashCode() { 55 | return Objects.hashCode(price, quantity); 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return "[" + price + ',' + quantity + ']'; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/market/Quotes.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.market; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import java.math.BigDecimal; 9 | 10 | import static com.google.common.base.Preconditions.checkArgument; 11 | 12 | public class Quotes { 13 | 14 | private final int instrumentId; 15 | private final BigDecimal last; 16 | private final int lastQuantity; 17 | private final BigDecimal bid; 18 | private final Integer bidQuantity; 19 | private final BigDecimal ask; 20 | private final Integer askQuantity; 21 | private final int volume; 22 | private final int openInterest; 23 | private final BigDecimal tap; 24 | private final BigDecimal lowerLimit; 25 | private final BigDecimal upperLimit; 26 | 27 | @JsonCreator 28 | public Quotes( 29 | @JsonProperty("instrument_id") int instrumentId, 30 | @JsonProperty("last") BigDecimal last, 31 | @JsonProperty("last_quantity") int lastQuantity, 32 | @JsonProperty("bid") BigDecimal bid, 33 | @JsonProperty("bid_quantity") Integer bidQuantity, 34 | @JsonProperty("ask") BigDecimal ask, 35 | @JsonProperty("ask_quantity") Integer askQuantity, 36 | @JsonProperty("volume") int volume, 37 | @JsonProperty("open_interest") int openInterest, 38 | @JsonProperty("tap") BigDecimal tap, 39 | @JsonProperty("lower_limit") BigDecimal lowerLimit, 40 | @JsonProperty("upper_limit") BigDecimal upperLimit 41 | ) { 42 | checkArgument(last.compareTo(BigDecimal.ZERO) > 0, "last=%s <= 0", last); 43 | checkArgument(lastQuantity >= 0, "lastQuantity=%s < 0", lastQuantity); // may be 0 when reference trade 44 | checkArgument(volume >= 0, "volume=%s < 0", volume); 45 | checkArgument(bid == null || bid.compareTo(BigDecimal.ZERO) > 0, "bid=%s <= 0", bid); 46 | checkArgument(bidQuantity == null || bidQuantity > 0, "bidQuantity=%s <= 0", bidQuantity); 47 | checkArgument(ask == null || ask.compareTo(BigDecimal.ZERO) > 0, "ask=%s <= 0", ask); 48 | checkArgument(askQuantity == null || askQuantity > 0, "askQuantity=%s <= 0", askQuantity); 49 | checkArgument(openInterest >= 0, "openInterest=%s < 0", openInterest); 50 | checkArgument(tap == null || tap.compareTo(BigDecimal.ZERO) > 0, "tap=%s <= 0", tap); 51 | checkArgument(lowerLimit == null || lowerLimit.compareTo(BigDecimal.ZERO) > 0, "lowerLimit=%s <= 0", lowerLimit); 52 | checkArgument(upperLimit == null || upperLimit.compareTo(BigDecimal.ZERO) > 0, "upperLimit=%s <= 0", upperLimit); 53 | this.instrumentId = instrumentId; 54 | this.last = last; 55 | this.lastQuantity = lastQuantity; 56 | this.bid = bid; 57 | this.bidQuantity = bidQuantity; 58 | this.ask = ask; 59 | this.askQuantity = askQuantity; 60 | this.volume = volume; 61 | this.openInterest = openInterest; 62 | this.tap = tap; 63 | this.lowerLimit = lowerLimit; 64 | this.upperLimit = upperLimit; 65 | } 66 | 67 | public int getInstrumentId() { 68 | return instrumentId; 69 | } 70 | 71 | public BigDecimal getLast() { 72 | return last; 73 | } 74 | 75 | public int getLastQuantity() { 76 | return lastQuantity; 77 | } 78 | 79 | /** 80 | * @return fist level of the order book if present, null otherwise 81 | */ 82 | public PriceQuantity getBid() { 83 | return bidQuantity == null ? null : new PriceQuantity(bid, bidQuantity); 84 | } 85 | 86 | /** 87 | * @return fist level of the order book if present, null otherwise 88 | */ 89 | public PriceQuantity getAsk() { 90 | return askQuantity == null ? null : new PriceQuantity(ask, askQuantity); 91 | } 92 | 93 | public int getVolume() { 94 | return volume; 95 | } 96 | 97 | public int getOpenInterest() { 98 | return openInterest; 99 | } 100 | 101 | public BigDecimal getTap() { 102 | return tap; 103 | } 104 | 105 | public BigDecimal getLowerLimit() { 106 | return lowerLimit; 107 | } 108 | 109 | public BigDecimal getUpperLimit() { 110 | return upperLimit; 111 | } 112 | 113 | @Override 114 | public boolean equals(Object o) { 115 | if (this == o) return true; 116 | if (o == null || getClass() != o.getClass()) return false; 117 | Quotes quotes = (Quotes) o; 118 | return instrumentId == quotes.instrumentId && 119 | lastQuantity == quotes.lastQuantity && 120 | volume == quotes.volume && 121 | openInterest == quotes.openInterest && 122 | Objects.equal(last, quotes.last) && 123 | Objects.equal(bid, quotes.bid) && 124 | Objects.equal(bidQuantity, quotes.bidQuantity) && 125 | Objects.equal(ask, quotes.ask) && 126 | Objects.equal(askQuantity, quotes.askQuantity) && 127 | Objects.equal(tap, quotes.tap) && 128 | Objects.equal(lowerLimit, quotes.lowerLimit) && 129 | Objects.equal(upperLimit, quotes.upperLimit); 130 | } 131 | 132 | @Override 133 | public int hashCode() { 134 | return Objects.hashCode( 135 | instrumentId, 136 | last, 137 | lastQuantity, 138 | bid, 139 | bidQuantity, 140 | ask, 141 | askQuantity, 142 | volume, 143 | openInterest, 144 | tap, 145 | lowerLimit, 146 | upperLimit 147 | ); 148 | } 149 | 150 | @Override 151 | public String toString() { 152 | return MoreObjects.toStringHelper(this) 153 | .add("instrumentId", instrumentId) 154 | .add("last", last) 155 | .add("lastQuantity", lastQuantity) 156 | .add("bid", bid) 157 | .add("bidQuantity", bidQuantity) 158 | .add("ask", ask) 159 | .add("askQuantity", askQuantity) 160 | .add("volume", volume) 161 | .add("openInterest", openInterest) 162 | .add("tap", tap) 163 | .add("lowerLimit", lowerLimit) 164 | .add("upperLimit", upperLimit) 165 | .toString(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/market/QuotesListener.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.market; 2 | 3 | @FunctionalInterface 4 | public interface QuotesListener { 5 | 6 | void onQuotes(Quotes quotes); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/market/Registration.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.market; 2 | 3 | import java.util.Collection; 4 | 5 | /** 6 | * A registration of a single market stream listener. May be subscribed and unsubscribed for particular instruments. 7 | */ 8 | public interface Registration { 9 | 10 | Registration subscribe(int instrumentId); 11 | 12 | Registration unsubscribe(int instrumentId); 13 | 14 | Registration subscribe(Collection instrumentIds); 15 | 16 | Registration unsubscribe(Collection instrumentIds); 17 | 18 | Registration unsubscribeAll(); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/market/SessionState.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.market; 2 | 3 | public enum SessionState { 4 | OPENING_AUCTION, CONTINUOUS, AUCTION, CLOSING_AUCTION, NO_TRADING, MAINTENANCE 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/market/SessionStateListener.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.market; 2 | 3 | @FunctionalInterface 4 | public interface SessionStateListener { 5 | 6 | void onSessionState(SessionState newSessionState); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/market/SpotDataListener.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.market; 2 | 3 | @FunctionalInterface 4 | public interface SpotDataListener { 5 | 6 | void onSpotData(SpotDataWrapper spotDataWrapper); 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/market/SpotDataWrapper.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.market; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import java.math.BigDecimal; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | import static com.google.common.base.Preconditions.checkArgument; 13 | import static com.google.common.base.Preconditions.checkNotNull; 14 | 15 | public class SpotDataWrapper { 16 | 17 | // maps from underlying symbol to SpotData 18 | private final @JsonProperty("spot_data") Map spotData; 19 | private final @JsonProperty("update_time") long timestamp; 20 | 21 | @JsonCreator 22 | public SpotDataWrapper(final @JsonProperty("spot_data") Map spotData, 23 | final @JsonProperty("update_time") long timestamp) { 24 | 25 | checkArgument(timestamp >= 0, "timestamp=%s < 0", timestamp); 26 | this.spotData = checkNotNull(spotData, "spotData"); 27 | this.timestamp = timestamp; 28 | } 29 | 30 | public Map getSpotData() { 31 | return spotData; 32 | } 33 | public long getTimestamp() { 34 | return timestamp; 35 | } 36 | 37 | @Override 38 | public boolean equals(final Object o) { 39 | if (this == o) { 40 | return true; 41 | } 42 | if (o == null || getClass() != o.getClass()) { 43 | return false; 44 | } 45 | final SpotDataWrapper that = (SpotDataWrapper) o; 46 | return Objects.equal(spotData, that.spotData) && 47 | timestamp == that.timestamp; 48 | } 49 | 50 | @Override 51 | public int hashCode() { 52 | return Objects.hashCode(spotData, timestamp); 53 | } 54 | 55 | @Override 56 | public String toString() { 57 | return MoreObjects.toStringHelper(this) 58 | .add("spotData", spotData) 59 | .add("timestamp", timestamp) 60 | .toString(); 61 | } 62 | 63 | public static class SpotData { 64 | private final BigDecimal spotIndex; 65 | private final BigDecimal spotIndexChange; 66 | private final BigDecimal settlementIndex; 67 | private final BigDecimal settlementIndexChange; 68 | private final List constituents; 69 | private final Map spotQuotes; 70 | 71 | @JsonCreator 72 | public SpotData(final @JsonProperty("spot_index") BigDecimal spotIndex, 73 | final @JsonProperty("spot_index_change") BigDecimal spotIndexChange, 74 | final @JsonProperty("settlement_index") BigDecimal settlementIndex, 75 | final @JsonProperty("settlement_index_change") BigDecimal settlementIndexChange, 76 | final @JsonProperty("constituents") List constituents, 77 | final @JsonProperty("spot_quotes") Map spotQuotes) { 78 | checkArgument(spotIndex != null && spotIndex.compareTo(BigDecimal.ZERO) > 0, "spotIndex=%s <= 0", spotIndex); 79 | checkArgument(settlementIndex != null && settlementIndex.compareTo(BigDecimal.ZERO) > 0, "settlementIndex=%s <= 0", settlementIndex); 80 | this.spotIndex = spotIndex; 81 | this.spotIndexChange = checkNotNull(spotIndexChange, "spotIndexChange"); 82 | this.settlementIndex = settlementIndex; 83 | this.settlementIndexChange = checkNotNull(settlementIndexChange, "settlementIndexChange"); 84 | this.constituents = checkNotNull(constituents, "constituents"); 85 | this.spotQuotes = checkNotNull(spotQuotes, "spotQuotes"); 86 | } 87 | 88 | public BigDecimal getSpotIndex() { 89 | return spotIndex; 90 | } 91 | public BigDecimal getSpotIndexChange() { 92 | return spotIndexChange; 93 | } 94 | public BigDecimal getSettlementIndex() { 95 | return settlementIndex; 96 | } 97 | public BigDecimal getSettlementIndexChange() { 98 | return settlementIndexChange; 99 | } 100 | public List getConstituents() { 101 | return constituents; 102 | } 103 | public Map getSpotQuotes() { 104 | return spotQuotes; 105 | } 106 | 107 | @Override 108 | public boolean equals(final Object o) { 109 | if (this == o) { 110 | return true; 111 | } 112 | if (o == null || getClass() != o.getClass()) { 113 | return false; 114 | } 115 | final SpotData that = (SpotData) o; 116 | return Objects.equal(spotIndex, that.spotIndex) && 117 | Objects.equal(spotIndexChange, that.spotIndexChange) && 118 | Objects.equal(settlementIndex, that.settlementIndex) && 119 | Objects.equal(settlementIndexChange, that.settlementIndexChange) && 120 | Objects.equal(constituents, that.constituents) && 121 | Objects.equal(spotQuotes, that.spotQuotes); 122 | } 123 | 124 | @Override 125 | public int hashCode() { 126 | return Objects.hashCode(spotIndex, spotIndexChange, settlementIndex, settlementIndexChange, constituents, spotQuotes); 127 | } 128 | 129 | @Override 130 | public String toString() { 131 | return MoreObjects.toStringHelper(this) 132 | .add("spotIndex", spotIndex) 133 | .add("spotIndexChange", spotIndexChange) 134 | .add("settlementIndex", settlementIndex) 135 | .add("settlementIndexChange", settlementIndexChange) 136 | .add("constituents", constituents) 137 | .add("spotQuotes", spotQuotes) 138 | .toString(); 139 | } 140 | } 141 | } 142 | 143 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/market/Trade.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.market; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import java.math.BigDecimal; 9 | 10 | import static com.google.common.base.Preconditions.checkArgument; 11 | import static com.google.common.base.Preconditions.checkNotNull; 12 | 13 | public final class Trade { 14 | 15 | public enum LiquidityProvider { 16 | BUYER, SELLER, AUCTION, REFERENCE, SETTLEMENT; 17 | 18 | @JsonCreator 19 | private static LiquidityProvider deserialize(String value) { 20 | return valueOf(value.toUpperCase()); 21 | } 22 | } 23 | 24 | private final int instrumentId; 25 | private final long tradeId; 26 | private final long timestamp; 27 | private final BigDecimal price; 28 | private final int quantity; 29 | 30 | private final LiquidityProvider liquidityProvider; 31 | 32 | @JsonCreator 33 | public Trade( 34 | @JsonProperty("instrument_id") int instrumentId, 35 | @JsonProperty("trade_id") long tradeId, 36 | @JsonProperty("timestamp") long timestamp, 37 | @JsonProperty("price") BigDecimal price, 38 | @JsonProperty("quantity") int quantity, 39 | @JsonProperty("liquidity_provider") LiquidityProvider liquidityProvider 40 | ) { 41 | checkArgument(quantity >= 0, "quantity=%s < 0", quantity); 42 | this.instrumentId = instrumentId; 43 | this.tradeId = tradeId; 44 | this.timestamp = timestamp; 45 | this.price = checkNotNull(price, "null price"); 46 | this.quantity = quantity; 47 | this.liquidityProvider = checkNotNull(liquidityProvider, "null liquidityProvider"); 48 | } 49 | 50 | public int getInstrumentId() { 51 | return instrumentId; 52 | } 53 | 54 | public long getTradeId() { 55 | return tradeId; 56 | } 57 | 58 | public long getTimestamp() { 59 | return timestamp; 60 | } 61 | 62 | public BigDecimal getPrice() { 63 | return price; 64 | } 65 | 66 | public int getQuantity() { 67 | return quantity; 68 | } 69 | 70 | public LiquidityProvider getLiquidityProvider() { 71 | return liquidityProvider; 72 | } 73 | 74 | @Override 75 | public boolean equals(Object o) { 76 | if (this == o) return true; 77 | if (o == null || getClass() != o.getClass()) return false; 78 | Trade trade = (Trade) o; 79 | return tradeId == trade.tradeId; 80 | } 81 | 82 | public boolean equalsFieldByField(Object o) { 83 | if (this == o) return true; 84 | if (o == null || getClass() != o.getClass()) return false; 85 | Trade trade = (Trade) o; 86 | return instrumentId == trade.instrumentId && 87 | tradeId == trade.tradeId && 88 | timestamp == trade.timestamp && 89 | quantity == trade.quantity && 90 | Objects.equal(price, trade.price) && 91 | liquidityProvider == trade.liquidityProvider; 92 | } 93 | 94 | @Override 95 | public int hashCode() { 96 | return Objects.hashCode(tradeId); 97 | } 98 | 99 | @Override 100 | public String toString() { 101 | return MoreObjects.toStringHelper(this) 102 | .add("instrumentId", instrumentId) 103 | .add("tradeId", tradeId) 104 | .add("price", price) 105 | .add("quantity", quantity) 106 | .add("liquidityProvider", liquidityProvider) 107 | .toString(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/market/TradeListener.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.market; 2 | 3 | @FunctionalInterface 4 | public interface TradeListener { 5 | 6 | void onTrade(Trade trade); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/market/WebsocketMarketStream.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.market; 2 | 3 | import net.quedex.api.common.Config; 4 | import net.quedex.api.common.WebsocketStream; 5 | import net.quedex.api.pgp.BcPublicKey; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | /** 10 | * 11 | */ 12 | public class WebsocketMarketStream extends WebsocketStream implements MarketStream { 13 | 14 | private static final Logger LOGGER = LoggerFactory.getLogger(WebsocketMarketStream.class); 15 | 16 | public WebsocketMarketStream(String marketStreamUrl, BcPublicKey qdxPublicKey) { 17 | super(LOGGER, marketStreamUrl, new MarketMessageReceiver(qdxPublicKey)); 18 | } 19 | 20 | public WebsocketMarketStream(Config config) { 21 | this(config.getMarketStreamUrl(), config.getQdxPublicKey()); 22 | } 23 | 24 | @Override 25 | public void registerInstrumentsListener(final InstrumentsListener instrumentsListener) { 26 | messageReceiver.registerInstrumentsListener(instrumentsListener); 27 | } 28 | 29 | @Override 30 | public Registration registerOrderBookListener(OrderBookListener orderBookListener) { 31 | return messageReceiver.registerOrderBookListener(orderBookListener); 32 | } 33 | 34 | @Override 35 | public Registration registerTradeListener(TradeListener tradeListener) { 36 | return messageReceiver.registerTradeListener(tradeListener); 37 | } 38 | 39 | @Override 40 | public Registration registerQuotesListener(QuotesListener quotesListener) { 41 | return messageReceiver.registerQuotesListener(quotesListener); 42 | } 43 | 44 | @Override 45 | public void registerSpotDataListener(final SpotDataListener spotDataListener) { 46 | messageReceiver.registerSpotDataListener(spotDataListener); 47 | } 48 | 49 | @Override 50 | public void registerAndSubscribeSessionStateListener(SessionStateListener sessionStateListener) { 51 | messageReceiver.registerAndSubscribeSessionStateListener(sessionStateListener); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/pgp/BcDecryptor.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.pgp; 2 | 3 | import com.google.common.io.ByteStreams; 4 | import org.bouncycastle.openpgp.*; 5 | import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; 6 | import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; 7 | import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; 8 | 9 | import java.io.ByteArrayInputStream; 10 | import java.io.ByteArrayOutputStream; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.nio.charset.StandardCharsets; 14 | import java.util.Iterator; 15 | 16 | import static com.google.common.base.Preconditions.checkNotNull; 17 | 18 | public class BcDecryptor { 19 | 20 | private static final long HIDDEN_RECIPIENT_KEY_ID = 0; 21 | 22 | private final BcPublicKey publicKey; 23 | private final BcPrivateKey ourKey; 24 | 25 | public BcDecryptor(BcPublicKey publicKey, BcPrivateKey ourKey) { 26 | this.publicKey = checkNotNull(publicKey); 27 | this.ourKey = checkNotNull(ourKey); 28 | } 29 | 30 | public String decrypt(String message) 31 | throws PGPDecryptionException, PGPKeyNotFoundException, PGPUnknownRecipientException, PGPInvalidSignatureException { 32 | try { 33 | InputStream in = PGPUtil.getDecoderStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))); 34 | 35 | PGPObjectFactory encryptedFactory = new BcPGPObjectFactory(in); 36 | Object object = encryptedFactory.nextObject(); 37 | 38 | PGPEncryptedDataList encryptedDataList; 39 | 40 | if (object instanceof PGPEncryptedDataList) { 41 | encryptedDataList = (PGPEncryptedDataList) object; 42 | } else { 43 | encryptedDataList = (PGPEncryptedDataList) encryptedFactory.nextObject(); 44 | } 45 | 46 | PGPPublicKeyEncryptedData encryptedData = null; 47 | InputStream clear = null; 48 | 49 | for (Iterator it = encryptedDataList.getEncryptedDataObjects(); it.hasNext(); ) { 50 | encryptedData = (PGPPublicKeyEncryptedData) it.next(); 51 | 52 | if (encryptedData.getKeyID() == HIDDEN_RECIPIENT_KEY_ID) { 53 | 54 | for (final PGPPrivateKey keyToCheck : ourKey.getPrivateKeys()) { 55 | try { 56 | clear = encryptedData.getDataStream(new BcPublicKeyDataDecryptorFactory(keyToCheck)); 57 | break; 58 | } catch (Exception e) { /* fall through and retry */ } 59 | } 60 | } else { 61 | try { 62 | PGPPrivateKey privateKey = ourKey.getPrivateKeyWithId(encryptedData.getKeyID()); 63 | clear = encryptedData.getDataStream(new BcPublicKeyDataDecryptorFactory(privateKey)); 64 | } catch (PGPKeyNotFoundException e) { /* fall through and retry */ } 65 | } 66 | 67 | if (clear != null) { 68 | break; 69 | } 70 | } 71 | 72 | if (clear == null) { 73 | throw new PGPUnknownRecipientException("Message is encrypted for unknown recipient"); 74 | } 75 | 76 | PGPObjectFactory plainFactory = new BcPGPObjectFactory(clear); 77 | Object nextObject = plainFactory.nextObject(); 78 | 79 | PGPCompressedData compressedData = (PGPCompressedData) nextObject; 80 | PGPObjectFactory uncompressedFactory = new BcPGPObjectFactory(compressedData.getDataStream()); 81 | 82 | plainFactory = uncompressedFactory; 83 | nextObject = uncompressedFactory.nextObject(); 84 | 85 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 86 | 87 | PGPOnePassSignatureList sigList = (PGPOnePassSignatureList) nextObject; 88 | PGPOnePassSignature signature = sigList.get(0); 89 | 90 | nextObject = plainFactory.nextObject(); 91 | if (!(nextObject instanceof PGPLiteralData)) { 92 | throw new PGPDataValidationException("Expected literal data packet"); 93 | } 94 | PGPLiteralData literalData = (PGPLiteralData) nextObject; 95 | ByteStreams.copy(literalData.getInputStream(), out); 96 | 97 | PGPSignatureList signatureList = (PGPSignatureList) plainFactory.nextObject(); 98 | 99 | signature.init(new BcPGPContentVerifierBuilderProvider(), publicKey.getSigningKey()); 100 | signature.update(out.toByteArray()); 101 | 102 | if (signature.verify(signatureList.get(0))) { 103 | checkIntegrity(encryptedData); 104 | 105 | return new String(out.toByteArray(), StandardCharsets.UTF_8); 106 | } 107 | 108 | throw new PGPInvalidSignatureException("The signature is not valid"); 109 | 110 | } catch (PGPException | RuntimeException | IOException e) { 111 | throw new PGPDecryptionException("Error Decrypting message", e); 112 | } 113 | } 114 | 115 | private static void checkIntegrity(PGPPublicKeyEncryptedData encryptedData) 116 | throws PGPException, IOException, PGPDecryptionException { 117 | if (encryptedData.isIntegrityProtected()) { 118 | if (!encryptedData.verify()) { 119 | throw new PGPDecryptionException("Message failed integrity check"); 120 | } 121 | } else { 122 | throw new PGPDecryptionException("Message not integrity protected"); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/pgp/BcEncryptor.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.pgp; 2 | 3 | import org.bouncycastle.bcpg.ArmoredOutputStream; 4 | import org.bouncycastle.bcpg.HashAlgorithmTags; 5 | import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; 6 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 7 | import org.bouncycastle.openpgp.*; 8 | import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; 9 | import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; 10 | import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; 11 | import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; 12 | 13 | import java.io.ByteArrayOutputStream; 14 | import java.io.IOException; 15 | import java.io.OutputStream; 16 | import java.nio.charset.StandardCharsets; 17 | import java.security.SecureRandom; 18 | import java.security.Security; 19 | import java.util.Date; 20 | import java.util.Hashtable; 21 | 22 | import static com.google.common.base.Preconditions.checkNotNull; 23 | 24 | public class BcEncryptor { 25 | 26 | private static final int BUFFER_SIZE = 2 << 7; 27 | 28 | private final BcPGPDataEncryptorBuilder dataEncryptor; 29 | private final BcPublicKey publicKey; 30 | private final BcPrivateKey ourKey; 31 | 32 | public BcEncryptor(BcPublicKey publicKey, BcPrivateKey ourKey) { 33 | this.publicKey = checkNotNull(publicKey); 34 | this.ourKey = checkNotNull(ourKey); 35 | 36 | Security.addProvider(new BouncyCastleProvider()); 37 | 38 | dataEncryptor = new BcPGPDataEncryptorBuilder(SymmetricKeyAlgorithmTags.AES_256); 39 | dataEncryptor.setWithIntegrityPacket(true); 40 | dataEncryptor.setSecureRandom(new SecureRandom()); 41 | } 42 | 43 | public String encrypt(String message, boolean sign) throws PGPEncryptionException, PGPKeyNotFoundException { 44 | 45 | try { 46 | PGPSecretKey secretKey = ourKey.getSecretKey(); 47 | PGPPrivateKey privateKey = ourKey.getPrivateKey(); 48 | 49 | byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8); 50 | 51 | PGPLiteralDataGenerator literalDataGenerator = new PGPLiteralDataGenerator(); 52 | PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator(dataEncryptor); 53 | encryptedDataGenerator.addMethod(new BcPublicKeyKeyEncryptionMethodGenerator(publicKey.getEncryptionKey())); 54 | PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(PGPCompressedData.ZLIB); 55 | PGPContentSignerBuilder signerBuilder = new BcPGPContentSignerBuilder( 56 | secretKey.getPublicKey().getAlgorithm(), 57 | HashAlgorithmTags.SHA256 58 | ); 59 | PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(signerBuilder); 60 | signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey); 61 | PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator(); 62 | spGen.setSignerUserID(false, (String) secretKey.getPublicKey().getUserIDs().next()); 63 | signatureGenerator.setHashedSubpackets(spGen.generate()); 64 | if (sign) { 65 | signatureGenerator.update(messageBytes); 66 | } 67 | 68 | ByteArrayOutputStream bOut = new ByteArrayOutputStream(); 69 | Hashtable headers = new Hashtable<>(); 70 | headers.put("Version", "QPG"); 71 | ArmoredOutputStream armoredOut = new ArmoredOutputStream(bOut, headers); 72 | 73 | OutputStream encryptedOut = encryptedDataGenerator.open(armoredOut, new byte[BUFFER_SIZE]); 74 | OutputStream compressedOut = compressedDataGenerator.open(encryptedOut); 75 | if (sign) { 76 | signatureGenerator.generateOnePassVersion(false).encode(compressedOut); 77 | } 78 | OutputStream literalOut = literalDataGenerator.open( 79 | compressedOut, 80 | PGPLiteralData.UTF8, 81 | PGPLiteralData.CONSOLE, 82 | messageBytes.length, 83 | new Date() 84 | ); 85 | literalOut.write(messageBytes); 86 | literalDataGenerator.close(); 87 | 88 | if (sign) { 89 | signatureGenerator.generate().encode(compressedOut); 90 | } 91 | 92 | compressedDataGenerator.close(); 93 | encryptedDataGenerator.close(); 94 | armoredOut.close(); 95 | 96 | return new String(bOut.toByteArray(), StandardCharsets.UTF_8); 97 | 98 | } catch (PGPException | RuntimeException | IOException e) { 99 | throw new PGPEncryptionException("Error encrypting message", e); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/pgp/BcPrivateKey.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.pgp; 2 | 3 | import com.google.common.base.Objects; 4 | import com.google.common.collect.ImmutableMap; 5 | import org.bouncycastle.bcpg.ArmoredInputStream; 6 | import org.bouncycastle.openpgp.*; 7 | import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; 8 | import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; 9 | import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; 10 | import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; 11 | 12 | import java.io.ByteArrayInputStream; 13 | import java.io.IOException; 14 | import java.nio.charset.StandardCharsets; 15 | import java.util.ArrayList; 16 | import java.util.Collection; 17 | import java.util.Iterator; 18 | import java.util.List; 19 | 20 | import static com.google.common.base.Preconditions.checkNotNull; 21 | 22 | public final class BcPrivateKey { 23 | 24 | private static final char[] EMPTY_CHAR_ARRAY = new char[0]; 25 | 26 | private final PGPSecretKey secretKey; 27 | private final PGPPrivateKey privateKey; 28 | private final String fingerprint; 29 | private final ImmutableMap privateKeys; 30 | private final BcPublicKey publicKey; 31 | 32 | public static BcPrivateKey fromArmored(String armoredKeyString) throws PGPKeyInitialisationException { 33 | return fromArmored(armoredKeyString, EMPTY_CHAR_ARRAY); 34 | } 35 | 36 | public static BcPrivateKey fromArmored(String armoredKeyString, char[] passphrase) 37 | throws PGPKeyInitialisationException { 38 | return new BcPrivateKey(armoredKeyString, passphrase); 39 | } 40 | 41 | BcPrivateKey(String armoredKeyString, char[] passphrase) throws PGPKeyInitialisationException { 42 | try { 43 | PGPSecretKeyRing secKeyRing = new PGPSecretKeyRing( 44 | new ArmoredInputStream(new ByteArrayInputStream(armoredKeyString.getBytes(StandardCharsets.US_ASCII))), 45 | new BcKeyFingerprintCalculator()); 46 | 47 | PBESecretKeyDecryptor decryptor = new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider()) 48 | .build(passphrase); 49 | 50 | ImmutableMap.Builder builder = ImmutableMap.builder(); 51 | List pubKeys = new ArrayList<>(2); 52 | 53 | for (Iterator iterator = secKeyRing.getSecretKeys(); iterator.hasNext(); ) { 54 | PGPSecretKey secretKey = (PGPSecretKey) iterator.next(); 55 | PGPPrivateKey privateKey = secretKey.extractPrivateKey(decryptor); 56 | builder.put(privateKey.getKeyID(), privateKey); 57 | pubKeys.add(secretKey.getPublicKey()); 58 | } 59 | 60 | this.secretKey = secKeyRing.getSecretKey(); 61 | this.privateKeys = builder.build(); 62 | this.privateKey = this.secretKey.extractPrivateKey(decryptor); 63 | if (pubKeys.size() >= 2) { 64 | this.publicKey = new BcPublicKey(pubKeys.get(0), pubKeys.get(1)); 65 | } else { 66 | this.publicKey = new BcPublicKey(pubKeys.get(0), pubKeys.get(0)); 67 | } 68 | 69 | } catch (PGPException | RuntimeException | IOException e) { 70 | throw new PGPKeyInitialisationException("Error instantiating a private key", e); 71 | } 72 | checkNotNull(this.secretKey); 73 | checkNotNull(this.privateKey); 74 | 75 | this.fingerprint = BcPublicKey.hexFingerprint(secretKey.getPublicKey()); 76 | } 77 | 78 | PGPSecretKey getSecretKey() { 79 | return secretKey; 80 | } 81 | 82 | PGPPrivateKey getPrivateKey() { 83 | return privateKey; 84 | } 85 | 86 | public String getFingerprint() { 87 | return fingerprint; 88 | } 89 | 90 | PGPPrivateKey getPrivateKeyWithId(long keyId) throws PGPKeyNotFoundException { 91 | if (!privateKeys.containsKey(keyId)) { 92 | throw new PGPKeyNotFoundException( 93 | String.format("Key with id: %s not found", Long.toHexString(keyId).toUpperCase()) 94 | ); 95 | } 96 | return privateKeys.get(keyId); 97 | } 98 | 99 | public Collection getPrivateKeys() { 100 | return privateKeys.values(); 101 | } 102 | 103 | public BcPublicKey getPublicKey() { 104 | return publicKey; 105 | } 106 | 107 | @Override 108 | public boolean equals(Object o) { 109 | if (this == o) { 110 | return true; 111 | } 112 | 113 | if (o == null || o.getClass() != getClass()) { 114 | return false; 115 | } 116 | 117 | BcPrivateKey that = (BcPrivateKey) o; 118 | 119 | return Objects.equal(fingerprint, that.fingerprint); 120 | } 121 | 122 | @Override 123 | public int hashCode() { 124 | return fingerprint.hashCode(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/pgp/BcPublicKey.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.pgp; 2 | 3 | import com.google.common.base.MoreObjects; 4 | import com.google.common.collect.Iterators; 5 | import com.google.common.collect.Lists; 6 | import org.bouncycastle.bcpg.ArmoredInputStream; 7 | import org.bouncycastle.bcpg.ArmoredOutputStream; 8 | import org.bouncycastle.openpgp.PGPPublicKey; 9 | import org.bouncycastle.openpgp.PGPPublicKeyRing; 10 | import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; 11 | 12 | import java.io.ByteArrayInputStream; 13 | import java.io.ByteArrayOutputStream; 14 | import java.io.IOException; 15 | import java.nio.charset.StandardCharsets; 16 | import java.util.List; 17 | 18 | public final class BcPublicKey { 19 | 20 | private final PGPPublicKey signingKey; 21 | private final PGPPublicKey encryptionKey; 22 | 23 | private final String fingerprint; 24 | private final String mainKeyIdentity; 25 | 26 | public static BcPublicKey fromArmored(String armoredKeyString) throws PGPKeyInitialisationException { 27 | 28 | try { 29 | PGPPublicKeyRing pubKeyRing = new PGPPublicKeyRing( 30 | new ArmoredInputStream(new ByteArrayInputStream(armoredKeyString.getBytes(StandardCharsets.UTF_8))), 31 | new BcKeyFingerprintCalculator() 32 | ); 33 | 34 | if (Iterators.size(pubKeyRing.getPublicKeys()) < 1) { 35 | throw new PGPKeyInitialisationException("No keys in keyring"); 36 | } 37 | 38 | PGPPublicKey signingKey = pubKeyRing.getPublicKey(); 39 | PGPPublicKey encryptionKey; 40 | 41 | @SuppressWarnings("unchecked") 42 | List keys = Lists.newArrayList(pubKeyRing.getPublicKeys()); 43 | 44 | if (keys.size() == 1) { 45 | encryptionKey = signingKey; 46 | } else { 47 | encryptionKey = keys.get(1); 48 | } 49 | 50 | if (!encryptionKey.isEncryptionKey()) { 51 | throw new PGPKeyInitialisationException("Error instatiating public key: sign-only key."); 52 | } 53 | 54 | return new BcPublicKey(signingKey, encryptionKey); 55 | 56 | } catch (RuntimeException | IOException e) { 57 | throw new PGPKeyInitialisationException("Error instantiating a public key", e); 58 | } 59 | } 60 | 61 | BcPublicKey(PGPPublicKey signingKey, PGPPublicKey encryptionKey) { 62 | this.signingKey = signingKey; 63 | this.encryptionKey = encryptionKey; 64 | fingerprint = hexFingerprint(signingKey); 65 | mainKeyIdentity = (String) signingKey.getUserIDs().next(); 66 | } 67 | 68 | PGPPublicKey getSigningKey() { 69 | return signingKey; 70 | } 71 | 72 | PGPPublicKey getEncryptionKey() { 73 | return encryptionKey; 74 | } 75 | 76 | public String getFingerprint() { 77 | return fingerprint; 78 | } 79 | 80 | public String getMainKeyIdentity() { 81 | return mainKeyIdentity; 82 | } 83 | 84 | public String armored() { 85 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 86 | ArmoredOutputStream armored = new ArmoredOutputStream(out); 87 | try { 88 | signingKey.encode(armored); 89 | encryptionKey.encode(armored); 90 | armored.close(); 91 | } catch (IOException e) { 92 | throw new IllegalStateException("Error writing armored public key", e); 93 | } 94 | return new String(out.toByteArray(), StandardCharsets.US_ASCII); 95 | } 96 | 97 | static String hexFingerprint(PGPPublicKey publicKey) { 98 | byte[] bytes = publicKey.getFingerprint(); 99 | StringBuilder sb = new StringBuilder(); 100 | for (byte b : bytes) { 101 | sb.append(String.format("%02X", b)); 102 | } 103 | return sb.toString(); 104 | } 105 | 106 | @Override 107 | public boolean equals(Object o) { 108 | if (this == o) { 109 | return true; 110 | } 111 | 112 | if (o == null || o.getClass() != getClass()) { 113 | return false; 114 | } 115 | 116 | BcPublicKey that = (BcPublicKey) o; 117 | 118 | return this.fingerprint.equals(that.fingerprint); 119 | } 120 | 121 | @Override 122 | public int hashCode() { 123 | return fingerprint.hashCode(); 124 | } 125 | 126 | @Override 127 | public String toString() { 128 | return MoreObjects.toStringHelper(this) 129 | .add("fingerprint", fingerprint) 130 | .toString(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/pgp/BcSignatureVerifier.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.pgp; 2 | 3 | import org.bouncycastle.bcpg.ArmoredInputStream; 4 | import org.bouncycastle.openpgp.PGPException; 5 | import org.bouncycastle.openpgp.PGPSignature; 6 | import org.bouncycastle.openpgp.PGPSignatureList; 7 | import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory; 8 | import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; 9 | 10 | import java.io.ByteArrayInputStream; 11 | import java.io.ByteArrayOutputStream; 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.nio.charset.StandardCharsets; 15 | 16 | import static com.google.common.base.Preconditions.checkNotNull; 17 | import static com.google.common.base.Preconditions.checkState; 18 | 19 | public final class BcSignatureVerifier { 20 | 21 | private final BcPublicKey publicKey; 22 | 23 | public BcSignatureVerifier(BcPublicKey publicKey) { 24 | this.publicKey = checkNotNull(publicKey, "null publicKey"); 25 | } 26 | 27 | public String verifySignature(String message) 28 | throws PGPInvalidSignatureException, PGPSignatureVerificationException { 29 | 30 | try { 31 | ArmoredInputStream aIn = new ArmoredInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))); 32 | 33 | ByteArrayOutputStream bOut = new ByteArrayOutputStream(); 34 | int ch; 35 | 36 | while ((ch = aIn.read()) >= 0 && aIn.isClearText()) { 37 | bOut.write((byte) ch); 38 | } 39 | 40 | JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(aIn); 41 | PGPSignatureList p3 = (PGPSignatureList) pgpFact.nextObject(); 42 | checkState(p3 != null && p3.size() >= 1, "No signatures"); 43 | PGPSignature sig = p3.get(0); 44 | 45 | sig.init(new BcPGPContentVerifierBuilderProvider(), publicKey.getSigningKey()); 46 | 47 | ByteArrayOutputStream lineOut = new ByteArrayOutputStream(); 48 | byte[] content = bOut.toByteArray(); 49 | InputStream sigIn = new ByteArrayInputStream(content); 50 | int lookAhead = readInputLine(lineOut, sigIn); 51 | 52 | processLine(sig, lineOut.toByteArray()); 53 | 54 | if (lookAhead != -1) { 55 | do { 56 | lookAhead = readInputLine(lineOut, lookAhead, sigIn); 57 | 58 | sig.update((byte) '\r'); 59 | sig.update((byte) '\n'); 60 | 61 | processLine(sig, lineOut.toByteArray()); 62 | } while (lookAhead != -1); 63 | } 64 | 65 | if (sig.verify()) { 66 | return new String(content, StandardCharsets.UTF_8); 67 | } 68 | 69 | throw new PGPInvalidSignatureException( 70 | "Invalid signature, received keyId=" + Long.toHexString(sig.getKeyID()).toUpperCase() 71 | ); 72 | 73 | } catch (IOException | PGPException e) { 74 | throw new PGPSignatureVerificationException("Error verifying message", e); 75 | } 76 | } 77 | 78 | private static void processLine(PGPSignature sig, byte[] line) throws IOException { 79 | int length = getLengthWithoutWhiteSpace(line); 80 | if (length > 0) { 81 | sig.update(line, 0, length); 82 | } 83 | } 84 | 85 | private static int getLengthWithoutWhiteSpace(byte[] line) { 86 | int end = line.length - 1; 87 | while (end >= 0 && isWhiteSpace(line[end])) { 88 | end--; 89 | } 90 | return end + 1; 91 | } 92 | 93 | private static boolean isWhiteSpace(byte b) { 94 | return b == '\r' || b == '\n' || b == '\t' || b == ' '; 95 | } 96 | 97 | private static int readInputLine(ByteArrayOutputStream bOut, InputStream fIn) throws IOException { 98 | bOut.reset(); 99 | int lookAhead = -1; 100 | int ch; 101 | while ((ch = fIn.read()) >= 0) { 102 | bOut.write(ch); 103 | if (ch == '\r' || ch == '\n') { 104 | lookAhead = readPassedEOL(bOut, ch, fIn); 105 | break; 106 | } 107 | } 108 | return lookAhead; 109 | } 110 | 111 | private static int readInputLine(ByteArrayOutputStream bOut, int lookAhead, InputStream fIn) throws IOException { 112 | bOut.reset(); 113 | int ch = lookAhead; 114 | do { 115 | bOut.write(ch); 116 | if (ch == '\r' || ch == '\n') { 117 | lookAhead = readPassedEOL(bOut, ch, fIn); 118 | break; 119 | } 120 | } while ((ch = fIn.read()) >= 0); 121 | return lookAhead; 122 | } 123 | 124 | private static int readPassedEOL(ByteArrayOutputStream bOut, int lastCh, InputStream fIn) throws IOException { 125 | int lookAhead = fIn.read(); 126 | if (lastCh == '\r' && lookAhead == '\n') { 127 | bOut.write(lookAhead); 128 | lookAhead = fIn.read(); 129 | } 130 | return lookAhead; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/pgp/PGPDecryptionException.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.pgp; 2 | 3 | public class PGPDecryptionException extends PGPExceptionBase { 4 | 5 | public PGPDecryptionException() { 6 | } 7 | 8 | public PGPDecryptionException(String message) { 9 | super(message); 10 | } 11 | 12 | public PGPDecryptionException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public PGPDecryptionException(Throwable cause) { 17 | super(cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/pgp/PGPEncryptionException.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.pgp; 2 | 3 | public class PGPEncryptionException extends PGPExceptionBase { 4 | 5 | public PGPEncryptionException() { 6 | } 7 | 8 | public PGPEncryptionException(String message) { 9 | super(message); 10 | } 11 | 12 | public PGPEncryptionException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public PGPEncryptionException(Throwable cause) { 17 | super(cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/pgp/PGPExceptionBase.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.pgp; 2 | 3 | public class PGPExceptionBase extends Exception { 4 | 5 | PGPExceptionBase() { 6 | } 7 | 8 | PGPExceptionBase(String message) { 9 | super(message); 10 | } 11 | 12 | PGPExceptionBase(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | PGPExceptionBase(Throwable cause) { 17 | super(cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/pgp/PGPInvalidSignatureException.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.pgp; 2 | 3 | public final class PGPInvalidSignatureException extends PGPExceptionBase { 4 | 5 | public PGPInvalidSignatureException() { 6 | } 7 | 8 | public PGPInvalidSignatureException(String message) { 9 | super(message); 10 | } 11 | 12 | public PGPInvalidSignatureException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public PGPInvalidSignatureException(Throwable cause) { 17 | super(cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/pgp/PGPKeyInitialisationException.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.pgp; 2 | 3 | public final class PGPKeyInitialisationException extends PGPExceptionBase { 4 | 5 | public PGPKeyInitialisationException() { 6 | } 7 | 8 | public PGPKeyInitialisationException(String message) { 9 | super(message); 10 | } 11 | 12 | public PGPKeyInitialisationException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public PGPKeyInitialisationException(Throwable cause) { 17 | super(cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/pgp/PGPKeyNotFoundException.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.pgp; 2 | 3 | public final class PGPKeyNotFoundException extends PGPExceptionBase { 4 | 5 | public PGPKeyNotFoundException() { 6 | } 7 | 8 | public PGPKeyNotFoundException(String message) { 9 | super(message); 10 | } 11 | 12 | public PGPKeyNotFoundException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public PGPKeyNotFoundException(Throwable cause) { 17 | super(cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/pgp/PGPSignatureVerificationException.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.pgp; 2 | 3 | public class PGPSignatureVerificationException extends PGPExceptionBase { 4 | 5 | public PGPSignatureVerificationException() { 6 | } 7 | 8 | public PGPSignatureVerificationException(String message) { 9 | super(message); 10 | } 11 | 12 | public PGPSignatureVerificationException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/pgp/PGPUnknownRecipientException.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.pgp; 2 | 3 | public final class PGPUnknownRecipientException extends PGPExceptionBase { 4 | 5 | public PGPUnknownRecipientException() { 6 | } 7 | 8 | public PGPUnknownRecipientException(String message) { 9 | super(message); 10 | } 11 | 12 | public PGPUnknownRecipientException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public PGPUnknownRecipientException(Throwable cause) { 17 | super(cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/AccountState.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import java.math.BigDecimal; 9 | 10 | import static com.google.common.base.Preconditions.checkNotNull; 11 | 12 | public class AccountState { 13 | 14 | public enum Status { 15 | ACTIVE, MARGIN_CALL, LIQUIDATION; 16 | 17 | @JsonCreator 18 | private static Status deserialize(String value) { 19 | return valueOf(value.toUpperCase()); 20 | } 21 | } 22 | 23 | private final BigDecimal balance; 24 | private final BigDecimal freeBalance; 25 | private final BigDecimal totalInitialMargin; 26 | private final BigDecimal totalMaintenanceMargin; 27 | private final BigDecimal totalLockedForOrders; 28 | private final BigDecimal totalUnsettledPnL; 29 | private final BigDecimal totalPendingWithdrawal; 30 | 31 | private final Status status; 32 | 33 | @JsonCreator 34 | public AccountState( 35 | @JsonProperty("balance") BigDecimal balance, 36 | @JsonProperty("free_balance") BigDecimal freeBalance, 37 | @JsonProperty("total_initial_margin") BigDecimal totalInitialMargin, 38 | @JsonProperty("total_maintenance_margin") BigDecimal totalMaintenanceMargin, 39 | @JsonProperty("total_unsettled_pnl") BigDecimal totalUnsettledPnL, 40 | @JsonProperty("total_locked_for_orders") BigDecimal totalLockedForOrders, 41 | @JsonProperty("total_pending_withdrawal") BigDecimal totalPendingWithdrawal, 42 | @JsonProperty("account_status") Status status 43 | ) { 44 | this.balance = checkNotNull(balance, "null balance"); 45 | this.freeBalance = checkNotNull(freeBalance, "null freeBalance"); 46 | this.totalInitialMargin = checkNotNull(totalInitialMargin, "null totalInitialMargin"); 47 | this.totalMaintenanceMargin = checkNotNull(totalMaintenanceMargin, "null totalMaintenanceMargin"); 48 | this.totalUnsettledPnL = checkNotNull(totalUnsettledPnL, "null totalUnsettledPnL"); 49 | this.totalLockedForOrders = checkNotNull(totalLockedForOrders, "null totalLockedForOrders"); 50 | this.totalPendingWithdrawal = checkNotNull(totalPendingWithdrawal, "null totalPendingWithdrawal"); 51 | this.status = checkNotNull(status, "null status"); 52 | } 53 | 54 | public BigDecimal getBalance() { 55 | return balance; 56 | } 57 | 58 | public BigDecimal getFreeBalance() { 59 | return freeBalance; 60 | } 61 | 62 | public BigDecimal getTotalInitialMargin() { 63 | return totalInitialMargin; 64 | } 65 | 66 | public BigDecimal getTotalMaintenanceMargin() { 67 | return totalMaintenanceMargin; 68 | } 69 | 70 | public BigDecimal getTotalLockedForOrders() { 71 | return totalLockedForOrders; 72 | } 73 | 74 | public BigDecimal getTotalUnsettledPnL() { 75 | return totalUnsettledPnL; 76 | } 77 | 78 | public BigDecimal getTotalPendingWithdrawal() { 79 | return totalPendingWithdrawal; 80 | } 81 | 82 | public Status getStatus() { 83 | return status; 84 | } 85 | 86 | @Override 87 | public boolean equals(Object o) { 88 | if (this == o) return true; 89 | if (o == null || getClass() != o.getClass()) return false; 90 | AccountState that = (AccountState) o; 91 | return Objects.equal(balance, that.balance) && 92 | Objects.equal(freeBalance, that.freeBalance) && 93 | Objects.equal(totalInitialMargin, that.totalInitialMargin) && 94 | Objects.equal(totalMaintenanceMargin, that.totalMaintenanceMargin) && 95 | Objects.equal(totalLockedForOrders, that.totalLockedForOrders) && 96 | Objects.equal(totalUnsettledPnL, that.totalUnsettledPnL) && 97 | Objects.equal(totalPendingWithdrawal, that.totalPendingWithdrawal) && 98 | status == that.status; 99 | } 100 | 101 | @Override 102 | public int hashCode() { 103 | return Objects.hashCode( 104 | balance, 105 | freeBalance, 106 | totalInitialMargin, 107 | totalMaintenanceMargin, 108 | totalLockedForOrders, 109 | totalUnsettledPnL, 110 | totalPendingWithdrawal, 111 | status 112 | ); 113 | } 114 | 115 | @Override 116 | public String toString() { 117 | return MoreObjects.toStringHelper(this) 118 | .add("balance", balance) 119 | .add("freeBalance", freeBalance) 120 | .add("totalInitialMargin", totalInitialMargin) 121 | .add("totalMaintenanceMargin", totalMaintenanceMargin) 122 | .add("totalLockedForOrders", totalLockedForOrders) 123 | .add("totalUnsettledPnL", totalUnsettledPnL) 124 | .add("totalPendingWithdrawal", totalPendingWithdrawal) 125 | .add("status", status) 126 | .toString(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/AccountStateListener.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | @FunctionalInterface 4 | public interface AccountStateListener { 5 | 6 | void onAccountState(AccountState accountState); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/CancelAllOrdersFailed.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import static com.google.common.base.Preconditions.checkNotNull; 9 | 10 | public class CancelAllOrdersFailed { 11 | public enum Cause { 12 | SESSION_NOT_ACTIVE; 13 | 14 | @JsonCreator 15 | public static Cause deserialize(final String str) { 16 | return valueOf(str.toUpperCase()); 17 | } 18 | } 19 | 20 | private final Cause cause; 21 | 22 | @JsonCreator 23 | public CancelAllOrdersFailed(final @JsonProperty("cause") Cause cause) { 24 | this.cause = checkNotNull(cause, "null cause"); 25 | } 26 | 27 | public Cause getCause() { 28 | return cause; 29 | } 30 | 31 | @Override 32 | public boolean equals(Object o) { 33 | if (this == o) return true; 34 | if (o == null || getClass() != o.getClass()) return false; 35 | CancelAllOrdersFailed that = (CancelAllOrdersFailed) o; 36 | return this.cause == that.cause; 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | return Objects.hashCode(cause); 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return MoreObjects.toStringHelper(this) 47 | .add("cause", cause) 48 | .toString(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/CancelAllOrdersSpec.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class CancelAllOrdersSpec implements OrderSpec { 6 | public static final CancelAllOrdersSpec INSTANCE = new CancelAllOrdersSpec(); 7 | 8 | @JsonProperty("type") 9 | private String getType() { 10 | return "cancel_all_orders"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/InternalTransfer.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.google.common.base.MoreObjects; 5 | import com.google.common.base.Objects; 6 | 7 | import java.math.BigDecimal; 8 | 9 | import static com.google.common.base.Preconditions.checkArgument; 10 | 11 | public class InternalTransfer { 12 | 13 | private final long destinationAccountId; 14 | private final BigDecimal amount; 15 | 16 | public InternalTransfer(final long destinationAccountId, final BigDecimal amount) { 17 | checkArgument(destinationAccountId > 0, "destinationAccountId=%s <= 0", destinationAccountId); 18 | checkArgument(amount.compareTo(BigDecimal.ZERO) > 0, "amount=%s <= 0", amount); 19 | this.destinationAccountId = destinationAccountId; 20 | this.amount = amount; 21 | } 22 | 23 | @JsonProperty("destination_account_id") 24 | public long getDestinationAccountId() { 25 | return destinationAccountId; 26 | } 27 | 28 | @JsonProperty("amount") 29 | public BigDecimal getAmount() { 30 | return amount; 31 | } 32 | 33 | @JsonProperty("type") 34 | private String getType() { 35 | return "internal_transfer"; 36 | } 37 | 38 | @Override 39 | public boolean equals(final Object o) { 40 | if (this == o) { 41 | return true; 42 | } 43 | if (o == null || getClass() != o.getClass()) { 44 | return false; 45 | } 46 | final InternalTransfer that = (InternalTransfer) o; 47 | return destinationAccountId == that.destinationAccountId && 48 | Objects.equal(amount, that.amount); 49 | } 50 | 51 | @Override 52 | public int hashCode() { 53 | return Objects.hashCode(destinationAccountId, amount); 54 | } 55 | 56 | @Override 57 | public String toString() { 58 | return MoreObjects.toStringHelper(this) 59 | .add("destinationAccountId", destinationAccountId) 60 | .add("amount", amount) 61 | .toString(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/InternalTransferExecuted.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import java.math.BigDecimal; 9 | 10 | import static com.google.common.base.Preconditions.checkArgument; 11 | 12 | public class InternalTransferExecuted { 13 | 14 | private final long destinationUserId; 15 | private final BigDecimal amount; 16 | 17 | @JsonCreator 18 | public InternalTransferExecuted( 19 | @JsonProperty("destination_account_id") long destinationUserId, 20 | @JsonProperty("amount") BigDecimal amount 21 | ) { 22 | checkArgument(destinationUserId > 0, "destinationUserId=%s <= 0", destinationUserId); 23 | checkArgument(amount.compareTo(BigDecimal.ZERO) > 0, "amount=%s <= 0", amount); 24 | this.destinationUserId = destinationUserId; 25 | this.amount = amount; 26 | } 27 | 28 | public long getDestinationUserId() { 29 | return destinationUserId; 30 | } 31 | 32 | public BigDecimal getAmount() { 33 | return amount; 34 | } 35 | 36 | @Override 37 | public boolean equals(final Object o) { 38 | if (this == o) { 39 | return true; 40 | } 41 | if (o == null || getClass() != o.getClass()) { 42 | return false; 43 | } 44 | final InternalTransferExecuted that = (InternalTransferExecuted) o; 45 | return destinationUserId == that.destinationUserId && 46 | Objects.equal(amount, that.amount); 47 | } 48 | 49 | @Override 50 | public int hashCode() { 51 | return Objects.hashCode(destinationUserId, amount); 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | return MoreObjects.toStringHelper(this) 57 | .add("destinationUserId", destinationUserId) 58 | .add("amount", amount) 59 | .toString(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/InternalTransferListener.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | public interface InternalTransferListener { 4 | 5 | void onInternalTransferExecuted(InternalTransferExecuted internalTransferExecuted); 6 | 7 | void onInternalTransferRejected(InternalTransferRejected internalTransferRejected); 8 | 9 | void onInternalTransferReceived(InternalTransferReceived internalTransferReceived); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/InternalTransferReceived.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import java.math.BigDecimal; 9 | 10 | import static com.google.common.base.Preconditions.checkArgument; 11 | 12 | public class InternalTransferReceived { 13 | 14 | private final long sourceUserId; 15 | private final BigDecimal amount; 16 | 17 | @JsonCreator 18 | public InternalTransferReceived( 19 | @JsonProperty("source_account_id") long sourceUserId, 20 | @JsonProperty("amount") BigDecimal amount 21 | ) { 22 | checkArgument(sourceUserId > 0, "sourceUserId=%s <= 0", sourceUserId); 23 | checkArgument(amount.compareTo(BigDecimal.ZERO) > 0, "amount=%s <= 0", amount); 24 | this.sourceUserId = sourceUserId; 25 | this.amount = amount; 26 | } 27 | 28 | public long getSourceUserId() { 29 | return sourceUserId; 30 | } 31 | 32 | public BigDecimal getAmount() { 33 | return amount; 34 | } 35 | 36 | @Override 37 | public boolean equals(final Object o) { 38 | if (this == o) { 39 | return true; 40 | } 41 | if (o == null || getClass() != o.getClass()) { 42 | return false; 43 | } 44 | final InternalTransferReceived that = (InternalTransferReceived) o; 45 | return sourceUserId == that.sourceUserId && 46 | Objects.equal(amount, that.amount); 47 | } 48 | 49 | @Override 50 | public int hashCode() { 51 | return Objects.hashCode(sourceUserId, amount); 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | return MoreObjects.toStringHelper(this) 57 | .add("sourceUserId", sourceUserId) 58 | .add("amount", amount) 59 | .toString(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/InternalTransferRejected.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import java.math.BigDecimal; 9 | 10 | import static com.google.common.base.Preconditions.checkArgument; 11 | import static com.google.common.base.Preconditions.checkNotNull; 12 | 13 | public class InternalTransferRejected { 14 | 15 | public enum Cause { 16 | INSUFFICIENT_FUNDS, 17 | FORBIDDEN; 18 | 19 | @JsonCreator 20 | private static Cause deserialize(String value) { 21 | return valueOf(value.toUpperCase()); 22 | } 23 | } 24 | 25 | private final long destinationUserId; 26 | private final BigDecimal amount; 27 | private final Cause cause; 28 | 29 | @JsonCreator 30 | public InternalTransferRejected( 31 | @JsonProperty("destination_account_id") long destinationUserId, 32 | @JsonProperty("amount") BigDecimal amount, 33 | @JsonProperty("cause") Cause cause 34 | ) { 35 | checkArgument(destinationUserId > 0, "destinationUserId=%s <= 0", destinationUserId); 36 | checkArgument(amount.compareTo(BigDecimal.ZERO) > 0, "amount=%s <= 0", amount); 37 | this.destinationUserId = destinationUserId; 38 | this.amount = amount; 39 | this.cause = checkNotNull(cause, "cause"); 40 | } 41 | 42 | public long getDestinationUserId() { 43 | return destinationUserId; 44 | } 45 | 46 | public BigDecimal getAmount() { 47 | return amount; 48 | } 49 | 50 | public Cause getCause() { 51 | return cause; 52 | } 53 | 54 | @Override 55 | public boolean equals(final Object o) { 56 | if (this == o) { 57 | return true; 58 | } 59 | if (o == null || getClass() != o.getClass()) { 60 | return false; 61 | } 62 | final InternalTransferRejected that = (InternalTransferRejected) o; 63 | return destinationUserId == that.destinationUserId && 64 | Objects.equal(amount, that.amount) && 65 | cause == that.cause; 66 | } 67 | 68 | @Override 69 | public int hashCode() { 70 | return Objects.hashCode(destinationUserId, amount, cause); 71 | } 72 | 73 | @Override 74 | public String toString() { 75 | return MoreObjects.toStringHelper(this) 76 | .add("destinationUserId", destinationUserId) 77 | .add("amount", amount) 78 | .add("cause", cause) 79 | .toString(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/LimitOrderSpec.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.google.common.base.MoreObjects; 5 | import com.google.common.base.Objects; 6 | 7 | import java.math.BigDecimal; 8 | 9 | import static com.google.common.base.Preconditions.checkArgument; 10 | import static com.google.common.base.Preconditions.checkNotNull; 11 | 12 | public class LimitOrderSpec implements OrderSpec { 13 | 14 | private final long clientOrderId; 15 | private final int instrumentId; 16 | private final OrderSide side; 17 | private final int quantity; 18 | private final BigDecimal limitPrice; 19 | private final boolean postOnly; 20 | 21 | /** 22 | * @param clientOrderId id of an order given by the client, has to be different than ids of all other pending orders 23 | * @param instrumentId id of an instrument this order is for 24 | * @param side side of the order 25 | * @param quantity quantity of the order, has to be positive 26 | * @param limitPrice limit price of the order, has to be positive 27 | * @param postOnly if true placing of order will fail if it would match immediately 28 | * @throws IllegalArgumentException if {@code quantity} or {@code limitPrice} is not positive 29 | * @throws NullPointerException if any of nullable arguments is null 30 | */ 31 | public LimitOrderSpec(final long clientOrderId, 32 | final int instrumentId, 33 | final OrderSide side, 34 | final int quantity, 35 | final BigDecimal limitPrice, 36 | final boolean postOnly) { 37 | checkArgument(clientOrderId > 0, "clientOrderId=%s <= 0", clientOrderId); 38 | checkArgument(instrumentId > 0, "instrumentId=%s <= 0", instrumentId); 39 | checkArgument(quantity > 0, "quantity=%s <= 0", quantity); 40 | checkArgument(limitPrice.compareTo(BigDecimal.ZERO) > 0, "limitPrice=%s <= 0", limitPrice); 41 | this.clientOrderId = clientOrderId; 42 | this.instrumentId = instrumentId; 43 | this.side = checkNotNull(side, "null side"); 44 | this.quantity = quantity; 45 | this.limitPrice = limitPrice; 46 | this.postOnly = postOnly; 47 | } 48 | 49 | public LimitOrderSpec(final long clientOrderId, 50 | final int instrumentId, 51 | final OrderSide side, 52 | final int quantity, 53 | final BigDecimal limitPrice) { 54 | this(clientOrderId, instrumentId, side, quantity, limitPrice, false); 55 | } 56 | 57 | @JsonProperty("client_order_id") 58 | public long getClientOrderId() { 59 | return clientOrderId; 60 | } 61 | 62 | @JsonProperty("instrument_id") 63 | public int getInstrumentId() { 64 | return instrumentId; 65 | } 66 | 67 | @JsonProperty("side") 68 | public OrderSide getSide() { 69 | return side; 70 | } 71 | 72 | @JsonProperty("quantity") 73 | public int getQuantity() { 74 | return quantity; 75 | } 76 | 77 | @JsonProperty("limit_price") 78 | public BigDecimal getLimitPrice() { 79 | return limitPrice; 80 | } 81 | 82 | @JsonProperty("order_type") 83 | public OrderType getOrderType() { 84 | return OrderType.LIMIT; 85 | } 86 | 87 | @JsonProperty("type") 88 | private String getType() { 89 | return "place_order"; 90 | } 91 | 92 | @JsonProperty("post_only") 93 | private boolean getPostOnly() { 94 | return postOnly; 95 | } 96 | 97 | @Override 98 | public boolean equals(Object o) { 99 | if (this == o) return true; 100 | if (o == null || getClass() != o.getClass()) return false; 101 | LimitOrderSpec that = (LimitOrderSpec) o; 102 | return clientOrderId == that.clientOrderId && 103 | instrumentId == that.instrumentId && 104 | quantity == that.quantity && 105 | side == that.side && 106 | Objects.equal(limitPrice, that.limitPrice) && 107 | postOnly == that.postOnly; 108 | } 109 | 110 | @Override 111 | public int hashCode() { 112 | return Objects.hashCode(clientOrderId, instrumentId, side, quantity, limitPrice, postOnly); 113 | } 114 | 115 | @Override 116 | public String toString() { 117 | return MoreObjects.toStringHelper(this) 118 | .add("clientOrderId", clientOrderId) 119 | .add("instrumentId", instrumentId) 120 | .add("side", side) 121 | .add("quantity", quantity) 122 | .add("limitPrice", limitPrice) 123 | .add("postOnly", postOnly) 124 | .toString(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/LiquidationOrderCancelled.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | /** 9 | * Notification of a cancelled liquidation order. 10 | * 11 | * @see LiquidationOrderPlaced 12 | */ 13 | public class LiquidationOrderCancelled { 14 | 15 | private final long systemOrderId; 16 | 17 | @JsonCreator 18 | public LiquidationOrderCancelled(@JsonProperty("system_order_id") long systemOrderId) { 19 | this.systemOrderId = systemOrderId; 20 | } 21 | 22 | public long getSystemOrderId() { 23 | return systemOrderId; 24 | } 25 | 26 | @Override 27 | public boolean equals(Object o) { 28 | if (this == o) return true; 29 | if (o == null || getClass() != o.getClass()) return false; 30 | LiquidationOrderCancelled that = (LiquidationOrderCancelled) o; 31 | return systemOrderId == that.systemOrderId; 32 | } 33 | 34 | @Override 35 | public int hashCode() { 36 | return Objects.hashCode(systemOrderId); 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | return MoreObjects.toStringHelper(this) 42 | .add("systemOrderId", systemOrderId) 43 | .toString(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/LiquidationOrderFilled.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import java.math.BigDecimal; 9 | 10 | import static com.google.common.base.Preconditions.checkArgument; 11 | import static com.google.common.base.Preconditions.checkNotNull; 12 | 13 | /** 14 | * Notification of a fill of a liquidation order. 15 | * 16 | * @see LiquidationOrderPlaced 17 | */ 18 | public class LiquidationOrderFilled { 19 | 20 | private final long systemOrderId; 21 | private final int instrumentId; 22 | private final OrderType orderType; 23 | private final OrderSide side; 24 | private final int orderInitialQuantity; 25 | private final int leavesOrderQuantity; 26 | private final BigDecimal tradePrice; 27 | private final int filledQuantity; 28 | 29 | @JsonCreator 30 | public LiquidationOrderFilled( 31 | @JsonProperty("system_order_id") long systemOrderId, 32 | @JsonProperty("instrument_id") int instrumentId, 33 | @JsonProperty("order_side") OrderSide side, 34 | @JsonProperty("order_initial_quantity") int orderInitialQuantity, 35 | @JsonProperty("leaves_order_quantity") int leavesOrderQuantity, 36 | @JsonProperty("trade_price") BigDecimal tradePrice, 37 | @JsonProperty("trade_quantity") int filledQuantity 38 | ) { 39 | checkArgument(filledQuantity > 0, "filledQuantity=%s <= 0", filledQuantity); 40 | checkArgument(tradePrice.compareTo(BigDecimal.ZERO) > 0, "tradePrice=%s <= 0", tradePrice); 41 | checkArgument(instrumentId > 0, "instrumentId=%s <= 0", instrumentId); 42 | checkArgument(orderInitialQuantity > 0, "orderInitialQuantity=%s "); 43 | this.systemOrderId = systemOrderId; 44 | this.instrumentId = instrumentId; 45 | this.orderType = OrderType.MARKET; 46 | this.side = checkNotNull(side, "side"); 47 | this.orderInitialQuantity = orderInitialQuantity; 48 | this.leavesOrderQuantity = leavesOrderQuantity; 49 | this.filledQuantity = filledQuantity; 50 | this.tradePrice = tradePrice; 51 | } 52 | 53 | public long getSystemOrderId() { 54 | return systemOrderId; 55 | } 56 | 57 | public int getInstrumentId() { 58 | return instrumentId; 59 | } 60 | 61 | public OrderType getOrderType() { 62 | return orderType; 63 | } 64 | 65 | public OrderSide getSide() { 66 | return side; 67 | } 68 | 69 | public int getOrderInitialQuantity() { 70 | return orderInitialQuantity; 71 | } 72 | 73 | public int getLeavesOrderQuantity() { 74 | return leavesOrderQuantity; 75 | } 76 | 77 | public BigDecimal getTradePrice() { 78 | return tradePrice; 79 | } 80 | 81 | public int getFilledQuantity() { 82 | return filledQuantity; 83 | } 84 | 85 | @Override 86 | public boolean equals(final Object o) { 87 | if (this == o) { 88 | return true; 89 | } 90 | if (o == null || getClass() != o.getClass()) { 91 | return false; 92 | } 93 | final LiquidationOrderFilled that = (LiquidationOrderFilled) o; 94 | return systemOrderId == that.systemOrderId && 95 | instrumentId == that.instrumentId && 96 | orderInitialQuantity == that.orderInitialQuantity && 97 | leavesOrderQuantity == that.leavesOrderQuantity && 98 | filledQuantity == that.filledQuantity && 99 | orderType == that.orderType && 100 | side == that.side && 101 | Objects.equal(tradePrice, that.tradePrice); 102 | } 103 | 104 | @Override 105 | public int hashCode() { 106 | return Objects.hashCode( 107 | systemOrderId, 108 | instrumentId, 109 | orderType, 110 | side, 111 | orderInitialQuantity, 112 | leavesOrderQuantity, 113 | tradePrice, 114 | filledQuantity 115 | ); 116 | } 117 | 118 | @Override 119 | public String toString() { 120 | return MoreObjects.toStringHelper(this) 121 | .add("systemOrderId", systemOrderId) 122 | .add("instrumentId", instrumentId) 123 | .add("orderType", orderType) 124 | .add("side", side) 125 | .add("orderInitialQuantity", orderInitialQuantity) 126 | .add("leavesOrderQuantity", leavesOrderQuantity) 127 | .add("tradePrice", tradePrice) 128 | .add("filledQuantity", filledQuantity) 129 | .toString(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/LiquidationOrderPlaced.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.google.common.base.MoreObjects; 7 | import com.google.common.base.Objects; 8 | 9 | import static com.google.common.base.Preconditions.checkArgument; 10 | import static com.google.common.base.Preconditions.checkNotNull; 11 | 12 | /** 13 | * Liquidation market order placed automatically by the exchange during liquidation. 14 | */ 15 | public class LiquidationOrderPlaced { 16 | 17 | private final long systemOrderId; 18 | private final int instrumentId; 19 | @JsonIgnore 20 | private final OrderType type; 21 | private final OrderSide side; 22 | private final int quantity; 23 | private final int initialQuantity; 24 | 25 | @JsonCreator 26 | public LiquidationOrderPlaced( 27 | @JsonProperty("system_order_id") long systemOrderId, 28 | @JsonProperty("instrument_id") int instrumentId, 29 | @JsonProperty("side") OrderSide side, 30 | @JsonProperty("quantity") int quantity, 31 | @JsonProperty("initial_quantity") int initialQuantity 32 | ) { 33 | checkArgument(systemOrderId > 0, "systemOrderId=%s <= 0", systemOrderId); 34 | checkArgument(instrumentId > 0, "instrumentId=%s <= 0", instrumentId); 35 | checkArgument(quantity > 0, "quantity=%s <= 0", quantity); 36 | checkArgument(initialQuantity > 0, "initialQuantity=%s <= 0", initialQuantity); 37 | this.systemOrderId = systemOrderId; 38 | this.instrumentId = instrumentId; 39 | this.type = OrderType.MARKET; 40 | this.side = checkNotNull(side, "null side"); 41 | this.quantity = quantity; 42 | this.initialQuantity = initialQuantity; 43 | } 44 | 45 | public long getSystemOrderId() { 46 | return systemOrderId; 47 | } 48 | 49 | public int getInstrumentId() { 50 | return instrumentId; 51 | } 52 | 53 | public OrderType getType() { 54 | return type; 55 | } 56 | 57 | public OrderSide getSide() { 58 | return side; 59 | } 60 | 61 | public int getQuantity() { 62 | return quantity; 63 | } 64 | 65 | public int getInitialQuantity() { 66 | return initialQuantity; 67 | } 68 | 69 | @Override 70 | public boolean equals(Object o) { 71 | if (this == o) return true; 72 | if (o == null || getClass() != o.getClass()) return false; 73 | LiquidationOrderPlaced that = (LiquidationOrderPlaced) o; 74 | return systemOrderId == that.systemOrderId && 75 | instrumentId == that.instrumentId && 76 | quantity == that.quantity && 77 | initialQuantity == that.initialQuantity && 78 | type == that.type && 79 | side == that.side; 80 | } 81 | 82 | @Override 83 | public int hashCode() { 84 | return Objects.hashCode(systemOrderId, instrumentId, type, side, quantity, initialQuantity); 85 | } 86 | 87 | @Override 88 | public String toString() { 89 | return MoreObjects.toStringHelper(this) 90 | .add("systemOrderId", systemOrderId) 91 | .add("instrumentId", instrumentId) 92 | .add("type", type) 93 | .add("side", side) 94 | .add("quantity", quantity) 95 | .add("initialQuantity", initialQuantity) 96 | .toString(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/OpenPosition.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import java.math.BigDecimal; 9 | 10 | import static com.google.common.base.Preconditions.checkArgument; 11 | import static com.google.common.base.Preconditions.checkNotNull; 12 | 13 | public class OpenPosition { 14 | 15 | public enum PositionSide { 16 | LONG, SHORT; 17 | 18 | @JsonCreator 19 | private static PositionSide deserialize(String value) { 20 | return valueOf(value.toUpperCase()); 21 | } 22 | } 23 | 24 | private final int instrumentId; 25 | private final BigDecimal pnl; // null for options 26 | private final BigDecimal maintenanceMargin; 27 | private final BigDecimal initialMargin; 28 | private final PositionSide side; // any of LONG, SHORT for a closed position 29 | private final int quantity; 30 | private final BigDecimal averageOpeningPrice; // any number for a closed position 31 | 32 | @JsonCreator 33 | public OpenPosition( 34 | @JsonProperty("instrument_id") int instrumentId, 35 | @JsonProperty("pnl") BigDecimal pnl, 36 | @JsonProperty("maintenance_margin") BigDecimal maintenanceMargin, 37 | @JsonProperty("initial_margin") BigDecimal initialMargin, 38 | @JsonProperty("side") PositionSide side, 39 | @JsonProperty("quantity") int quantity, 40 | @JsonProperty("average_opening_price") BigDecimal averageOpeningPrice 41 | ) { 42 | checkArgument(instrumentId > 0, "instrumentId=%s <= 0", instrumentId); 43 | checkArgument(quantity >= 0, "quantity=%s < 0", quantity); 44 | this.instrumentId = instrumentId; 45 | this.pnl = pnl; 46 | this.maintenanceMargin = checkNotNull(maintenanceMargin, "null maintenanceMargin"); 47 | this.initialMargin = checkNotNull(initialMargin, "null initialMargin"); 48 | this.side = checkNotNull(side, "null side"); 49 | this.quantity = quantity; 50 | this.averageOpeningPrice = averageOpeningPrice; 51 | } 52 | 53 | public int getInstrumentId() { 54 | return instrumentId; 55 | } 56 | 57 | public BigDecimal getPnl() { 58 | return pnl; 59 | } 60 | 61 | public BigDecimal getMaintenanceMargin() { 62 | return maintenanceMargin; 63 | } 64 | 65 | public BigDecimal getInitialMargin() { 66 | return initialMargin; 67 | } 68 | 69 | /** 70 | * @return side of this position, may be any of LONG, SHORT for a closed position 71 | */ 72 | public PositionSide getSide() { 73 | return side; 74 | } 75 | 76 | /** 77 | * @return nonnegative quantity of this position, zero for a closed position 78 | */ 79 | public int getQuantity() { 80 | return quantity; 81 | } 82 | 83 | public int getQuantitySigned() { 84 | return side == PositionSide.LONG ? quantity : -quantity; 85 | } 86 | 87 | /** 88 | * @return weighted (by quantities) average of the prices this position has been opened at, may be any number for a 89 | * closed position 90 | */ 91 | public BigDecimal getAverageOpeningPrice() { 92 | return averageOpeningPrice; 93 | } 94 | 95 | @Override 96 | public boolean equals(Object o) { 97 | if (this == o) return true; 98 | if (o == null || getClass() != o.getClass()) return false; 99 | OpenPosition that = (OpenPosition) o; 100 | return instrumentId == that.instrumentId && 101 | quantity == that.quantity && 102 | Objects.equal(pnl, that.pnl) && 103 | Objects.equal(maintenanceMargin, that.maintenanceMargin) && 104 | Objects.equal(initialMargin, that.initialMargin) && 105 | side == that.side && 106 | Objects.equal(averageOpeningPrice, that.averageOpeningPrice); 107 | } 108 | 109 | @Override 110 | public int hashCode() { 111 | return Objects.hashCode(instrumentId, pnl, maintenanceMargin, initialMargin, side, quantity, averageOpeningPrice); 112 | } 113 | 114 | @Override 115 | public String toString() { 116 | return MoreObjects.toStringHelper(this) 117 | .add("instrumentId", instrumentId) 118 | .add("pnl", pnl) 119 | .add("maintenanceMargin", maintenanceMargin) 120 | .add("initialMargin", initialMargin) 121 | .add("side", side) 122 | .add("quantity", quantity) 123 | .add("averageOpeningPrice", averageOpeningPrice) 124 | .toString(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/OpenPositionForcefullyClosed.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | import net.quedex.api.user.OpenPosition.PositionSide; 8 | 9 | import java.math.BigDecimal; 10 | 11 | import static com.google.common.base.Preconditions.checkArgument; 12 | import static com.google.common.base.Preconditions.checkNotNull; 13 | 14 | public class OpenPositionForcefullyClosed { 15 | 16 | public enum Cause { 17 | BANKRUPTCY, 18 | DELEVERAGING; 19 | 20 | @JsonCreator 21 | private static Cause deserialize(String value) { 22 | return valueOf(value.toUpperCase()); 23 | } 24 | } 25 | 26 | private final int instrumentId; 27 | private final PositionSide side; 28 | private final int closedQuantity; 29 | private final int remainingQuantity; 30 | private final BigDecimal closePrice; 31 | private final Cause cause; 32 | 33 | @JsonCreator 34 | public OpenPositionForcefullyClosed(final @JsonProperty("instrument_id") int instrumentId, 35 | final @JsonProperty("side") PositionSide side, 36 | final @JsonProperty("closed_quantity") int closedQuantity, 37 | final @JsonProperty("remaining_quantity") int remainingQuantity, 38 | final @JsonProperty("close_price") BigDecimal closePrice, 39 | final @JsonProperty("cause") Cause cause) { 40 | checkArgument(instrumentId > 0, "instrumentId=%s <= 0", instrumentId); 41 | checkArgument(closedQuantity >= 0, "closedQuantity=%s < 0", closedQuantity); 42 | checkArgument(remainingQuantity >= 0, "remainingQuantity=%s < 0", remainingQuantity); 43 | checkArgument(closePrice != null && closePrice.compareTo(BigDecimal.ZERO) > 0, "closePrice=%s <= 0", closePrice); 44 | this.instrumentId = instrumentId; 45 | this.side = checkNotNull(side, "null side"); 46 | this.closedQuantity = closedQuantity; 47 | this.remainingQuantity = remainingQuantity; 48 | this.closePrice = closePrice; 49 | this.cause = checkNotNull(cause, "cause"); 50 | } 51 | 52 | public int getInstrumentId() { 53 | return instrumentId; 54 | } 55 | public PositionSide getSide() { 56 | return side; 57 | } 58 | public int getClosedQuantity() { 59 | return closedQuantity; 60 | } 61 | public int getRemainingQuantity() { 62 | return remainingQuantity; 63 | } 64 | public BigDecimal getClosePrice() { 65 | return closePrice; 66 | } 67 | public Cause getCause() { 68 | return cause; 69 | } 70 | 71 | @Override 72 | public boolean equals(final Object o) { 73 | if (this == o) { 74 | return true; 75 | } 76 | if (o == null || getClass() != o.getClass()) { 77 | return false; 78 | } 79 | final OpenPositionForcefullyClosed that = (OpenPositionForcefullyClosed) o; 80 | return instrumentId == that.instrumentId && 81 | side == that.side && 82 | closedQuantity == that.closedQuantity && 83 | remainingQuantity == that.remainingQuantity && 84 | cause == that.cause && 85 | Objects.equal(closePrice, that.closePrice); 86 | } 87 | 88 | @Override 89 | public int hashCode() { 90 | return Objects.hashCode(instrumentId, side, closedQuantity, remainingQuantity, closePrice, cause); 91 | } 92 | 93 | @Override 94 | public String toString() { 95 | return MoreObjects.toStringHelper(this) 96 | .add("instrumentId", instrumentId) 97 | .add("side", side) 98 | .add("closedQuantity", closedQuantity) 99 | .add("remainingQuantity", remainingQuantity) 100 | .add("closePrice", closePrice) 101 | .add("cause", cause) 102 | .toString(); 103 | } 104 | } 105 | 106 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/OpenPositionListener.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | public interface OpenPositionListener { 4 | 5 | void onOpenPosition(OpenPosition openPosition); 6 | 7 | void onOpenPositionForcefullyClosed(OpenPositionForcefullyClosed openPositionForcefullyClosed); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/OrderCancelFailed.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import static com.google.common.base.Preconditions.checkNotNull; 9 | 10 | public class OrderCancelFailed { 11 | 12 | public enum Cause { 13 | NOT_FOUND, 14 | SESSION_NOT_ACTIVE; 15 | 16 | @JsonCreator 17 | private static Cause deserialize(String value) { 18 | return valueOf(value.toUpperCase()); 19 | } 20 | } 21 | 22 | private final long clientOrderId; 23 | private final Cause cause; 24 | 25 | @JsonCreator 26 | public OrderCancelFailed(@JsonProperty("client_order_id") long clientOrderId, @JsonProperty("cause") Cause cause) { 27 | this.clientOrderId = clientOrderId; 28 | this.cause = checkNotNull(cause, "null cause"); 29 | } 30 | 31 | public long getClientOrderId() { 32 | return clientOrderId; 33 | } 34 | 35 | public Cause getCause() { 36 | return cause; 37 | } 38 | 39 | @Override 40 | public boolean equals(Object o) { 41 | if (this == o) return true; 42 | if (o == null || getClass() != o.getClass()) return false; 43 | OrderCancelFailed that = (OrderCancelFailed) o; 44 | return clientOrderId == that.clientOrderId && 45 | cause == that.cause; 46 | } 47 | 48 | @Override 49 | public int hashCode() { 50 | return Objects.hashCode(clientOrderId, cause); 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | return MoreObjects.toStringHelper(this) 56 | .add("clientOrderId", clientOrderId) 57 | .add("cause", cause) 58 | .toString(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/OrderCancelSpec.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.google.common.base.MoreObjects; 5 | import com.google.common.base.Objects; 6 | 7 | public class OrderCancelSpec implements OrderSpec { 8 | 9 | private final long clientOrderId; 10 | 11 | public OrderCancelSpec(long clientOrderId) { 12 | this.clientOrderId = clientOrderId; 13 | } 14 | 15 | @JsonProperty("client_order_id") 16 | public long getClientOrderId() { 17 | return clientOrderId; 18 | } 19 | 20 | @JsonProperty("type") 21 | private String getType() { 22 | return "cancel_order"; 23 | } 24 | 25 | @Override 26 | public boolean equals(Object o) { 27 | if (this == o) return true; 28 | if (o == null || getClass() != o.getClass()) return false; 29 | OrderCancelSpec that = (OrderCancelSpec) o; 30 | return clientOrderId == that.clientOrderId; 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | return Objects.hashCode(clientOrderId); 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return MoreObjects.toStringHelper(this) 41 | .add("clientOrderId", clientOrderId) 42 | .toString(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/OrderCancelled.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | public class OrderCancelled { 9 | 10 | private final long clientOrderId; 11 | 12 | @JsonCreator 13 | public OrderCancelled(@JsonProperty("client_order_id") long clientOrderId) { 14 | this.clientOrderId = clientOrderId; 15 | } 16 | 17 | public long getClientOrderId() { 18 | return clientOrderId; 19 | } 20 | 21 | @Override 22 | public boolean equals(Object o) { 23 | if (this == o) return true; 24 | if (o == null || getClass() != o.getClass()) return false; 25 | OrderCancelled that = (OrderCancelled) o; 26 | return clientOrderId == that.clientOrderId; 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | return Objects.hashCode(clientOrderId); 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return MoreObjects.toStringHelper(this) 37 | .add("clientOrderId", clientOrderId) 38 | .toString(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/OrderFilled.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import java.math.BigDecimal; 9 | 10 | import static com.google.common.base.Preconditions.checkArgument; 11 | import static com.google.common.base.Preconditions.checkNotNull; 12 | 13 | public class OrderFilled { 14 | 15 | private final long clientOrderId; 16 | private final int instrumentId; 17 | private final BigDecimal orderLimitPrice; 18 | private final OrderType orderType; 19 | private final OrderSide side; 20 | private final int orderInitialQuantity; 21 | private final int leavesOrderQuantity; 22 | private final BigDecimal tradePrice; 23 | private final int filledQuantity; 24 | 25 | @JsonCreator 26 | public OrderFilled(@JsonProperty("client_order_id") long clientOrderId, 27 | @JsonProperty("instrument_id") int instrumentId, 28 | @JsonProperty("order_limit_price") BigDecimal orderLimitPrice, 29 | @JsonProperty("order_side") OrderSide side, 30 | @JsonProperty("order_initial_quantity") int orderInitialQuantity, 31 | @JsonProperty("leaves_order_quantity") int leavesOrderQuantity, 32 | @JsonProperty("trade_price") BigDecimal tradePrice, 33 | @JsonProperty("trade_quantity") int filledQuantity) { 34 | checkArgument(filledQuantity > 0, "filledQuantity=%s <= 0", filledQuantity); 35 | checkArgument(tradePrice.compareTo(BigDecimal.ZERO) > 0, "tradePrice=%s <= 0", tradePrice); 36 | checkArgument(instrumentId > 0, "instrumentId=%s <= 0", instrumentId); 37 | checkArgument(orderLimitPrice.compareTo(BigDecimal.ZERO) > 0, "orderLimitPrice=%s <= 0", orderLimitPrice); 38 | checkArgument(orderInitialQuantity > 0, "orderInitialQuantity=%s "); 39 | this.clientOrderId = clientOrderId; 40 | this.instrumentId = instrumentId; 41 | this.orderLimitPrice = orderLimitPrice; 42 | this.orderType = OrderType.LIMIT; 43 | this.side = checkNotNull(side, "side"); 44 | this.orderInitialQuantity = orderInitialQuantity; 45 | this.leavesOrderQuantity = leavesOrderQuantity; 46 | this.filledQuantity = filledQuantity; 47 | this.tradePrice = tradePrice; 48 | } 49 | 50 | public long getClientOrderId() { 51 | return clientOrderId; 52 | } 53 | 54 | public int getInstrumentId() { 55 | return instrumentId; 56 | } 57 | 58 | public BigDecimal getOrderLimitPrice() { 59 | return orderLimitPrice; 60 | } 61 | 62 | public OrderType getOrderType() { 63 | return orderType; 64 | } 65 | 66 | public OrderSide getSide() { 67 | return side; 68 | } 69 | 70 | public int getOrderInitialQuantity() { 71 | return orderInitialQuantity; 72 | } 73 | 74 | public int getLeavesOrderQuantity() { 75 | return leavesOrderQuantity; 76 | } 77 | 78 | public BigDecimal getTradePrice() { 79 | return tradePrice; 80 | } 81 | 82 | public int getFilledQuantity() { 83 | return filledQuantity; 84 | } 85 | 86 | @Override 87 | public boolean equals(final Object o) { 88 | if (this == o) { 89 | return true; 90 | } 91 | if (o == null || getClass() != o.getClass()) { 92 | return false; 93 | } 94 | final OrderFilled that = (OrderFilled) o; 95 | return clientOrderId == that.clientOrderId && 96 | instrumentId == that.instrumentId && 97 | orderInitialQuantity == that.orderInitialQuantity && 98 | leavesOrderQuantity == that.leavesOrderQuantity && 99 | filledQuantity == that.filledQuantity && 100 | Objects.equal(orderLimitPrice, that.orderLimitPrice) && 101 | orderType == that.orderType && 102 | side == that.side && 103 | Objects.equal(tradePrice, that.tradePrice); 104 | } 105 | 106 | @Override 107 | public int hashCode() { 108 | return Objects.hashCode( 109 | clientOrderId, 110 | instrumentId, 111 | orderLimitPrice, 112 | orderType, 113 | side, 114 | orderInitialQuantity, 115 | leavesOrderQuantity, 116 | tradePrice, 117 | filledQuantity 118 | ); 119 | } 120 | 121 | @Override 122 | public String toString() { 123 | return MoreObjects.toStringHelper(this) 124 | .add("clientOrderId", clientOrderId) 125 | .add("instrumentId", instrumentId) 126 | .add("orderLimitPrice", orderLimitPrice) 127 | .add("orderType", orderType) 128 | .add("side", side) 129 | .add("orderInitialQuantity", orderInitialQuantity) 130 | .add("leavesOrderQuantity", leavesOrderQuantity) 131 | .add("tradePrice", tradePrice) 132 | .add("filledQuantity", filledQuantity) 133 | .toString(); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/OrderForcefullyCancelled.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import static com.google.common.base.Preconditions.checkNotNull; 9 | 10 | /** 11 | * @author Wiktor Gromniak <wgromniak@quedex.net>. 12 | */ 13 | public class OrderForcefullyCancelled { 14 | 15 | public enum Cause { 16 | LIQUIDATION, 17 | SETTLEMENT; 18 | 19 | @JsonCreator 20 | private static Cause deserialize(String value) { 21 | return valueOf(value.toUpperCase()); 22 | } 23 | } 24 | 25 | private final long clientOrderId; 26 | private final Cause cause; 27 | 28 | @JsonCreator 29 | public OrderForcefullyCancelled(@JsonProperty("client_order_id") long clientOrderId, 30 | @JsonProperty("cause") Cause cause) { 31 | this.clientOrderId = clientOrderId; 32 | this.cause = checkNotNull(cause, "cause"); 33 | } 34 | 35 | public long getClientOrderId() { 36 | return clientOrderId; 37 | } 38 | 39 | public Cause getCause() { 40 | return cause; 41 | } 42 | 43 | @Override 44 | public boolean equals(final Object o) { 45 | if (this == o) { 46 | return true; 47 | } 48 | if (o == null || getClass() != o.getClass()) { 49 | return false; 50 | } 51 | final OrderForcefullyCancelled that = (OrderForcefullyCancelled) o; 52 | return clientOrderId == that.clientOrderId && 53 | cause == that.cause; 54 | } 55 | 56 | @Override 57 | public int hashCode() { 58 | return Objects.hashCode(clientOrderId, cause); 59 | } 60 | 61 | @Override 62 | public String toString() { 63 | return MoreObjects.toStringHelper(this) 64 | .add("clientOrderId", clientOrderId) 65 | .add("cause", cause) 66 | .toString(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/OrderListener.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | public interface OrderListener { 4 | 5 | void onOrderPlaced(OrderPlaced orderPlaced); 6 | 7 | void onOrderPlaceFailed(OrderPlaceFailed orderPlaceFailed); 8 | 9 | void onOrderCancelled(OrderCancelled orderCancelled); 10 | 11 | void onOrderForcefullyCancelled(OrderForcefullyCancelled orderForcefullyCancelled); 12 | 13 | void onOrderCancelFailed(OrderCancelFailed orderCancelFailed); 14 | 15 | void onAllOrdersCancelled(); 16 | 17 | void onCancelAllOrdersFailed(CancelAllOrdersFailed cancelAllOrdersFailed); 18 | 19 | void onOrderModified(OrderModified orderModified); 20 | 21 | void onOrderModificationFailed(OrderModificationFailed orderModificationFailed); 22 | 23 | void onOrderFilled(OrderFilled orderFilled); 24 | 25 | void onLiquidationOrderPlaced(LiquidationOrderPlaced liquidationOrderPlaced); 26 | 27 | void onLiquidationOrderCancelled(LiquidationOrderCancelled liquidationOrderCancelled); 28 | 29 | void onLiquidationOrderFilled(LiquidationOrderFilled liquidationOrderFilled); 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/OrderModificationFailed.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import static com.google.common.base.Preconditions.checkNotNull; 9 | 10 | public class OrderModificationFailed { 11 | 12 | public enum Cause { 13 | INVALID_ORDER_ID, 14 | INVALID_INSTRUMENT_ID, 15 | NONPOSITIVE_QUANTITY, 16 | NONPOSITIVE_PRICE, 17 | SESSION_NOT_ACTIVE, 18 | INVALID_TICK_SIZE, 19 | INSUFFICIENT_FUNDS, 20 | MARGIN_CALL, 21 | NOT_FOUND, 22 | OPEN_POSITION_QUANTITY_TOO_HIGH, 23 | NOT_POST; 24 | 25 | @JsonCreator 26 | private static Cause deserialize(String value) { 27 | return valueOf(value.toUpperCase()); 28 | } 29 | } 30 | 31 | private final long clientOrderId; 32 | private final Cause cause; 33 | 34 | public OrderModificationFailed( 35 | @JsonProperty("client_order_id") long clientOrderId, 36 | @JsonProperty("cause") Cause cause 37 | ) { 38 | this.clientOrderId = clientOrderId; 39 | this.cause = checkNotNull(cause, "null cause"); 40 | } 41 | 42 | public long getClientOrderId() { 43 | return clientOrderId; 44 | } 45 | 46 | public Cause getCause() { 47 | return cause; 48 | } 49 | 50 | @Override 51 | public boolean equals(Object o) { 52 | if (this == o) return true; 53 | if (o == null || getClass() != o.getClass()) return false; 54 | OrderModificationFailed that = (OrderModificationFailed) o; 55 | return clientOrderId == that.clientOrderId && 56 | cause == that.cause; 57 | } 58 | 59 | @Override 60 | public int hashCode() { 61 | return Objects.hashCode(clientOrderId, cause); 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | return MoreObjects.toStringHelper(this) 67 | .add("clientOrderId", clientOrderId) 68 | .add("cause", cause) 69 | .toString(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/OrderModificationSpec.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.google.common.base.MoreObjects; 5 | import com.google.common.base.Objects; 6 | 7 | import java.math.BigDecimal; 8 | 9 | import static com.google.common.base.Preconditions.checkArgument; 10 | 11 | public class OrderModificationSpec implements OrderSpec { 12 | 13 | private final long clientOrderId; 14 | private final Integer newQuantity; 15 | private final BigDecimal newLimitPrice; 16 | private final boolean postOnly; 17 | 18 | public OrderModificationSpec(final long clientOrderId, 19 | final int newQuantity, 20 | final BigDecimal newLimitPrice, 21 | final boolean postOnly) { 22 | this(clientOrderId, newQuantity, newLimitPrice, postOnly, null); 23 | } 24 | 25 | public OrderModificationSpec(final long clientOrderId, final int newQuantity, final BigDecimal newLimitPrice) { 26 | this(clientOrderId, newQuantity, newLimitPrice, false, null); 27 | } 28 | 29 | public OrderModificationSpec(long clientOrderId, int newQuantity) { 30 | this(clientOrderId, newQuantity, null, false, null); 31 | } 32 | 33 | public OrderModificationSpec(final long clientOrderId, final BigDecimal newLimitPrice, final boolean postOnly) { 34 | this(clientOrderId, null, newLimitPrice, postOnly, null); 35 | } 36 | 37 | public OrderModificationSpec(final long clientOrderId, final BigDecimal newLimitPrice) { 38 | this(clientOrderId, null, newLimitPrice, false, null); 39 | } 40 | 41 | private OrderModificationSpec(final long clientOrderId, 42 | final Integer newQuantity, 43 | final BigDecimal newLimitPrice, 44 | final boolean postOnly, 45 | final Void dummy) { 46 | checkArgument(clientOrderId > 0, "clientOrderId=%s <= 0", clientOrderId); 47 | checkArgument( 48 | newQuantity != null || newLimitPrice != null, 49 | "at least one of newQuantity and newLimitPrice must be specified" 50 | ); 51 | checkArgument(newQuantity == null || newQuantity > 0, "newQuantity=%s <= 0", newQuantity); 52 | checkArgument( 53 | newLimitPrice == null || newLimitPrice.compareTo(BigDecimal.ZERO) > 0, 54 | "newLImitPrice=%s <= 0", newLimitPrice 55 | ); 56 | 57 | this.clientOrderId = clientOrderId; 58 | this.newQuantity = newQuantity; 59 | this.newLimitPrice = newLimitPrice; 60 | this.postOnly = postOnly; 61 | } 62 | 63 | @JsonProperty("client_order_id") 64 | public long getClientOrderId() { 65 | return clientOrderId; 66 | } 67 | 68 | /** 69 | * @return new quantity of the order if present, null otherwise 70 | */ 71 | @JsonProperty("new_quantity") 72 | public Integer getNewQuantity() { 73 | return newQuantity; 74 | } 75 | 76 | /** 77 | * @return new price of the order if present, null otherwise 78 | */ 79 | @JsonProperty("new_limit_price") 80 | public BigDecimal getNewLimitPrice() { 81 | return newLimitPrice; 82 | } 83 | 84 | @JsonProperty("post_only") 85 | public Boolean getPostOnly() { 86 | return postOnly; 87 | } 88 | 89 | @JsonProperty("type") 90 | private String getType() { 91 | return "modify_order"; 92 | } 93 | 94 | @Override 95 | public boolean equals(Object o) { 96 | if (this == o) return true; 97 | if (o == null || getClass() != o.getClass()) return false; 98 | OrderModificationSpec that = (OrderModificationSpec) o; 99 | return clientOrderId == that.clientOrderId && 100 | Objects.equal(newQuantity, that.newQuantity) && 101 | Objects.equal(newLimitPrice, that.newLimitPrice) && 102 | Objects.equal(postOnly, that.postOnly); 103 | } 104 | 105 | @Override 106 | public int hashCode() { 107 | return Objects.hashCode(clientOrderId, newQuantity, newLimitPrice, postOnly); 108 | } 109 | 110 | @Override 111 | public String toString() { 112 | return MoreObjects.toStringHelper(this) 113 | .add("clientOrderId", clientOrderId) 114 | .add("newQuantity", newQuantity) 115 | .add("newLimitPrice", newLimitPrice) 116 | .add("postOnly", postOnly) 117 | .toString(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/OrderModified.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | public class OrderModified { 9 | 10 | private final long clientOrderId; 11 | 12 | @JsonCreator 13 | public OrderModified(@JsonProperty("client_order_id") long clientOrderId) { 14 | this.clientOrderId = clientOrderId; 15 | } 16 | 17 | public long getClientOrderId() { 18 | return clientOrderId; 19 | } 20 | 21 | @Override 22 | public boolean equals(Object o) { 23 | if (this == o) return true; 24 | if (o == null || getClass() != o.getClass()) return false; 25 | OrderModified that = (OrderModified) o; 26 | return clientOrderId == that.clientOrderId; 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | return Objects.hashCode(clientOrderId); 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return MoreObjects.toStringHelper(this) 37 | .add("clientOrderId", clientOrderId) 38 | .toString(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/OrderPlaceFailed.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import static com.google.common.base.Preconditions.checkNotNull; 9 | 10 | public class OrderPlaceFailed { 11 | 12 | public enum Cause { 13 | INVALID_ORDER_ID, 14 | INVALID_INSTRUMENT_ID, 15 | SESSION_NOT_ACTIVE, 16 | INVALID_TICK_SIZE, 17 | INSUFFICIENT_FUNDS, 18 | TOO_MANY_OPEN_ORDERS, 19 | OPEN_POSITION_QUANTITY_TOO_HIGH, 20 | NOT_POST; 21 | 22 | @JsonCreator 23 | private static Cause deserialize(String value) { 24 | return valueOf(value.toUpperCase()); 25 | } 26 | } 27 | 28 | private final long clientOrderId; 29 | private final Cause cause; 30 | 31 | @JsonCreator 32 | public OrderPlaceFailed(@JsonProperty("client_order_id") long clientOrderId, @JsonProperty("cause") Cause cause) { 33 | this.clientOrderId = clientOrderId; 34 | this.cause = checkNotNull(cause, "Null cause"); 35 | } 36 | 37 | public long getClientOrderId() { 38 | return clientOrderId; 39 | } 40 | 41 | public Cause getCause() { 42 | return cause; 43 | } 44 | 45 | @Override 46 | public boolean equals(Object o) { 47 | if (this == o) return true; 48 | if (o == null || getClass() != o.getClass()) return false; 49 | OrderPlaceFailed that = (OrderPlaceFailed) o; 50 | return clientOrderId == that.clientOrderId && 51 | cause == that.cause; 52 | } 53 | 54 | @Override 55 | public int hashCode() { 56 | return Objects.hashCode(clientOrderId, cause); 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return MoreObjects.toStringHelper(this) 62 | .add("clientOrderId", clientOrderId) 63 | .add("cause", cause) 64 | .toString(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/OrderPlaced.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.google.common.base.MoreObjects; 7 | import com.google.common.base.Objects; 8 | 9 | import java.math.BigDecimal; 10 | 11 | import static com.google.common.base.Preconditions.checkArgument; 12 | import static com.google.common.base.Preconditions.checkNotNull; 13 | 14 | public class OrderPlaced { 15 | 16 | private final long clientOrderId; 17 | private final int instrumentId; 18 | @JsonIgnore 19 | private final OrderType type; 20 | private final BigDecimal price; 21 | private final OrderSide side; 22 | private final int quantity; 23 | private final int initialQuantity; 24 | 25 | @JsonCreator 26 | public OrderPlaced( 27 | @JsonProperty("client_order_id") long clientOrderId, 28 | @JsonProperty("instrument_id") int instrumentId, 29 | @JsonProperty("limit_price") BigDecimal price, 30 | @JsonProperty("side") OrderSide side, 31 | @JsonProperty("quantity") int quantity, 32 | @JsonProperty("initial_quantity") int initialQuantity 33 | ) { 34 | checkArgument(clientOrderId > 0, "clientOrderId=%s <= 0", clientOrderId); 35 | checkArgument(instrumentId > 0, "instrumentId=%s <= 0", instrumentId); 36 | checkArgument(price.compareTo(BigDecimal.ZERO) > 0, "price=%s <= 0", price); 37 | checkArgument(quantity > 0, "quantity=%s <= 0", quantity); 38 | checkArgument(initialQuantity > 0, "initialQuantity=%s <= 0", initialQuantity); 39 | this.clientOrderId = clientOrderId; 40 | this.instrumentId = instrumentId; 41 | this.type = OrderType.LIMIT; 42 | this.price = price; 43 | this.side = checkNotNull(side, "null side"); 44 | this.quantity = quantity; 45 | this.initialQuantity = initialQuantity; 46 | } 47 | 48 | 49 | public long getClientOrderId() { 50 | return clientOrderId; 51 | } 52 | 53 | public int getInstrumentId() { 54 | return instrumentId; 55 | } 56 | 57 | public OrderType getType() { 58 | return type; 59 | } 60 | 61 | public BigDecimal getPrice() { 62 | return price; 63 | } 64 | 65 | public OrderSide getSide() { 66 | return side; 67 | } 68 | 69 | public int getQuantity() { 70 | return quantity; 71 | } 72 | 73 | public int getInitialQuantity() { 74 | return initialQuantity; 75 | } 76 | 77 | @Override 78 | public boolean equals(Object o) { 79 | if (this == o) return true; 80 | if (o == null || getClass() != o.getClass()) return false; 81 | OrderPlaced that = (OrderPlaced) o; 82 | return clientOrderId == that.clientOrderId && 83 | instrumentId == that.instrumentId && 84 | quantity == that.quantity && 85 | initialQuantity == that.initialQuantity && 86 | type == that.type && 87 | Objects.equal(price, that.price) && 88 | side == that.side; 89 | } 90 | 91 | @Override 92 | public int hashCode() { 93 | return Objects.hashCode(clientOrderId, instrumentId, type, price, side, quantity, initialQuantity); 94 | } 95 | 96 | @Override 97 | public String toString() { 98 | return MoreObjects.toStringHelper(this) 99 | .add("clientOrderId", clientOrderId) 100 | .add("instrumentId", instrumentId) 101 | .add("type", type) 102 | .add("price", price) 103 | .add("side", side) 104 | .add("quantity", quantity) 105 | .add("initialQuantity", initialQuantity) 106 | .toString(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/OrderSide.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | 5 | public enum OrderSide { 6 | BUY, SELL; 7 | 8 | @JsonCreator 9 | private static OrderSide deserialize(String value) { 10 | return valueOf(value.toUpperCase()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/OrderSpec.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | public interface OrderSpec { } 4 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/OrderType.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | /** 4 | * @author Wiktor Gromniak <wgromniak@quedex.net>. 5 | */ 6 | public enum OrderType { 7 | LIMIT, MARKET 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/TimerAdded.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | public class TimerAdded { 9 | 10 | private final long timerId; 11 | 12 | @JsonCreator 13 | public TimerAdded(final @JsonProperty("timer_id") long timerId) { 14 | this.timerId = timerId; 15 | } 16 | 17 | public long getTimerId() { 18 | return timerId; 19 | } 20 | 21 | @Override 22 | public boolean equals(final Object o) { 23 | if (this == o) { 24 | return true; 25 | } 26 | if (o == null || getClass() != o.getClass()) { 27 | return false; 28 | } 29 | final TimerAdded that = (TimerAdded) o; 30 | return timerId == that.timerId; 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | return Objects.hashCode(timerId); 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return MoreObjects.toStringHelper(this) 41 | .add("timerId", timerId) 42 | .toString(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/TimerCancelFailed.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import static com.google.common.base.Preconditions.checkNotNull; 9 | 10 | public class TimerCancelFailed { 11 | 12 | public enum Cause { 13 | NOT_FOUND; 14 | 15 | @JsonCreator 16 | private static Cause deserialize(String value) { 17 | return valueOf(value.toUpperCase()); 18 | } 19 | } 20 | 21 | private final long timerId; 22 | private final Cause cause; 23 | 24 | @JsonCreator 25 | public TimerCancelFailed(final @JsonProperty("timer_id") long timerId, 26 | final @JsonProperty("cause") Cause cause) { 27 | this.timerId = timerId; 28 | this.cause = checkNotNull(cause, "Null cause"); 29 | } 30 | 31 | public long getTimerId() { 32 | return timerId; 33 | } 34 | 35 | public Cause getCause() { 36 | return cause; 37 | } 38 | 39 | @Override 40 | public boolean equals(Object o) { 41 | if (this == o) return true; 42 | if (o == null || getClass() != o.getClass()) return false; 43 | TimerCancelFailed that = (TimerCancelFailed) o; 44 | return timerId == that.timerId && 45 | cause == that.cause; 46 | } 47 | 48 | @Override 49 | public int hashCode() { 50 | return Objects.hashCode(timerId, cause); 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | return MoreObjects.toStringHelper(this) 56 | .add("timerId", timerId) 57 | .add("cause", cause) 58 | .toString(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/TimerCancelled.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | public class TimerCancelled { 9 | 10 | private final long timerId; 11 | 12 | @JsonCreator 13 | public TimerCancelled(final @JsonProperty("timer_id") long timerId) { 14 | this.timerId = timerId; 15 | } 16 | 17 | public long getTimerId() { 18 | return timerId; 19 | } 20 | 21 | @Override 22 | public boolean equals(final Object o) { 23 | if (this == o) { 24 | return true; 25 | } 26 | if (o == null || getClass() != o.getClass()) { 27 | return false; 28 | } 29 | final TimerCancelled that = (TimerCancelled) o; 30 | return timerId == that.timerId; 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | return Objects.hashCode(timerId); 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return MoreObjects.toStringHelper(this) 41 | .add("timerId", timerId) 42 | .toString(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/TimerExpired.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | public class TimerExpired { 9 | 10 | private final long timerId; 11 | 12 | @JsonCreator 13 | public TimerExpired(final @JsonProperty("timer_id") long timerId) { 14 | this.timerId = timerId; 15 | } 16 | 17 | public long getTimerId() { 18 | return timerId; 19 | } 20 | 21 | @Override 22 | public boolean equals(final Object o) { 23 | if (this == o) { 24 | return true; 25 | } 26 | if (o == null || getClass() != o.getClass()) { 27 | return false; 28 | } 29 | final TimerExpired that = (TimerExpired) o; 30 | return timerId == that.timerId; 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | return Objects.hashCode(timerId); 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return MoreObjects.toStringHelper(this) 41 | .add("timerId", timerId) 42 | .toString(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/TimerListener.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | public interface TimerListener { 4 | 5 | void onTimerAdded(TimerAdded timerAdded); 6 | 7 | void onTimerRejected(TimerRejected timerRejected); 8 | 9 | void onTimerExpired(TimerExpired timerExpired); 10 | 11 | void onTimerTriggered(TimerTriggered timerTriggered); 12 | 13 | void onTimerUpdated(TimerUpdated timerUpdated); 14 | 15 | void onTimerUpdateFailed(TimerUpdateFailed timerUpdateFailed); 16 | 17 | void onTimerCancelled(TimerCancelled timerCancelled); 18 | 19 | void onTimerCancelFailed(TimerCancelFailed timerCancelFailed); 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/TimerRejected.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import static com.google.common.base.Preconditions.checkNotNull; 9 | 10 | public class TimerRejected { 11 | 12 | public enum Cause { 13 | TOO_MANY_ACTIVE_TIMERS, 14 | TIMER_ALREADY_EXPIRED, 15 | TIMER_ALREADY_EXISTS; 16 | 17 | @JsonCreator 18 | private static Cause deserialize(String value) { 19 | return valueOf(value.toUpperCase()); 20 | } 21 | } 22 | 23 | private final long timerId; 24 | private final Cause cause; 25 | 26 | @JsonCreator 27 | public TimerRejected(final @JsonProperty("timer_id") long timerId, 28 | final @JsonProperty("cause") Cause cause) { 29 | this.timerId = timerId; 30 | this.cause = checkNotNull(cause, "Null cause"); 31 | } 32 | 33 | public long getTimerId() { 34 | return timerId; 35 | } 36 | 37 | public Cause getCause() { 38 | return cause; 39 | } 40 | 41 | @Override 42 | public boolean equals(Object o) { 43 | if (this == o) return true; 44 | if (o == null || getClass() != o.getClass()) return false; 45 | TimerRejected that = (TimerRejected) o; 46 | return timerId == that.timerId && 47 | cause == that.cause; 48 | } 49 | 50 | @Override 51 | public int hashCode() { 52 | return Objects.hashCode(timerId, cause); 53 | } 54 | 55 | @Override 56 | public String toString() { 57 | return MoreObjects.toStringHelper(this) 58 | .add("timerId", timerId) 59 | .add("cause", cause) 60 | .toString(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/TimerTriggered.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | public class TimerTriggered { 9 | 10 | private final long timerId; 11 | 12 | @JsonCreator 13 | public TimerTriggered(final @JsonProperty("timer_id") long timerId) { 14 | this.timerId = timerId; 15 | } 16 | 17 | public long getTimerId() { 18 | return timerId; 19 | } 20 | 21 | @Override 22 | public boolean equals(final Object o) { 23 | if (this == o) { 24 | return true; 25 | } 26 | if (o == null || getClass() != o.getClass()) { 27 | return false; 28 | } 29 | final TimerTriggered that = (TimerTriggered) o; 30 | return timerId == that.timerId; 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | return Objects.hashCode(timerId); 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return MoreObjects.toStringHelper(this) 41 | .add("timerId", timerId) 42 | .toString(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/TimerUpdateFailed.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | import static com.google.common.base.Preconditions.checkNotNull; 9 | 10 | public class TimerUpdateFailed { 11 | 12 | public enum Cause { 13 | NOT_FOUND, 14 | TIMER_EXECUTION_INTERVAL_BROKEN; 15 | 16 | @JsonCreator 17 | private static Cause deserialize(String value) { 18 | return valueOf(value.toUpperCase()); 19 | } 20 | } 21 | 22 | private final long timerId; 23 | private final Cause cause; 24 | 25 | @JsonCreator 26 | public TimerUpdateFailed(final @JsonProperty("timer_id") long timerId, 27 | final @JsonProperty("cause") Cause cause) { 28 | this.timerId = timerId; 29 | this.cause = checkNotNull(cause, "Null cause"); 30 | } 31 | 32 | public long getTimerId() { 33 | return timerId; 34 | } 35 | 36 | public Cause getCause() { 37 | return cause; 38 | } 39 | 40 | @Override 41 | public boolean equals(Object o) { 42 | if (this == o) return true; 43 | if (o == null || getClass() != o.getClass()) return false; 44 | TimerUpdateFailed that = (TimerUpdateFailed) o; 45 | return timerId == that.timerId && 46 | cause == that.cause; 47 | } 48 | 49 | @Override 50 | public int hashCode() { 51 | return Objects.hashCode(timerId, cause); 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | return MoreObjects.toStringHelper(this) 57 | .add("timerId", timerId) 58 | .add("cause", cause) 59 | .toString(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/TimerUpdated.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.common.base.MoreObjects; 6 | import com.google.common.base.Objects; 7 | 8 | public class TimerUpdated { 9 | 10 | private final long timerId; 11 | 12 | @JsonCreator 13 | public TimerUpdated(final @JsonProperty("timer_id") long timerId) { 14 | this.timerId = timerId; 15 | } 16 | 17 | public long getTimerId() { 18 | return timerId; 19 | } 20 | 21 | @Override 22 | public boolean equals(final Object o) { 23 | if (this == o) { 24 | return true; 25 | } 26 | if (o == null || getClass() != o.getClass()) { 27 | return false; 28 | } 29 | final TimerUpdated that = (TimerUpdated) o; 30 | return timerId == that.timerId; 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | return Objects.hashCode(timerId); 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return MoreObjects.toStringHelper(this) 41 | .add("timerId", timerId) 42 | .toString(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/UserMessageSender.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.fasterxml.jackson.databind.ObjectWriter; 7 | import com.fasterxml.jackson.databind.node.ObjectNode; 8 | import net.quedex.api.common.CommunicationException; 9 | import net.quedex.api.common.DisconnectedException; 10 | import net.quedex.api.common.MessageReceiver; 11 | import net.quedex.api.common.StreamFailureListener; 12 | import net.quedex.api.pgp.BcEncryptor; 13 | import net.quedex.api.pgp.PGPExceptionBase; 14 | import org.java_websocket.client.WebSocketClient; 15 | import org.java_websocket.exceptions.WebsocketNotConnectedException; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | import java.util.List; 20 | import java.util.concurrent.ExecutorService; 21 | import java.util.concurrent.Executors; 22 | import java.util.function.Supplier; 23 | 24 | import static com.google.common.base.Preconditions.checkArgument; 25 | import static com.google.common.base.Preconditions.checkNotNull; 26 | import static com.google.common.base.Preconditions.checkState; 27 | 28 | class UserMessageSender { 29 | 30 | private static final Logger LOGGER = LoggerFactory.getLogger(UserMessageSender.class); 31 | private static final ObjectMapper OBJECT_MAPPER = MessageReceiver.OBJECT_MAPPER; 32 | private static final ObjectWriter OBJECT_WRITER = OBJECT_MAPPER.writer(); 33 | 34 | private final WebSocketClient webSocketClient; 35 | private final BcEncryptor encryptor; 36 | private final long accountId; 37 | private final int nonceGroup; 38 | private final ExecutorService executor = Executors.newSingleThreadExecutor(); // single threaded for sequencing 39 | 40 | private volatile StreamFailureListener streamFailureListener; 41 | private volatile long nonce; 42 | 43 | UserMessageSender( 44 | WebSocketClient webSocketClient, 45 | long accountId, 46 | int nonceGroup, 47 | BcEncryptor encryptor 48 | ) { 49 | checkArgument(nonceGroup >= 0, "nonceGroup=%s < 0", nonceGroup); 50 | checkArgument(accountId > 0, "accountId=%s <= 0", accountId); 51 | this.webSocketClient = checkNotNull(webSocketClient, "null webSocketClient"); 52 | this.encryptor = checkNotNull(encryptor); 53 | this.accountId = accountId; 54 | this.nonceGroup = nonceGroup; 55 | } 56 | 57 | void registerStreamFailureListener(StreamFailureListener streamFailureListener) { 58 | this.streamFailureListener = streamFailureListener; 59 | } 60 | 61 | void setStartNonce(long startNonce) { 62 | LOGGER.debug("setStartNonce({})", startNonce); 63 | nonce = startNonce; 64 | } 65 | 66 | void sendGetLastNonce() throws CommunicationException { 67 | try { 68 | sendMessage( 69 | OBJECT_MAPPER.createObjectNode() 70 | .put("type", "get_last_nonce") 71 | .put("nonce_group", nonceGroup) 72 | .put("account_id", accountId) 73 | ); 74 | } catch (PGPExceptionBase | JsonProcessingException e) { 75 | throw new CommunicationException("Error sending get_last_nonce", e); 76 | } 77 | } 78 | 79 | void sendSubscribe() { 80 | sendMessageQueued(() -> addNonceAccountId(OBJECT_MAPPER.createObjectNode().put("type", "subscribe"))); 81 | } 82 | 83 | void sendOrderSpec(OrderSpec orderSpec) { 84 | sendMessageQueued(() -> addNonceAccountId(OBJECT_MAPPER.valueToTree(orderSpec))); 85 | } 86 | 87 | void sendBatch(List batch) { 88 | sendMessageQueued(() -> createBatchNode(batch)); 89 | } 90 | 91 | void sendTimeTriggeredBatch(long timerId, 92 | long executionStartTimestamp, 93 | long executionExpirationTimestamp, 94 | List batch) { 95 | sendMessageQueued(() -> { 96 | final ObjectNode mainCommand = OBJECT_MAPPER.createObjectNode() 97 | .put("type", "add_timer") 98 | .put("timer_id", timerId) 99 | .put("execution_start_timestamp", executionStartTimestamp) 100 | .put("execution_expiration_timestamp", executionExpirationTimestamp); 101 | 102 | addNonceAccountId(mainCommand); 103 | 104 | mainCommand.set("command", createBatchNode(batch)); 105 | 106 | return mainCommand; 107 | }); 108 | } 109 | 110 | void sendTimeTriggeredBatchUpdate(long timerId, 111 | Long executionStartTimestamp, 112 | Long executionExpirationTimestamp, 113 | List batch) { 114 | sendMessageQueued(() -> { 115 | final ObjectNode mainCommand = OBJECT_MAPPER.createObjectNode() 116 | .put("type", "update_timer") 117 | .put("timer_id", timerId) 118 | .put("new_execution_start_timestamp", executionStartTimestamp) 119 | .put("new_execution_expiration_timestamp", executionExpirationTimestamp); 120 | 121 | addNonceAccountId(mainCommand); 122 | 123 | mainCommand.set("new_command", batch != null ? createBatchNode(batch) : null); 124 | 125 | return mainCommand; 126 | }); 127 | } 128 | 129 | void sendTimeTriggeredBatchCancellation(long timerId) { 130 | sendMessageQueued(() -> { 131 | final ObjectNode cancelCommand = OBJECT_MAPPER.createObjectNode() 132 | .put("type", "cancel_timer") 133 | .put("timer_id", timerId); 134 | 135 | addNonceAccountId(cancelCommand); 136 | 137 | return cancelCommand; 138 | }); 139 | } 140 | 141 | void sendInternalTransfer(InternalTransfer internalTransfer) { 142 | sendMessageQueued(() -> addNonceAccountId(OBJECT_MAPPER.valueToTree(internalTransfer))); 143 | } 144 | 145 | void stop() { 146 | executor.shutdown(); 147 | } 148 | 149 | private JsonNode createBatchNode(final List batch) { 150 | JsonNode batchJson = OBJECT_MAPPER.valueToTree(batch); 151 | for (final JsonNode node : batchJson) { 152 | checkState(node instanceof ObjectNode, "Expected ObjectNode"); 153 | addNonceAccountId((ObjectNode) node); 154 | } 155 | return OBJECT_MAPPER.createObjectNode() 156 | .put("type", "batch") 157 | .put("account_id", accountId) 158 | .set("batch", batchJson); 159 | } 160 | 161 | private void sendMessageQueued(Supplier supplier) { 162 | executor.execute(() -> { 163 | try { 164 | sendMessage(supplier.get()); 165 | } catch (WebsocketNotConnectedException e) { 166 | onError(new DisconnectedException(e)); 167 | } catch (Exception e) { 168 | onError(new CommunicationException("Error sending message", e)); 169 | } 170 | }); 171 | } 172 | 173 | private ObjectNode addNonceAccountId(ObjectNode jsonMessage) { 174 | return jsonMessage 175 | .put("account_id", accountId) 176 | .put("nonce", getNonce()) 177 | .put("nonce_group", nonceGroup); 178 | } 179 | 180 | private long getNonce() { 181 | return ++nonce; 182 | } 183 | 184 | private void sendMessage(JsonNode jsonMessage) throws JsonProcessingException, PGPExceptionBase { 185 | String messageStr = OBJECT_WRITER.writeValueAsString(jsonMessage); 186 | webSocketClient.send(encryptor.encrypt(messageStr, true)); 187 | 188 | LOGGER.trace("sendMessage({})", messageStr); 189 | } 190 | 191 | private void onError(Exception e) { 192 | LOGGER.warn("onError({})", e); 193 | StreamFailureListener streamFailureListener = this.streamFailureListener; 194 | if (streamFailureListener != null) { 195 | streamFailureListener.onStreamFailure(e); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/main/java/net/quedex/api/user/UserStream.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.user; 2 | 3 | import net.quedex.api.common.CommunicationException; 4 | import net.quedex.api.common.StreamFailureListener; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Represents the stream of realtime private data streamed from and trading commands which may be sent to Quedex. 10 | * Allows registering and subscribing for particular data types. The registered listeners will be called (in a single 11 | * thread) for every event that arrives. Trading commands include placing, canceling and modifying orders - batching of 12 | * these commands is possible via {@link #batch} methods and should be used whenever possible. The data exchanged on the 13 | * stream has the form of PGP-encrypted messages - all the encryption/decryption and serialization/deserialization is 14 | * handled by the implementations and the interaction with the stream is based on Java objects. 15 | *

16 | * The stream gives some guarantees about the order of received events useful for state initialisation - see 17 | * documentation of {@link #subscribeListeners}. 18 | *

19 | * To handle all errors properly, always {@link #registerStreamFailureListener} before {@link #start}ing the stream. 20 | */ 21 | public interface UserStream { 22 | 23 | void registerStreamFailureListener(StreamFailureListener streamFailureListener); 24 | 25 | void start() throws CommunicationException; 26 | 27 | void registerOrderListener(OrderListener orderListener); 28 | 29 | void registerOpenPositionListener(OpenPositionListener openPositionListener); 30 | 31 | void registerAccountStateListener(AccountStateListener accountStateListener); 32 | 33 | void registerInternalTransferListener(InternalTransferListener listener); 34 | 35 | void registerTimerListener(TimerListener listener); 36 | 37 | /** 38 | * Subscribes previously registered listeners. Causes a welcome package to be sent to the listeners. The welcome 39 | * package includes: 40 | *

45 | * 46 | * The welcome package constitutes an initial state that will be modified by the subsequent events received by the 47 | * listeners. 48 | *

49 | * The first received {@link AccountState} marks the end of the welcome package and may be used to detect the end 50 | * of initialisation. 51 | */ 52 | void subscribeListeners(); 53 | 54 | /** 55 | * Sends the given {@link LimitOrderSpec} to the exchange. This method is asynchronous - the fact that it returned 56 | * does not guarantee that the command has been received nor processed by the exchange. 57 | */ 58 | void placeOrder(LimitOrderSpec limitOrderSpec); 59 | 60 | /** 61 | * Sends the given {@link OrderCancelSpec} to the exchange. This method is asynchronous - the fact that it returned 62 | * does not guarantee that the command has been received nor processed by the exchange. 63 | */ 64 | void cancelOrder(OrderCancelSpec orderCancelSpec); 65 | 66 | /** 67 | * Sends command to cancel all pending orders for an user to the exchange. This method is asynchronous - the fact 68 | * that it returned does not guarantee that the command has ben received nor processed by the exchange. 69 | */ 70 | void cancelAllOrders(); 71 | 72 | /** 73 | * Sends the given {@link OrderModificationSpec} to the exchange. This method is asynchronous - the fact that it 74 | * returned does not guarantee that the command has been received nor processed by the exchange. 75 | */ 76 | void modifyOrder(OrderModificationSpec orderModificationSpec); 77 | 78 | /** 79 | * Returns an object (not thread-safe) which may be used fluently to send a batch of {@link OrderSpec}s to the 80 | * exchange. Calling {@link Batch#send()} sends batched {@link OrderSpec}s to the exchange. This method is 81 | * asynchronous - the fact that it returned does not guarantee that the commands have been received nor processed by 82 | * the exchange. 83 | */ 84 | Batch batch(); 85 | 86 | /** 87 | * Sends the given list of {@link OrderSpec}s to the exchange. This method is asynchronous - the fact that it 88 | * returned does not guarantee that the commands have been received nor processed by the exchange. 89 | */ 90 | void batch(List batch); 91 | 92 | /** 93 | *

94 | * Returns an object (not thread-safe) which may be used fluently to create a time triggered batch of {@link OrderSpec}s. 95 | * Calling {@link Batch#send()} sends this batch of {@link OrderSpec}s to the exchange. 96 | *

97 | *

98 | * When a time triggered batch is received by the exchange engine, a new timer is registered. 99 | * Based on the timer configuration, at some point in the future (between executionStartTimestamp and executionExpirationTimestamp), 100 | * all the carried order commands are processed, one by one, in the creation order. 101 | *

102 | *

103 | * Please refer to the API documentation for detailed explanation of creating timers 104 | * (https://quedex.net/doc/api/). 105 | *

106 | * This method is asynchronous - the fact that it returned does not guarantee that the commands have been received 107 | * nor processed by the exchange. 108 | * 109 | * @param timerId a user defined timer identifier, can be used to cancel or update batch 110 | * @param executionStartTimestamp the defined batch will not be executed before this timestamp 111 | * @param executionExpirationTimestamp the defined batch will not ne executed after this timestamp 112 | */ 113 | Batch timeTriggeredBatch(long timerId, long executionStartTimestamp, long executionExpirationTimestamp); 114 | 115 | /** 116 | *

117 | * Sends a time triggered batch with the given list of {@link OrderSpec}s to the exchange. 118 | *

119 | *

120 | * When a time triggered batch is received by the exchange engine, a new timer is registered. 121 | * Based on the timer configuration, at some point in the future (between executionStartTimestamp and executionExpirationTimestamp), 122 | * all the carried order commands are processed, one by one, in the creation order. 123 | *

124 | *

125 | * Please refer to the API documentation for detailed explanation of creating timers 126 | * (https://quedex.net/doc/api/). 127 | *

128 | * This method is asynchronous - the fact that it returned does not guarantee that the commands have been received 129 | * nor processed by the exchange. 130 | * 131 | * @param timerId a user defined batch identifier, can be used to cancel or update batch 132 | * @param executionStartTimestamp the defined batch will not be executed before this timestamp 133 | * @param executionExpirationTimestamp the defined batch will not be executed after this timestamp 134 | * @param batch list of {@link OrderSpec}s to be executed 135 | */ 136 | void timeTriggeredBatch(long timerId, 137 | long executionStartTimestamp, 138 | long executionExpirationTimestamp, 139 | List batch); 140 | 141 | /** 142 | * Returns an object (not thread-safe) which may be used fluently to update an already existing batch of {@link OrderSpec}s on the 143 | * exchange. At least one of the following must be modified: 144 | * 149 | *

150 | * Specified commands replace commands registered during the timer creation. 151 | * Calling {@link Batch#send()} sends modified batch of {@link OrderSpec}s to the exchange. 152 | *

153 | *

154 | * Please refer to the API documentation for detailed explanation of updating timers 155 | * (https://quedex.net/doc/api/). 156 | *

157 | * This method is asynchronous - the fact that it returned does not guarantee that the commands have been received 158 | * nor processed by the exchange. 159 | * 160 | * @param timerId a user defined timer identifier, the same as used when creating the batch 161 | * @param executionStartTimestamp new value of executionStartTimestamp (optional) 162 | * @param executionExpirationTimestamp new value of executionExpirationTimestamp (optional) 163 | */ 164 | Batch updateTimeTriggeredBatch(long timerId, Long executionStartTimestamp, Long executionExpirationTimestamp); 165 | 166 | /** 167 | * 168 | * Sends the modified batch to the exchange. At least one of the following must be modified: 169 | * 174 | *

175 | * Specified commands replace commands registered during the timer creation. 176 | *

177 | *

178 | * Please refer to the API documentation for detailed explanation of updating timers 179 | * (https://quedex.net/doc/api/). 180 | *

181 | * This method is asynchronous - the fact that it returned does not guarantee that the commands have been received 182 | * nor processed by the exchange. 183 | * 184 | * @param timerId a user defined timer identifier, the same as used when creating the batch 185 | * @param executionStartTimestamp new value of executionStartTimestamp (optional) 186 | * @param executionExpirationTimestamp new value of executionExpirationTimestamp (optional) 187 | * @param batch new value of batch (optional) 188 | */ 189 | void updateTimeTriggeredBatch(long timerId, 190 | Long executionStartTimestamp, 191 | Long executionExpirationTimestamp, 192 | List batch); 193 | 194 | /** 195 | * Sends command to cancel an existing time triggered batch. 196 | * @param timerId a user defined batch identifier, the same as used when creating the batch 197 | */ 198 | void cancelTimeTriggeredBatch(long timerId); 199 | 200 | void executeInternalTransfer(InternalTransfer internalTransfer); 201 | 202 | void stop() throws CommunicationException; 203 | 204 | interface Batch { 205 | 206 | Batch placeOrder(LimitOrderSpec limitOrderSpec); 207 | 208 | Batch placeOrders(List limitOrderSpecs); 209 | 210 | Batch cancelOrder(OrderCancelSpec orderCancelSpec); 211 | 212 | Batch cancelOrders(List orderCancelSpecs); 213 | 214 | Batch cancelAllOrders(); 215 | 216 | Batch modifyOrder(OrderModificationSpec orderModificationSpec); 217 | 218 | Batch modifyOrders(List orderModificationSpec); 219 | 220 | void send(); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/main/resources/qdxConfig.properties.example: -------------------------------------------------------------------------------- 1 | net.quedex.client.api.marketStreamUrl = wss://api.quedex.net/market_stream 2 | net.quedex.client.api.userStreamUrl = wss://api.quedex.net/user_stream 3 | 4 | net.quedex.client.api.accountId = 123456789 5 | # value between 0 and 9, has to be different for every WebSocket connection opened to the exchange 6 | # (e.g. browser and trading bot); our webapp uses nonce_group=0 7 | net.quedex.client.api.nonceGroup = 5 8 | 9 | net.quedex.client.api.qdxPublicKey = -----BEGIN PGP PUBLIC KEY BLOCK-----\n\ 10 | Version: QPG\n\ 11 | \n\ 12 | mQENBFlPvjsBCACr/UfHzXAezskLqcq9NiiaNFDDT5A+biC8VrOglB0ZSQOYRira\n\ 13 | NgQ2Cp8Jd+XU77F+J1012BjB5y87Z+hdnwBDsqF7CjkjeQzsE3PSvm9I+E3cneqx\n\ 14 | UcinRaUD1wfwVytbg9Q9rpqQ7CTjVWY1UPYjs6dAo1WAp/ux/VTeOFbpO0R3D7if\n\ 15 | ZGY1QeISRpLWiMpcG2YCOALnuazABVCNXLhVqa8Y7tt2I+cI0uE9tBf41gjGPPtd\n\ 16 | KdASPVz1plpOEl2dOpmy8jICqcSzUsT4Sy8vAqW3U1HF+TA2QGRcrrUItL4GjxNL\n\ 17 | lcL8wh7mclsjRe5Q5dYnrACC9NWS6vSp/eAPABEBAAG0G1F1ZWRleCA8Y29udGFj\n\ 18 | dEBxdWVkZXgubmV0PokBOAQTAQIAIgUCWU++OwIbAwYLCQgHAwIGFQgCCQoLBBYC\n\ 19 | AwECHgECF4AACgkQzsLQUmv6vk9Rlwf+LiJA37dhDdGFU/fexNRHZWTUh2TdqBsv\n\ 20 | MiNtarf+HlZIioWMCzlHmb3bolVrfFUNUh/GGlPENtlaSmFGuPhMlFcNDGYM+I7k\n\ 21 | ufhM95jxmtzy97NYMeMx4xjnaBAu8kFsvi80BR/05ZhCHqyI3K9NpYoXBfsyzss+\n\ 22 | j/jX1NHayzMmXNdqQ5JjzuICZj0EY9ryLP/jPAZ6DS9LVwi9Vr2JzZheCx5Q77Ud\n\ 23 | HuGTOBu3Azor2f4n4ccELs7lgU7uGrt1cK/oiML9UDmqjelunzTFU/5Q0tp7C3Qm\n\ 24 | 1wymd+PYTvvX/5htnLar1nIuYmmvtCZb1zyuzPzJWWtCcFFsiV9kerkBDQRZT747\n\ 25 | AQgAn/9rwQn4yM7pVYO9rTxNveI60lgp4RLKi1WYpAy6gxwUp3zLE/h59rykb7qM\n\ 26 | 9QArABsMEGGKt9urq7hjsKu8bM+hVTcAuoDre5qNFEfhZuPBS9LF7wQWDikORZxR\n\ 27 | Mk5WIiCt3U2soQ4Lismw1bLDX8uqkv3GFtR+IaKzuwYBEVPwuZ15EOt9G83JR3uV\n\ 28 | MKqeUtFW9+z5WEAh2JLU6C357sftJIJeWDEgF2TPtQOzc8isI8rpIFNyl6x1Aiy6\n\ 29 | LaSWmOI3d9EQ8SH4LxCXtAgWvnIoPL0JsP5/FWzt6qJR4teu+A2xwG7001va+DUc\n\ 30 | 34AbSV9Ogqa519OfbKK6HDyFIQARAQABiQEfBBgBAgAJBQJZT747AhsMAAoJEM7C\n\ 31 | 0FJr+r5PtEUH/0KmXQWbm4qXxOnaXrk+CKLDBxtfY6BaoJ6ekdGfqVMd8YM+UGnL\n\ 32 | 6d49vex4O80uIhIDSex446gKVlhdwOjIlUFmTCtMgGOa06G2T4sx8a9y2UYK45hN\n\ 33 | rj9aVfhJ8nn9yuPj7cBNtLEYJ4VkRKxJO9XX8cfhUsomhB3DQDbOLfikYqfmupm6\n\ 34 | mYX84CO/DD8JAXx4qt9Rg+5AUQegq26iZ/og1ZjYZ/tvBjrc45u23XCWvgVHbGhb\n\ 35 | wWCNjZijaY1VnTwTe6uZv1AqovZpprqZKWImN5myaJI3AJU2W2FCbI0ezfoVEVO4\n\ 36 | zMipOYZzRziJeCz1gX9geNseLvfJ8EtZRKU=\n\ 37 | =e4C9\n\ 38 | -----END PGP PUBLIC KEY BLOCK----- 39 | 40 | net.quedex.client.api.userPrivateKey = -------------------------------------------------------------------------------- /src/test/java/net/quedex/api/market/WebsocketMarketStreamIT.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.market; 2 | 3 | import net.quedex.api.common.Config; 4 | import net.quedex.api.common.StreamFailureListener; 5 | import net.quedex.api.testcommons.Utils; 6 | import org.hamcrest.BaseMatcher; 7 | import org.hamcrest.Description; 8 | import org.hamcrest.Matcher; 9 | import org.mockito.Mock; 10 | import org.mockito.MockitoAnnotations; 11 | import org.testng.annotations.BeforeMethod; 12 | import org.testng.annotations.Test; 13 | 14 | import java.util.ArrayList; 15 | import java.util.Collections; 16 | import java.util.List; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.mockito.Matchers.any; 20 | import static org.mockito.Matchers.argThat; 21 | import static org.mockito.Mockito.never; 22 | import static org.mockito.Mockito.timeout; 23 | import static org.mockito.Mockito.verify; 24 | 25 | /** 26 | * An integration test with live Quedex Websocket. To run it: 27 | *
    28 | *
  1. Copy {@code market.properties.example} to {@code market.properties} in test resources.
  2. 29 | *
  3. Fill in the details in the properties file.
  4. 30 | *
  5. Enable the test method.
  6. 31 | *
  7. Run the test.
  8. 32 | *
33 | */ 34 | public class WebsocketMarketStreamIT { 35 | 36 | private static final boolean TEST_ENABLED = false; // enable to run 37 | 38 | @Mock private OrderBookListener orderBookListener; 39 | @Mock private QuotesListener quotesListener; 40 | @Mock private SessionStateListener sessionStateListener; 41 | @Mock private TradeListener tradeListener; 42 | @Mock private StreamFailureListener streamFailureListener; 43 | 44 | private MarketStream marketStream; 45 | 46 | @BeforeMethod 47 | public void setUp() throws Exception { 48 | MockitoAnnotations.initMocks(this); 49 | 50 | Config config = Config.fromResource(Utils.getKeyPassphraseFromProps()); 51 | marketStream = new WebsocketMarketStream(config); 52 | } 53 | 54 | @Test(enabled = TEST_ENABLED) 55 | public void testIntegrationWithLiveWS() throws Exception { 56 | 57 | List instrumentIds = Collections.synchronizedList(new ArrayList<>()); 58 | 59 | marketStream.start(); 60 | 61 | marketStream.registerStreamFailureListener(streamFailureListener); 62 | marketStream.registerAndSubscribeSessionStateListener(sessionStateListener); 63 | 64 | marketStream.registerInstrumentsListener(instruments -> { 65 | instrumentIds.addAll(instruments.keySet()); 66 | assertThat(instrumentIds).isNotEmpty(); 67 | 68 | System.out.println("instrumentIds = " + instrumentIds); // for debugging 69 | 70 | marketStream.registerOrderBookListener(orderBookListener).subscribe(instrumentIds); 71 | marketStream.registerQuotesListener(quotesListener).subscribe(instrumentIds); 72 | marketStream.registerTradeListener(tradeListener).subscribe(instrumentIds); 73 | }); 74 | 75 | instrumentIds 76 | .forEach(id -> verify(orderBookListener, timeout(1000)).onOrderBook(argThat(obHasInstrumentId(id)))); 77 | instrumentIds 78 | .forEach(id -> verify(quotesListener, timeout(1000)).onQuotes(argThat(quotesHaveInstrumentId(id)))); 79 | verify(sessionStateListener, timeout(1000)).onSessionState(any()); 80 | 81 | marketStream.stop(); 82 | 83 | verify(streamFailureListener, never()).onStreamFailure(any()); 84 | } 85 | 86 | private static Matcher obHasInstrumentId(int id) { 87 | return new BaseMatcher() { 88 | @Override 89 | public boolean matches(Object o) { 90 | if (o instanceof OrderBook) { 91 | return ((OrderBook) o).getInstrumentId() == id; 92 | } else { 93 | return false; 94 | } 95 | } 96 | 97 | @Override 98 | public void describeTo(Description description) { 99 | description.appendText("OrderBook with instrumentId").appendValue(id); 100 | } 101 | }; 102 | } 103 | 104 | private static Matcher quotesHaveInstrumentId(int id) { 105 | return new BaseMatcher() { 106 | @Override 107 | public boolean matches(Object o) { 108 | if (o instanceof Quotes) { 109 | return ((Quotes) o).getInstrumentId() == id; 110 | } else { 111 | return false; 112 | } 113 | } 114 | 115 | @Override 116 | public void describeTo(Description description) { 117 | description.appendText("Wuotes with instrumentId").appendValue(id); 118 | } 119 | }; 120 | } 121 | } -------------------------------------------------------------------------------- /src/test/java/net/quedex/api/testcommons/Keys.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.testcommons; 2 | 3 | /** 4 | * @author Wiktor Gromniak <wgromniak@quedex.net>. 5 | */ 6 | public class Keys { 7 | 8 | public static final String QUEDEX_PRIVATE = "-----BEGIN PGP public KEY BLOCK-----\n" + 9 | "Version: GnuPG v1\n" + 10 | "\n" + 11 | "lQHYBFluLQMBBADP0huibLNNcLNRcQ7wTtCbn2S8K0q3V1c2rRrcTGMFmoKK+VD5\n" + 12 | "zo/bcbAtfm/6O+cxm+Cj2toK1bsBdF5Ep4C28MHkpbyJIsrwC/LKJ58k0/iMLF/C\n" + 13 | "nFDin6/hsjU2E7LKKC7uz/4F6LKFybM8gomiUFSC5smaeifj4fvAzkFv3QARAQAB\n" + 14 | "AAP9GLFAulBLD5uhRWnMWhjV6B+3XYo78u5254FdjH5WUk9KTSPpZKe6klrUPDDm\n" + 15 | "h3zARWCIKj/cd2bEtPFe1Vkc+FNd8tABkgGPFmd8MlnH2xjJkmTzBd/fU+iLWFFc\n" + 16 | "SeF3fWdW+5y5P4/8jjFIXXE3T6F2d/XBSzflXvmD+7Jv7rsCANrYKeftdr3Vyf79\n" + 17 | "cxHmofPDinIvrpbeDnw2kWLqzpAnpxrhOzZIDFmsJRvgyTYYtXcWv6u/OfuEZKE+\n" + 18 | "AbvhTOcCAPMa0BtbHiYVq3FQhY5KzXNIhg1y8sVIzGUecu6vvJl53s0uvrHc5AAw\n" + 19 | "86ENpxYrwlS2/W4kt4tLAS1YgjiNIJsB/RBdPA0GUTrP89htBUFpc7BIYTry5LMS\n" + 20 | "vNugK1SSYLMGgDNf/DK12EbkjI8rLfY825eNZ6R1PKf+q2blSVTg9SCkD7QIVGVz\n" + 21 | "dCBLZXmIuAQTAQIAIgUCWW4tAwIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AA\n" + 22 | "CgkQ0IVp55u0qmt8XAQAuhhHHz6gpORXxmXUdxEPa1Ulz2m5HbnpdB9O/nqGWlGV\n" + 23 | "KB5AGBEvnyQHN3396yG1f3xwwkt+Ow1Au15g96jBWGgbVGA03gUhmWnEtsR+wMvt\n" + 24 | "5zLrv3zzrWsngKMG88kBBdNCCgEq23ioIhxE2uqXiEhFtvPKk+cSRStn4qy1Q2Wd\n" + 25 | "AdgEWW4tAwEEAK0PXO5uq1qwPyJZyoNugn2TESVNuxADkT/FBW8fELrSrVaW8XCN\n" + 26 | "FpGMxPGtGlWQEXPErrZFibfoptMbRB+tFCEHfqgLQURyQtX3o5B/x5UoHscydIJl\n" + 27 | "vPmaB/JTcj/8Gl+D2vsUvefpeIVZlr3mVJDe2T6iOhnz5Rn5dJ/dgGCTABEBAAEA\n" + 28 | "A/0enp6/MwaPc6qf/coWihl9dWVtt7yWv0LWSRpGiHUR0Q/JR8itNNFe7Ey2Q/3q\n" + 29 | "UKS92nldF52f1AKcTHE3t4xdZyuuI9OuM+gmHjc3bbz85yU4osykXxEolxVVC/+B\n" + 30 | "AfjjXiD/jbMpTgFgDmzW3iMzOj2Ub32aJovb9guPPoTz4QIAy7UD59i6Jsng+LjK\n" + 31 | "kJ1G+4l5mWx/IO1dqFzIilSpO3N5XwDwiMwX7eaDYcaGh7JK6yX7CQVJ+AGE+Iis\n" + 32 | "m9kOMwIA2XxRnMLvniJQLdA7mPF8WKtXYK7tGSyQKQpvmTeeP5bJMy7ZFq+iiyAh\n" + 33 | "Y4VYTH7loQYu8YEqGeRjQQX5JkBEIQH9EGKkZroIacX1u3GJjjxD6l70hgH2ql7i\n" + 34 | "6mYdfDE+Ue3l7WwKqTQhSA6QkVwBgXCcB1k3bDyG9qB70ObPLW43np3WiJ8EGAEC\n" + 35 | "AAkFAlluLQMCGwwACgkQ0IVp55u0qmsrggQAuFVzKE7i9jIFzOpGyX2Kay8bGrPx\n" + 36 | "RUE7W8JFSmMFbhRo8AbOeyDPb+ndMwtrJM2lSn3D48wsM8U/06ubONJp/GJg+TVz\n" + 37 | "o/67hpUJ+aPL2e8SuNAnaEDtYFkvg0yJNLdqyExz8Db/bElGbn5jBirCenkC3ooc\n" + 38 | "rxH32RRqhjMh/JU=\n" + 39 | "=9e5q\n" + 40 | "-----END PGP public KEY BLOCK-----\n"; 41 | 42 | public static final String QUEDEX_PUBLIC = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + 43 | "Version: GnuPG v1\n" + 44 | "\n" + 45 | "mI0EWW4tAwEEAM/SG6Jss01ws1FxDvBO0JufZLwrSrdXVzatGtxMYwWagor5UPnO\n" + 46 | "j9txsC1+b/o75zGb4KPa2grVuwF0XkSngLbwweSlvIkiyvAL8sonnyTT+IwsX8Kc\n" + 47 | "UOKfr+GyNTYTssooLu7P/gXosoXJszyCiaJQVILmyZp6J+Ph+8DOQW/dABEBAAG0\n" + 48 | "CFRlc3QgS2V5iLgEEwECACIFAlluLQMCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B\n" + 49 | "AheAAAoJENCFaeebtKprfFwEALoYRx8+oKTkV8Zl1HcRD2tVJc9puR256XQfTv56\n" + 50 | "hlpRlSgeQBgRL58kBzd9/eshtX98cMJLfjsNQLteYPeowVhoG1RgNN4FIZlpxLbE\n" + 51 | "fsDL7ecy6798861rJ4CjBvPJAQXTQgoBKtt4qCIcRNrql4hIRbbzypPnEkUrZ+Ks\n" + 52 | "tUNluI0EWW4tAwEEAK0PXO5uq1qwPyJZyoNugn2TESVNuxADkT/FBW8fELrSrVaW\n" + 53 | "8XCNFpGMxPGtGlWQEXPErrZFibfoptMbRB+tFCEHfqgLQURyQtX3o5B/x5UoHscy\n" + 54 | "dIJlvPmaB/JTcj/8Gl+D2vsUvefpeIVZlr3mVJDe2T6iOhnz5Rn5dJ/dgGCTABEB\n" + 55 | "AAGInwQYAQIACQUCWW4tAwIbDAAKCRDQhWnnm7SqayuCBAC4VXMoTuL2MgXM6kbJ\n" + 56 | "fYprLxsas/FFQTtbwkVKYwVuFGjwBs57IM9v6d0zC2skzaVKfcPjzCwzxT/Tq5s4\n" + 57 | "0mn8YmD5NXOj/ruGlQn5o8vZ7xK40CdoQO1gWS+DTIk0t2rITHPwNv9sSUZufmMG\n" + 58 | "KsJ6eQLeihyvEffZFGqGMyH8lQ==\n" + 59 | "=TPlM\n" + 60 | "-----END PGP PUBLIC KEY BLOCK-----"; 61 | 62 | public static final String TRADER_PRIVATE = "-----BEGIN PGP public KEY BLOCK-----\n" + 63 | "Version: GnuPG v1\n" + 64 | "\n" + 65 | "lQHYBFluLcABBADzVTqi51bbFlKyGybGzTkumKn9mNhaJzJzfnmeOBdCc54gW/pd\n" + 66 | "WnQDlQEchUZj3HR5zaRB8VGYxOjVAvGAGRLzgAcKE5GG2CxRsuFz3Ldc8tvInpG1\n" + 67 | "V81Z75yfpKDMtUPHMsH2GffGl3pbJWTVzxFUvuf/9J9h3XYOWzERODSUxQARAQAB\n" + 68 | "AAP+IbLH8A+Bo82vniLMd/Or25wgzpIARFvxTLVTOmoiLn28hFa4kX0ZW/WitcRv\n" + 69 | "Px0ktEmaWdeFqVZ7uCQ0Nb1DNlh+Evg470BcLfpAF9eTgx+DdurD2MkOiQX1PwKW\n" + 70 | "D2BDsq8Jyc1Ti3gHkrxCtVnxncnLOdBgOm+HOy4wXnv91nECAPaItX17gzmJbbbt\n" + 71 | "iFn94Nf4vzwKq/no9jm6a3xqch7vN0be8hWbplxGTOWvAZ+hTok/jnLgQ5KypZ8b\n" + 72 | "ZNtWgG8CAPytDh5hnys7ehX0/TvGm/L8lFKKi/4fc334oOzWDwqt1iJeMmnAZ6lY\n" + 73 | "+/nUsouILSafp3LLycr/lM8qMD678AsCAKzMlR+q5eqn6OXQSSs4dWHcn+HQD7rJ\n" + 74 | "uCwF9jGUlxoAUmeaygO+Mwh/Twrnp3MlVmOHoPYcKKRapIY+Gp/rRxekl7QIVGVz\n" + 75 | "dCBLZXmIuAQTAQIAIgUCWW4twAIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AA\n" + 76 | "CgkQCVkAznkaitL13wP/U247yHUJw2n3JsvH8vLN8Y2+6Ni7Fy3IEqTL228ISHBU\n" + 77 | "UfVbGXotOsP9xLdFAMgW0iE358+vSU5Kss0l5dbXPPIO+PfJ9fUBL3mpst6Kpp8m\n" + 78 | "z4ngLxF1tJv2gul51dUU/3RITDQyqwwYSxCW1uTNuAGQJ3N5NiVeyfzH8GywghSd\n" + 79 | "AdgEWW4twAEEAJ+660sVhrU3zvmsSIx35BhpcWy9GfhgkUKPSkWLVus9ZiS+TVKs\n" + 80 | "ZTHTuoS4KcSfsqqnW/DBt6w7YCepdd0Sf4PFF7wgm9+X++F8kQQhxBvFDwx1BMoh\n" + 81 | "rJH80CnFbbxA39Vf4IUo7mVxajUMZ3O06gAkuZRzpHHr8V8sOMQK3mtNABEBAAEA\n" + 82 | "A/sFqgc1/mlse7InQGjCMm4wP2z6QiptmF8OUS59ENfgN6krnGP5jot4HN38Xtt8\n" + 83 | "UX1wd8bW3se4n9JlFalMUZ+b6kWFZqZto0Ge1VHOwDYEdRmLlWO7z+QUN1FlANjo\n" + 84 | "oqB4MuNzau5jgLUyId352t8oar3EldBUmFci+JMaiaZMsQIAx4Y6+WJ9dU/BKrWC\n" + 85 | "L/rJ2FV9RZ6RmbJ8NMNr7jVojoJzPnxzQa8c0s1SRu+3ccIOH2Jhhgdgnk5WmWg5\n" + 86 | "xCttxwIAzPEnsEaR+jeSjpek+MBvGlQkO18J3ejjngN0AqF4wovHGmEsE/3NyUam\n" + 87 | "80e9DNknIGsZ2+NNJ1UeouOVpjiuSwIAuOlFox2I5k4KJEQpIFrR7YKeNh8SLgQD\n" + 88 | "tOpsWlZ6naePgkD31Y0dQ28mvaAiTlMdGCpOdxhcgiw8MrRClcZv45sRiJ8EGAEC\n" + 89 | "AAkFAlluLcACGwwACgkQCVkAznkaitLEtAP/YNt1fAPF6ieX1U6KM1RlMvuqa08q\n" + 90 | "Pkg7z5GRti34Czq8HdNnYXfiN7dpikT0CwUV9v7xw7BtlENxv9rF/FMFDjh8yWdr\n" + 91 | "YwpCofOLRmMHxgTCpFRP6L3N/fmJaLUVmXy+gZrjVnglhRB+G5q8wemUusvCygSa\n" + 92 | "OGGHLWrBcvtjoq4=\n" + 93 | "=AE6u\n" + 94 | "-----END PGP public KEY BLOCK-----"; 95 | 96 | public static final String TRADER_PUBLIC = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + 97 | "Version: GnuPG v1\n" + 98 | "\n" + 99 | "mI0EWW4twAEEAPNVOqLnVtsWUrIbJsbNOS6Yqf2Y2FonMnN+eZ44F0JzniBb+l1a\n" + 100 | "dAOVARyFRmPcdHnNpEHxUZjE6NUC8YAZEvOABwoTkYbYLFGy4XPct1zy28iekbVX\n" + 101 | "zVnvnJ+koMy1Q8cywfYZ98aXelslZNXPEVS+5//0n2Hddg5bMRE4NJTFABEBAAG0\n" + 102 | "CFRlc3QgS2V5iLgEEwECACIFAlluLcACGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B\n" + 103 | "AheAAAoJEAlZAM55GorS9d8D/1NuO8h1CcNp9ybLx/LyzfGNvujYuxctyBKky9tv\n" + 104 | "CEhwVFH1Wxl6LTrD/cS3RQDIFtIhN+fPr0lOSrLNJeXW1zzyDvj3yfX1AS95qbLe\n" + 105 | "iqafJs+J4C8RdbSb9oLpedXVFP90SEw0MqsMGEsQltbkzbgBkCdzeTYlXsn8x/Bs\n" + 106 | "sIIUuI0EWW4twAEEAJ+660sVhrU3zvmsSIx35BhpcWy9GfhgkUKPSkWLVus9ZiS+\n" + 107 | "TVKsZTHTuoS4KcSfsqqnW/DBt6w7YCepdd0Sf4PFF7wgm9+X++F8kQQhxBvFDwx1\n" + 108 | "BMohrJH80CnFbbxA39Vf4IUo7mVxajUMZ3O06gAkuZRzpHHr8V8sOMQK3mtNABEB\n" + 109 | "AAGInwQYAQIACQUCWW4twAIbDAAKCRAJWQDOeRqK0sS0A/9g23V8A8XqJ5fVTooz\n" + 110 | "VGUy+6prTyo+SDvPkZG2LfgLOrwd02dhd+I3t2mKRPQLBRX2/vHDsG2UQ3G/2sX8\n" + 111 | "UwUOOHzJZ2tjCkKh84tGYwfGBMKkVE/ovc39+YlotRWZfL6BmuNWeCWFEH4bmrzB\n" + 112 | "6ZS6y8LKBJo4YYctasFy+2Oirg==\n" + 113 | "=zdwG\n" + 114 | "-----END PGP PUBLIC KEY BLOCK-----"; 115 | } 116 | -------------------------------------------------------------------------------- /src/test/java/net/quedex/api/testcommons/Utils.java: -------------------------------------------------------------------------------- 1 | package net.quedex.api.testcommons; 2 | 3 | import com.google.common.io.Resources; 4 | 5 | import java.io.IOException; 6 | import java.math.BigDecimal; 7 | import java.util.Properties; 8 | 9 | public class Utils { 10 | private Utils() {} 11 | 12 | public static char[] getKeyPassphraseFromProps() { 13 | Properties props = new Properties(); 14 | try { 15 | props.load(Resources.getResource("qdxConfig.properties").openStream()); 16 | } catch (IOException e) { 17 | throw new IllegalStateException("Error reading properties file", e); 18 | } 19 | 20 | return props.getProperty("dont.do.it.in.production.privateKeyPasspharse").toCharArray(); 21 | } 22 | 23 | public static BigDecimal $(String price) { 24 | return new BigDecimal(price); 25 | } 26 | 27 | public static BigDecimal $(int price) { 28 | return BigDecimal.valueOf(price); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %date [%thread] %-5level %logger - %message%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/test/resources/qdxConfig.properties.example: -------------------------------------------------------------------------------- 1 | net.quedex.client.api.marketStreamUrl = wss://api.quedex.net 2 | net.quedex.client.api.userStreamUrl = wss://api.quedex.net 3 | 4 | net.quedex.client.api.accountId = 123456789 5 | net.quedex.client.api.nonceGroup = 5 6 | 7 | net.quedex.client.api.qdxPublicKey = -----BEGIN PGP PUBLIC KEY BLOCK-----\n\ 8 | Version: QPG\n\ 9 | \n\ 10 | mQENBFlPvjsBCACr/UfHzXAezskLqcq9NiiaNFDDT5A+biC8VrOglB0ZSQOYRira\n\ 11 | NgQ2Cp8Jd+XU77F+J1012BjB5y87Z+hdnwBDsqF7CjkjeQzsE3PSvm9I+E3cneqx\n\ 12 | UcinRaUD1wfwVytbg9Q9rpqQ7CTjVWY1UPYjs6dAo1WAp/ux/VTeOFbpO0R3D7if\n\ 13 | ZGY1QeISRpLWiMpcG2YCOALnuazABVCNXLhVqa8Y7tt2I+cI0uE9tBf41gjGPPtd\n\ 14 | KdASPVz1plpOEl2dOpmy8jICqcSzUsT4Sy8vAqW3U1HF+TA2QGRcrrUItL4GjxNL\n\ 15 | lcL8wh7mclsjRe5Q5dYnrACC9NWS6vSp/eAPABEBAAG0G1F1ZWRleCA8Y29udGFj\n\ 16 | dEBxdWVkZXgubmV0PokBOAQTAQIAIgUCWU++OwIbAwYLCQgHAwIGFQgCCQoLBBYC\n\ 17 | AwECHgECF4AACgkQzsLQUmv6vk9Rlwf+LiJA37dhDdGFU/fexNRHZWTUh2TdqBsv\n\ 18 | MiNtarf+HlZIioWMCzlHmb3bolVrfFUNUh/GGlPENtlaSmFGuPhMlFcNDGYM+I7k\n\ 19 | ufhM95jxmtzy97NYMeMx4xjnaBAu8kFsvi80BR/05ZhCHqyI3K9NpYoXBfsyzss+\n\ 20 | j/jX1NHayzMmXNdqQ5JjzuICZj0EY9ryLP/jPAZ6DS9LVwi9Vr2JzZheCx5Q77Ud\n\ 21 | HuGTOBu3Azor2f4n4ccELs7lgU7uGrt1cK/oiML9UDmqjelunzTFU/5Q0tp7C3Qm\n\ 22 | 1wymd+PYTvvX/5htnLar1nIuYmmvtCZb1zyuzPzJWWtCcFFsiV9kerkBDQRZT747\n\ 23 | AQgAn/9rwQn4yM7pVYO9rTxNveI60lgp4RLKi1WYpAy6gxwUp3zLE/h59rykb7qM\n\ 24 | 9QArABsMEGGKt9urq7hjsKu8bM+hVTcAuoDre5qNFEfhZuPBS9LF7wQWDikORZxR\n\ 25 | Mk5WIiCt3U2soQ4Lismw1bLDX8uqkv3GFtR+IaKzuwYBEVPwuZ15EOt9G83JR3uV\n\ 26 | MKqeUtFW9+z5WEAh2JLU6C357sftJIJeWDEgF2TPtQOzc8isI8rpIFNyl6x1Aiy6\n\ 27 | LaSWmOI3d9EQ8SH4LxCXtAgWvnIoPL0JsP5/FWzt6qJR4teu+A2xwG7001va+DUc\n\ 28 | 34AbSV9Ogqa519OfbKK6HDyFIQARAQABiQEfBBgBAgAJBQJZT747AhsMAAoJEM7C\n\ 29 | 0FJr+r5PtEUH/0KmXQWbm4qXxOnaXrk+CKLDBxtfY6BaoJ6ekdGfqVMd8YM+UGnL\n\ 30 | 6d49vex4O80uIhIDSex446gKVlhdwOjIlUFmTCtMgGOa06G2T4sx8a9y2UYK45hN\n\ 31 | rj9aVfhJ8nn9yuPj7cBNtLEYJ4VkRKxJO9XX8cfhUsomhB3DQDbOLfikYqfmupm6\n\ 32 | mYX84CO/DD8JAXx4qt9Rg+5AUQegq26iZ/og1ZjYZ/tvBjrc45u23XCWvgVHbGhb\n\ 33 | wWCNjZijaY1VnTwTe6uZv1AqovZpprqZKWImN5myaJI3AJU2W2FCbI0ezfoVEVO4\n\ 34 | zMipOYZzRziJeCz1gX9geNseLvfJ8EtZRKU=\n\ 35 | =e4C9\n\ 36 | -----END PGP PUBLIC KEY BLOCK----- 37 | 38 | net.quedex.client.api.userPrivateKey = --------------------------------------------------------------------------------