├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── exchange │ │ └── core2 │ │ └── rest │ │ ├── CommandEventsRouter.java │ │ ├── Config.java │ │ ├── GatewayState.java │ │ ├── RestGatewayApplication.java │ │ ├── WebSocketConfig.java │ │ ├── commands │ │ ├── ApiErrorCodes.java │ │ ├── HelloMessage.java │ │ ├── RestApiCancelOrder.java │ │ ├── RestApiMoveOrder.java │ │ ├── RestApiPlaceOrder.java │ │ ├── admin │ │ │ ├── RestApiAccountBalanceAdjustment.java │ │ │ ├── RestApiAddSymbol.java │ │ │ ├── RestApiAddUser.java │ │ │ ├── RestApiAdminAsset.java │ │ │ ├── RestApiRequestMarketData.java │ │ │ └── StompApiNotificationMessage.java │ │ └── util │ │ │ └── ArithmeticHelper.java │ │ ├── controllers │ │ ├── GreetingController.java │ │ ├── RestControllerHelper.java │ │ ├── SyncAdminApiAccountsController.java │ │ ├── SyncAdminApiSymbolsController.java │ │ ├── SyncTradeAccountApiController.java │ │ ├── SyncTradeChartsApiController.java │ │ ├── SyncTradeMiscApiController.java │ │ └── SyncTradeOrdersApiController.java │ │ ├── events │ │ ├── MatchingRole.java │ │ ├── NewTradeRecord.java │ │ ├── OrderBookEvent.java │ │ ├── OrderSizeChangeRecord.java │ │ ├── OrderUpdateEvent.java │ │ ├── ReduceRecord.java │ │ ├── RejectionRecord.java │ │ ├── RestGenericResponse.java │ │ └── admin │ │ │ ├── SymbolUpdateAdminEvent.java │ │ │ ├── UserBalanceAdjustmentAdminEvent.java │ │ │ └── UserCreatedAdminEvent.java │ │ └── model │ │ ├── api │ │ ├── GatewayOrderState.java │ │ ├── RestApiAccountState.java │ │ ├── RestApiAccountTradeHistory.java │ │ ├── RestApiAsset.java │ │ ├── RestApiBar.java │ │ ├── RestApiDeal.java │ │ ├── RestApiExchangeInfo.java │ │ ├── RestApiOrder.java │ │ ├── RestApiOrderBook.java │ │ ├── RestApiSymbol.java │ │ ├── RestApiTime.java │ │ ├── RestApiUserState.java │ │ ├── RestApiUserTradesHistory.java │ │ ├── StompAccountUpdate.java │ │ ├── StompApiTick.java │ │ ├── StompOrderUpdate.java │ │ └── TimeFrame.java │ │ └── internal │ │ ├── BarsData.java │ │ ├── ChartData.java │ │ ├── GatewayAssetSpec.java │ │ ├── GatewayBarLast.java │ │ ├── GatewayBarStatic.java │ │ ├── GatewayDeal.java │ │ ├── GatewayOrder.java │ │ ├── GatewaySymbolSpec.java │ │ ├── GatewayUserProfile.java │ │ └── TickRecord.java └── resources │ ├── application.properties │ ├── db │ └── migration │ │ └── V0000__initial_schema.sql │ ├── index.html │ ├── javascript │ ├── application.js │ ├── atmosphere.js │ └── jquery-2.0.3.js │ ├── logback.xml │ └── static │ ├── app.js │ ├── index.html │ └── main.css └── test ├── java └── exchange │ └── core2 │ └── rest │ ├── ITExchangeGatewayHttp.java │ ├── TestConfiguraton.java │ └── support │ ├── StompTestClient.java │ ├── TestService.java │ └── TestSupport.java └── resources └── it-local.properties /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .mvn/ 3 | 4 | ### IntelliJ IDEA ### 5 | .idea 6 | *.iws 7 | *.iml 8 | *.ipr 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | sudo: false 5 | script: mvn clean verify 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # exchange-gateway-rest -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | exchange.core2 7 | exchange-gateway-rest 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | Exchange REST Gateway 12 | Java Exchange Market Gateway 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.7.0 18 | 19 | 20 | 21 | 22 | false 23 | 24 | UTF-8 25 | UTF-8 26 | 1.8 27 | 28 | 25.0-jre 29 | 3.6.1 30 | 3.7 31 | 32 | 3.1.0 33 | 34 | 9.2.0 35 | 36 | 37 | 38 | 39 | 40 | 41 | com.google.guava 42 | guava 43 | ${guava.version} 44 | 45 | 46 | 47 | org.apache.commons 48 | commons-lang3 49 | ${commons-lang3.version} 50 | 51 | 52 | 53 | org.apache.commons 54 | commons-math3 55 | ${commons-math3.version} 56 | 57 | 58 | 59 | 60 | org.eclipse.collections 61 | eclipse-collections-api 62 | 9.2.0 63 | 64 | 65 | org.eclipse.collections 66 | eclipse-collections 67 | 9.2.0 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | exchange.core2 76 | exchange-core 77 | 0.5.5-SNAPSHOT 78 | 79 | 80 | 81 | 82 | org.springframework.boot 83 | spring-boot-starter-websocket 84 | 85 | 86 | 87 | org.webjars 88 | webjars-locator-core 89 | 90 | 91 | org.webjars 92 | sockjs-client 93 | 1.0.2 94 | 95 | 96 | org.webjars 97 | stomp-websocket 98 | 2.3.3 99 | 100 | 101 | org.webjars 102 | bootstrap 103 | 3.3.7 104 | 105 | 106 | org.webjars 107 | jquery 108 | 3.1.0 109 | 110 | 111 | 112 | com.fasterxml.jackson.datatype 113 | jackson-datatype-jsr310 114 | 115 | 116 | 117 | org.projectlombok 118 | lombok 119 | 1.18.24 120 | 121 | 122 | 123 | org.slf4j 124 | slf4j-api 125 | 126 | 127 | 128 | com.google.guava 129 | guava 130 | 131 | 132 | org.apache.commons 133 | commons-lang3 134 | 135 | 136 | org.springframework.boot 137 | spring-boot-configuration-processor 138 | 139 | 140 | org.springframework.boot 141 | spring-boot-starter 142 | 143 | 144 | 145 | 146 | org.hibernate.javax.persistence 147 | hibernate-jpa-2.1-api 148 | 1.0.0.Final 149 | 150 | 151 | org.springframework.boot 152 | spring-boot-starter-jdbc 153 | 154 | 155 | org.apache.tomcat 156 | tomcat-jdbc 157 | 158 | 159 | 160 | 161 | org.springframework.boot 162 | spring-boot-starter-data-jpa 163 | 164 | 165 | org.postgresql 166 | postgresql 167 | runtime 168 | 169 | 170 | org.hibernate 171 | hibernate-java8 172 | 173 | 174 | org.flywaydb 175 | flyway-core 176 | 177 | 178 | com.zaxxer 179 | HikariCP 180 | ${HikariCP.version} 181 | 182 | 183 | 184 | 185 | org.eclipse.collections 186 | eclipse-collections-api 187 | 188 | 189 | org.eclipse.collections 190 | eclipse-collections 191 | 192 | 193 | 194 | 195 | junit 196 | junit 197 | test 198 | 199 | 200 | org.mockito 201 | mockito-core 202 | test 203 | 204 | 205 | org.springframework.boot 206 | spring-boot-starter-test 207 | test 208 | 209 | 210 | 211 | 212 | 213 | 214 | default 215 | 216 | true 217 | 218 | 219 | 220 | 221 | org.apache.maven.plugins 222 | maven-surefire-plugin 223 | 224 | 225 | 226 | 227 | 228 | 229 | it 230 | 231 | 232 | 233 | org.apache.maven.plugins 234 | maven-surefire-plugin 235 | 236 | 237 | true 238 | 239 | **/IT*.java 240 | **/*IntegrationTest.java 241 | 242 | 243 | **/remote/**/*.class 244 | **/stress/**/*.class 245 | **/*Remote*Test.java 246 | **/*Stress*Test.java 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/CommandEventsRouter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest; 17 | 18 | import exchange.core2.core.IEventsHandler; 19 | import exchange.core2.core.IEventsHandler.RejectEvent; 20 | import exchange.core2.core.common.L2MarketData; 21 | import exchange.core2.core.common.MatcherEventType; 22 | import exchange.core2.core.common.MatcherTradeEvent; 23 | import exchange.core2.core.common.cmd.CommandResultCode; 24 | import exchange.core2.core.common.cmd.OrderCommand; 25 | import exchange.core2.core.common.cmd.OrderCommandType; 26 | import exchange.core2.rest.commands.util.ArithmeticHelper; 27 | import exchange.core2.rest.events.MatchingRole; 28 | import exchange.core2.rest.events.NewTradeRecord; 29 | import exchange.core2.rest.events.OrderBookEvent; 30 | import exchange.core2.rest.events.OrderSizeChangeRecord; 31 | import exchange.core2.rest.events.OrderUpdateEvent; 32 | import exchange.core2.rest.events.ReduceRecord; 33 | import exchange.core2.rest.events.RejectionRecord; 34 | import exchange.core2.rest.events.admin.UserBalanceAdjustmentAdminEvent; 35 | import exchange.core2.rest.events.admin.UserCreatedAdminEvent; 36 | import exchange.core2.rest.model.api.StompApiTick; 37 | import exchange.core2.rest.model.api.StompOrderUpdate; 38 | import exchange.core2.rest.model.internal.GatewayOrder; 39 | import exchange.core2.rest.model.internal.GatewaySymbolSpec; 40 | import exchange.core2.rest.model.internal.GatewayUserProfile; 41 | import exchange.core2.rest.model.internal.TickRecord; 42 | import java.math.BigDecimal; 43 | import java.util.ArrayList; 44 | import java.util.List; 45 | import java.util.function.ObjLongConsumer; 46 | import lombok.extern.slf4j.Slf4j; 47 | import org.agrona.collections.MutableReference; 48 | import org.springframework.beans.factory.annotation.Autowired; 49 | import org.springframework.messaging.simp.SimpMessagingTemplate; 50 | import org.springframework.stereotype.Service; 51 | 52 | 53 | @Service 54 | @Slf4j 55 | public class CommandEventsRouter implements ObjLongConsumer { 56 | 57 | @Autowired 58 | private GatewayState gatewayState; 59 | 60 | @Autowired 61 | private SimpMessagingTemplate simpMessagingTemplate; 62 | // 63 | // @Autowired 64 | // private WebSocketServer webSocketServer; 65 | 66 | public static final String STOMP_TOPIC_TICKS_PREFIX = "/topic/ticks/"; 67 | public static final String STOMP_TOPIC_ORDER_PREFIX = "/topic/orders/"; 68 | 69 | /** 70 | * TODO put non-latency-critical commands into a queue 71 | * 72 | * @param cmd command placeholder 73 | */ 74 | @Override 75 | public void accept(OrderCommand cmd, long seq) { 76 | log.debug("seq={} EVENT CMD: {}", seq, cmd); 77 | 78 | // processData(seq, cmd); 79 | 80 | // final CommandResultCode resultCode = cmd.resultCode; 81 | // final int ticket = cmd.userCookie; 82 | // 83 | // 84 | // if (resp == null) { 85 | // log.error("can not find resp #{}", ticket); 86 | // return; 87 | // } 88 | // 89 | // Object data = (resultCode == CommandResultCode.SUCCESS) 90 | // ? processData(cmd) 91 | // : null; 92 | // 93 | // RestGenericResponse response = RestGenericResponse.builder() 94 | // .ticket(ticket) 95 | // .coreResultCode(resultCode.getCode()) 96 | // .data(data) 97 | // .build(); 98 | // 99 | // resp.json(response).done(); 100 | 101 | // processing events in original order 102 | 103 | // TODO 104 | 105 | if (cmd.command == OrderCommandType.BINARY_DATA_COMMAND || cmd.command == OrderCommandType.BINARY_DATA_QUERY) { 106 | // ignore binary commands further 107 | return; 108 | } 109 | 110 | final GatewaySymbolSpec symbolSpec = gatewayState.getSymbolSpec(cmd.symbol); 111 | 112 | // activate order (dont send update because placing is sync) 113 | if (cmd.command == OrderCommandType.PLACE_ORDER && cmd.resultCode == CommandResultCode.SUCCESS) { 114 | final GatewayUserProfile userProfile = gatewayState.getOrCreateUserProfile(cmd.uid); 115 | userProfile.activateOrder(cmd.orderId); 116 | } 117 | 118 | // update order price 119 | if (cmd.command == OrderCommandType.MOVE_ORDER && cmd.resultCode == CommandResultCode.SUCCESS) { 120 | final GatewayUserProfile userProfile = gatewayState.getOrCreateUserProfile(cmd.uid); 121 | userProfile.updateOrderPrice( 122 | cmd.orderId, 123 | ArithmeticHelper.fromLongPrice(cmd.price, symbolSpec), 124 | order -> sendOrderUpdate(cmd.uid, order)); 125 | } 126 | 127 | MatcherTradeEvent firstEvent = cmd.matcherEvent; 128 | if (firstEvent == null) { 129 | return; 130 | } 131 | if (firstEvent.eventType == MatcherEventType.REDUCE) { 132 | if (firstEvent.nextEvent != null) { 133 | throw new IllegalStateException("Only single REDUCE event is expected"); 134 | } 135 | final GatewayUserProfile profile = gatewayState.getOrCreateUserProfile(cmd.uid); 136 | profile.cancelOrder(cmd.orderId, order -> sendOrderUpdate(cmd.uid, order)); 137 | return; 138 | } 139 | 140 | final MutableReference rejectEvent = new MutableReference<>(null); 141 | List ticks = new ArrayList<>(); 142 | cmd.processMatcherEvents(evt -> { 143 | log.debug("INTERNAL EVENT: " + evt); 144 | if (evt.eventType == MatcherEventType.TRADE) { 145 | 146 | // resolve trade price 147 | final BigDecimal tradePrice = ArithmeticHelper.fromLongPrice(evt.price, symbolSpec); 148 | 149 | // update taker's profile 150 | final GatewayUserProfile takerProfile = gatewayState.getOrCreateUserProfile(cmd.uid); 151 | takerProfile.tradeOrder( 152 | cmd.orderId, 153 | evt.size, 154 | tradePrice, 155 | MatchingRole.TAKER, 156 | cmd.timestamp, 157 | evt.matchedOrderId, 158 | evt.matchedOrderUid, 159 | order -> sendOrderUpdate(cmd.uid, order)); 160 | 161 | // update maker's profile 162 | final GatewayUserProfile makerProfile = gatewayState.getOrCreateUserProfile(evt.matchedOrderUid); 163 | makerProfile.tradeOrder( 164 | evt.matchedOrderId, 165 | evt.size, 166 | tradePrice, 167 | MatchingRole.MAKER, 168 | cmd.timestamp, 169 | cmd.orderId, 170 | cmd.uid, 171 | order -> sendOrderUpdate(evt.matchedOrderUid, order)); 172 | 173 | // todo aggregate ticks having same price 174 | ticks.add(new TickRecord(tradePrice, evt.size, cmd.timestamp, cmd.action)); 175 | 176 | } else if (evt.eventType == MatcherEventType.REJECT) { 177 | rejectEvent.set(new IEventsHandler.RejectEvent( 178 | cmd.symbol, 179 | evt.size, 180 | evt.price, 181 | cmd.orderId, 182 | cmd.uid, 183 | cmd.timestamp)); 184 | } else if (evt.eventType == MatcherEventType.REDUCE) { 185 | //todo reduce 186 | } 187 | }); 188 | 189 | if (rejectEvent.ref != null) { 190 | final GatewayUserProfile profile = gatewayState.getOrCreateUserProfile(cmd.uid); 191 | profile.rejectOrder(rejectEvent, order -> sendOrderUpdate(cmd.uid, order)); 192 | } 193 | 194 | if (!ticks.isEmpty()) { 195 | gatewayState.addTicks(symbolSpec.symbolCode, ticks); 196 | 197 | ticks.forEach(tick -> { 198 | final StompApiTick apiTick = new StompApiTick(tick.getPrice(), tick.getSize(), tick.getTimestamp()); 199 | simpMessagingTemplate.convertAndSend(STOMP_TOPIC_TICKS_PREFIX + symbolSpec.symbolCode, apiTick); 200 | log.debug("#### Sent tick {} {}", STOMP_TOPIC_TICKS_PREFIX + symbolSpec.symbolCode, apiTick); 201 | // simpMessagingTemplate.convertAndSend("abc", "symbol123"); 202 | 203 | }); 204 | } 205 | 206 | } 207 | 208 | private void processData(long seq, OrderCommand cmd) { 209 | switch (cmd.command) { 210 | 211 | case PLACE_ORDER: 212 | case MOVE_ORDER: 213 | case CANCEL_ORDER: 214 | handleOrderCommand(cmd); 215 | break; 216 | 217 | case ORDER_BOOK_REQUEST: 218 | handleOrderBookCommand(cmd); 219 | break; 220 | 221 | case BALANCE_ADJUSTMENT: 222 | balanceAdjustment(cmd); 223 | break; 224 | 225 | case ADD_USER: 226 | handleAddUser(cmd); 227 | break; 228 | } 229 | } 230 | 231 | private OrderUpdateEvent handleOrderCommand(OrderCommand cmd) { 232 | List tradeRecords = new ArrayList<>(); 233 | 234 | // TODO implement remaining size 235 | 236 | cmd.processMatcherEvents(evt -> { 237 | if (evt.eventType == MatcherEventType.TRADE) { 238 | MatchingRole role = evt.matchedOrderId == cmd.orderId ? MatchingRole.MAKER : MatchingRole.TAKER; 239 | tradeRecords.add(NewTradeRecord.builder().filledSize(evt.size).fillPrice(evt.price).matchingRole(role).build()); 240 | 241 | } else if (evt.eventType == MatcherEventType.REDUCE) { 242 | 243 | tradeRecords.add(ReduceRecord.builder().reducedSize(evt.size).build()); 244 | 245 | } else if (evt.eventType == MatcherEventType.REJECT) { 246 | 247 | tradeRecords.add(RejectionRecord.builder().rejectedSize(evt.size).build()); 248 | 249 | } else { 250 | 251 | throw new UnsupportedOperationException("unknown event type"); 252 | } 253 | }); 254 | 255 | long activeSize = cmd.size - tradeRecords.stream().mapToLong(OrderSizeChangeRecord::getAffectedSize).sum(); 256 | return OrderUpdateEvent.builder().price(cmd.price).orderId(cmd.orderId).activeSize(activeSize).trades(tradeRecords).build(); 257 | } 258 | 259 | private UserBalanceAdjustmentAdminEvent balanceAdjustment(OrderCommand cmd) { 260 | UserBalanceAdjustmentAdminEvent apiEvent = UserBalanceAdjustmentAdminEvent.builder() 261 | .uid(cmd.uid) 262 | .transactionId(cmd.orderId) 263 | .amount(cmd.price) 264 | .balance(cmd.size) 265 | .build(); 266 | //webSocketServer.broadcast(apiEvent); 267 | return apiEvent; 268 | } 269 | 270 | 271 | private OrderBookEvent handleOrderBookCommand(OrderCommand cmd) { 272 | 273 | if (cmd.marketData == null) { 274 | log.error("No market data object found"); 275 | //future.response().code(500).done(); 276 | //resp.chunk("{error:FAILED".getBytes()); 277 | return null; 278 | } 279 | 280 | log.debug("MARKET DATA: " + cmd.marketData.copy()); 281 | 282 | L2MarketData marketData = cmd.marketData; 283 | OrderBookEvent orderBook = new OrderBookEvent( 284 | "UNKNOWN", 285 | marketData.timestamp, 286 | marketData.getAskPricesCopy(), 287 | marketData.getAskVolumesCopy(), 288 | marketData.getBidPricesCopy(), 289 | marketData.getBidVolumesCopy() 290 | ); 291 | 292 | //log.debug("req.isAsync()={} req.isDone()={}", req.isAsync(), req.isDone()); 293 | 294 | // try { 295 | // Thread.sleep(10000); 296 | // } catch (InterruptedException e) { 297 | // // 298 | // } 299 | 300 | //resp.json(orderBook).done(); 301 | 302 | // webSocketServer.broadcast(orderBook); 303 | 304 | return orderBook; 305 | } 306 | 307 | private UserCreatedAdminEvent handleAddUser(OrderCommand cmd) { 308 | UserCreatedAdminEvent apiEvent = UserCreatedAdminEvent.builder().uid(cmd.uid).build(); 309 | //webSocketServer.broadcast(apiEvent); 310 | return apiEvent; 311 | } 312 | 313 | private void sendOrderUpdate(long uid, GatewayOrder gatewayOrder) { 314 | 315 | final StompOrderUpdate orderUpdate = StompOrderUpdate.builder() 316 | .uid(uid) 317 | .orderId(gatewayOrder.getOrderId()) 318 | .price(gatewayOrder.getPrice()) 319 | .size(gatewayOrder.getSize()) 320 | .filled(gatewayOrder.getFilled()) 321 | .state(gatewayOrder.getState()) 322 | .userCookie(gatewayOrder.getUserCookie()) 323 | .action(gatewayOrder.getAction()) 324 | .orderType(gatewayOrder.getOrderType()) 325 | .symbol(gatewayOrder.getSymbol()) 326 | .build(); 327 | 328 | simpMessagingTemplate.convertAndSend(STOMP_TOPIC_ORDER_PREFIX + "uid/" + uid, orderUpdate); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/Config.java: -------------------------------------------------------------------------------- 1 | package exchange.core2.rest; 2 | 3 | import exchange.core2.core.ExchangeCore; 4 | import exchange.core2.core.common.config.ExchangeConfiguration; 5 | import exchange.core2.core.common.config.InitialStateConfiguration; 6 | import exchange.core2.core.common.config.LoggingConfiguration; 7 | import exchange.core2.core.common.config.OrdersProcessingConfiguration; 8 | import exchange.core2.core.common.config.PerformanceConfiguration; 9 | import exchange.core2.core.common.config.ReportsQueriesConfiguration; 10 | import exchange.core2.core.common.config.SerializationConfiguration; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | 15 | /** 16 | *

Created by qdd on 2022/5/26. 17 | */ 18 | @Configuration 19 | public class Config { 20 | 21 | @Autowired 22 | CommandEventsRouter eventsRouter; 23 | 24 | @Bean 25 | public ExchangeCore exchangeCore() { 26 | final ExchangeConfiguration exchangeConfiguration = ExchangeConfiguration.defaultBuilder() 27 | .initStateCfg(InitialStateConfiguration.CLEAN_TEST) 28 | .performanceCfg(PerformanceConfiguration.baseBuilder().build()) 29 | .reportsQueriesCfg(ReportsQueriesConfiguration.createStandardConfig()) 30 | .ordersProcessingCfg(OrdersProcessingConfiguration.DEFAULT) 31 | .loggingCfg(LoggingConfiguration.DEFAULT) 32 | .serializationCfg(SerializationConfiguration.DEFAULT) 33 | .build(); 34 | 35 | ExchangeCore core = ExchangeCore.builder() 36 | .resultsConsumer(eventsRouter) 37 | .exchangeConfiguration(exchangeConfiguration) 38 | .build(); 39 | 40 | core.startup(); 41 | 42 | return core; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/GatewayState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest; 17 | 18 | import exchange.core2.core.ExchangeCore; 19 | import exchange.core2.rest.model.api.TimeFrame; 20 | import exchange.core2.rest.model.internal.*; 21 | import lombok.extern.slf4j.Slf4j; 22 | import org.eclipse.collections.impl.map.mutable.ConcurrentHashMap; 23 | import org.springframework.beans.factory.annotation.Autowired; 24 | import org.springframework.stereotype.Service; 25 | 26 | import javax.annotation.PostConstruct; 27 | import javax.annotation.PreDestroy; 28 | import java.util.List; 29 | import java.util.Map; 30 | import java.util.Optional; 31 | import java.util.concurrent.atomic.AtomicInteger; 32 | import java.util.function.Function; 33 | import java.util.stream.Collectors; 34 | 35 | 36 | // TODO separate interfaces for admin and user 37 | @Service 38 | @Slf4j 39 | public class GatewayState { 40 | 41 | private final AtomicInteger syncRequestsSequence = new AtomicInteger(0); 42 | 43 | // promises cache (TODO can be changed to queue?) 44 | 45 | private final Map symbolsByCode = new ConcurrentHashMap<>(); 46 | private final Map symbolsById = new ConcurrentHashMap<>(); 47 | 48 | private final Map assetsByCode = new ConcurrentHashMap<>(); 49 | private final Map assetsById = new ConcurrentHashMap<>(); 50 | 51 | private final Map userProfiles = new ConcurrentHashMap<>(); 52 | 53 | private final Map charts = new ConcurrentHashMap<>(); 54 | 55 | // @Autowired 56 | // private ExchangeCore exchangeCore; 57 | 58 | public GatewaySymbolSpec getSymbolSpec(String symbolCode) { 59 | return symbolsByCode.get(symbolCode); 60 | } 61 | 62 | public GatewaySymbolSpec getSymbolSpec(int symbolId) { 63 | return symbolsById.get(symbolId); 64 | } 65 | 66 | public boolean registerNewSymbol(GatewaySymbolSpec spec) { 67 | if (symbolsById.putIfAbsent(spec.symbolId, spec) == null) { 68 | symbolsByCode.put(spec.symbolCode, spec); 69 | charts.put(spec.symbolCode, new ChartData()); 70 | return true; 71 | } 72 | return false; 73 | } 74 | 75 | public List getActiveAssets(Function mapper) { 76 | return assetsById.values().stream() 77 | .filter(a -> a.active) 78 | .map(mapper) 79 | .collect(Collectors.toList()); 80 | } 81 | 82 | public List getActiveSymbols(Function mapper) { 83 | return symbolsById.values().stream() 84 | .filter(s -> s.status == GatewaySymbolSpec.GatewaySymbolLifecycle.ACTIVE) 85 | .map(mapper) 86 | .collect(Collectors.toList()); 87 | } 88 | 89 | public boolean registerNewAsset(GatewayAssetSpec spec) { 90 | 91 | // TODO implement validation and lifesycle 92 | if (assetsByCode.putIfAbsent(spec.assetCode, spec) == null) { 93 | assetsById.put(spec.assetId, spec); 94 | return true; 95 | } 96 | return false; 97 | } 98 | 99 | public GatewayAssetSpec getAssetSpec(String assetCode) { 100 | return assetsByCode.get(assetCode); 101 | } 102 | 103 | public GatewayAssetSpec getAssetSpec(int assetId) { 104 | return assetsById.get(assetId); 105 | } 106 | 107 | public GatewaySymbolSpec activateSymbol(final int symbolId) { 108 | 109 | final GatewaySymbolSpec newSpec = symbolsById.compute(symbolId, (k, v) -> 110 | (v != null && v.status == GatewaySymbolSpec.GatewaySymbolLifecycle.NEW) 111 | ? v.withStatus(GatewaySymbolSpec.GatewaySymbolLifecycle.ACTIVE) 112 | : null); 113 | 114 | if (newSpec != null) { 115 | symbolsByCode.put(newSpec.symbolCode, newSpec); 116 | } 117 | 118 | return newSpec; 119 | } 120 | 121 | public Optional getUserProfile(long uid) { 122 | return Optional.ofNullable(userProfiles.get(uid)); 123 | } 124 | 125 | public GatewayUserProfile getOrCreateUserProfile(long uid) { 126 | return userProfiles.computeIfAbsent(uid, k -> new GatewayUserProfile()); 127 | } 128 | 129 | public void addTicks(String symbol, List records) { 130 | charts.compute(symbol, (s, chart) -> chart == null ? new ChartData() : chart).addTicks(records); 131 | } 132 | 133 | public Optional> getBars(String symbolCode, int barsNum, TimeFrame timeFrame) { 134 | return Optional.ofNullable(charts.get(symbolCode)).map(chartData -> chartData.getBarsData(barsNum, timeFrame)); 135 | } 136 | // 137 | // @PostConstruct 138 | // public void start() { 139 | // log.debug("START1"); 140 | // exchangeCore.startup(); 141 | // } 142 | // 143 | // @PreDestroy 144 | // public void stop() { 145 | // log.debug("STOP1"); 146 | // exchangeCore.shutdown(); 147 | // } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/RestGatewayApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest; 17 | 18 | import org.springframework.boot.SpringApplication; 19 | import org.springframework.boot.autoconfigure.SpringBootApplication; 20 | 21 | 22 | @SpringBootApplication 23 | public class RestGatewayApplication { 24 | public static void main(String[] args) { 25 | SpringApplication.run(RestGatewayApplication.class, args); 26 | } 27 | 28 | 29 | // @Bean 30 | // public Consumer resultsConsumer() { 31 | // return cmd -> { 32 | // System.out.println(">>>" + cmd); 33 | // }; 34 | // } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest; 17 | 18 | import org.springframework.context.annotation.Configuration; 19 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 20 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 21 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 22 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 23 | 24 | @Configuration 25 | @EnableWebSocketMessageBroker 26 | public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { 27 | 28 | @Override 29 | public void configureMessageBroker(MessageBrokerRegistry config) { 30 | config.enableSimpleBroker("/topic"); 31 | config.setApplicationDestinationPrefixes("/app"); 32 | } 33 | 34 | @Override 35 | public void registerStompEndpoints(StompEndpointRegistry registry) { 36 | registry.addEndpoint("/ticks-websocket").setAllowedOrigins("*").withSockJS(); 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/commands/ApiErrorCodes.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.commands; 17 | 18 | import lombok.AllArgsConstructor; 19 | 20 | @AllArgsConstructor 21 | public enum ApiErrorCodes { 22 | 23 | SYMBOL_ALREADY_EXISTS(1000, 400, "symbol already exists"), 24 | UNKNOWN_BASE_ASSET(1001, 400, "unknown base asset"), 25 | UNKNOWN_QUOTE_CURRENCY(1002, 400, "unknown quote currency"), 26 | ASSET_ALREADY_EXISTS(1003, 400, "asset already exists"), 27 | UNKNOWN_CURRENCY(1004, 400, "unknown currency"), 28 | PRECISION_IS_TOO_HIGH(1005, 400, "precision is too high, reduce precision"), 29 | UNKNOWN_SYMBOL(1006, 400, "unknown symbol"), 30 | UNKNOWN_SYMBOL_404(1007, 404, "symbol not found"), 31 | 32 | INVALID_CONFIGURATION(1008, 400, "invalid configuration: %s"), 33 | 34 | INVALID_PRICE(1009, 400, "invalid price"), 35 | UNKNOWN_USER_404(1010, 404, "unknown user"), 36 | ; 37 | 38 | public final int gatewayErrorCode; 39 | public final int httpStatus; 40 | public final String errorDescription; 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/commands/HelloMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.commands; 17 | 18 | 19 | public class HelloMessage { 20 | 21 | private String name; 22 | 23 | public HelloMessage() { 24 | } 25 | 26 | public HelloMessage(String name) { 27 | this.name = name; 28 | } 29 | 30 | public String getName() { 31 | return name; 32 | } 33 | 34 | public void setName(String name) { 35 | this.name = name; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/commands/RestApiCancelOrder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.commands; 17 | 18 | 19 | import com.fasterxml.jackson.annotation.JsonCreator; 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | import lombok.Getter; 22 | 23 | @Getter 24 | public final class RestApiCancelOrder { 25 | 26 | private final long orderId; 27 | 28 | private final String symbol; 29 | 30 | // TODO remove 31 | private final long uid; 32 | 33 | @JsonCreator 34 | public RestApiCancelOrder( 35 | @JsonProperty("orderId") long orderId, 36 | @JsonProperty("symbol") String symbol, 37 | @JsonProperty("uid") long uid) { 38 | 39 | this.orderId = orderId; 40 | this.symbol = symbol; 41 | this.uid = uid; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return "[CANCEL " + orderId + "]"; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/commands/RestApiMoveOrder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.commands; 17 | 18 | 19 | import com.fasterxml.jackson.annotation.JsonCreator; 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | import lombok.Getter; 22 | 23 | import java.math.BigDecimal; 24 | 25 | @Getter 26 | public final class RestApiMoveOrder { 27 | 28 | private final BigDecimal price; 29 | 30 | @JsonCreator 31 | public RestApiMoveOrder( 32 | @JsonProperty("price") BigDecimal price) { 33 | 34 | this.price = price; 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return "[MOVE " + price + "]"; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/commands/RestApiPlaceOrder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.commands; 17 | 18 | 19 | import com.fasterxml.jackson.annotation.JsonCreator; 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | import exchange.core2.core.common.OrderAction; 22 | import exchange.core2.core.common.OrderType; 23 | import lombok.Getter; 24 | 25 | import java.math.BigDecimal; 26 | 27 | @Getter 28 | public final class RestApiPlaceOrder { 29 | 30 | private final BigDecimal price; 31 | private final long size; // only integer sizes allowed 32 | 33 | private final int userCookie; 34 | private final OrderAction action; 35 | private final OrderType orderType; 36 | 37 | @JsonCreator 38 | public RestApiPlaceOrder( 39 | @JsonProperty("price") BigDecimal price, 40 | @JsonProperty("size") long size, 41 | @JsonProperty("userCookie") int userCookie, 42 | @JsonProperty("action") OrderAction action, 43 | @JsonProperty("orderType") OrderType orderType) { 44 | 45 | this.price = price; 46 | this.size = size; 47 | this.userCookie = userCookie; 48 | this.action = action; 49 | this.orderType = orderType; 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return "[ADD " + (action == OrderAction.ASK ? 'A' : 'B') + orderType 55 | + price + ":" + size + "]"; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/commands/admin/RestApiAccountBalanceAdjustment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.commands.admin; 17 | 18 | 19 | import com.fasterxml.jackson.annotation.JsonCreator; 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | import lombok.Getter; 22 | 23 | import java.math.BigDecimal; 24 | 25 | @Getter 26 | public final class RestApiAccountBalanceAdjustment { 27 | 28 | //public final long uid; 29 | public final long transactionId; 30 | public final BigDecimal amount; 31 | public final String currency; 32 | 33 | @JsonCreator 34 | public RestApiAccountBalanceAdjustment( 35 | //@JsonProperty("uid") long uid, 36 | @JsonProperty("transactionId") long transactionId, 37 | @JsonProperty("amount") BigDecimal amount, 38 | @JsonProperty("currency") String currency) { 39 | 40 | //this.uid = uid; 41 | this.transactionId = transactionId; 42 | this.amount = amount; 43 | this.currency = currency; 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | return "[BALANCE_ADJ " + amount + " " + currency + " transactionId:" + transactionId + "]"; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/commands/admin/RestApiAddSymbol.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.commands.admin; 17 | 18 | 19 | import com.fasterxml.jackson.annotation.JsonCreator; 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | import exchange.core2.core.common.SymbolType; 22 | import lombok.Builder; 23 | 24 | import java.math.BigDecimal; 25 | 26 | public final class RestApiAddSymbol { 27 | 28 | public final int symbolId; 29 | public final String symbolCode; 30 | 31 | public final SymbolType symbolType; 32 | 33 | public final String baseAsset; // base asset 34 | public final String quoteCurrency; // quote/counter currency (OR futures contract currency) 35 | public final BigDecimal lotSize; // lot size in base asset units 36 | public final BigDecimal stepSize; // step size in quote currency units 37 | 38 | public final BigDecimal takerFee; // TODO check invariant: taker fee is not less than maker fee 39 | public final BigDecimal makerFee; 40 | 41 | public final BigDecimal marginBuy; 42 | public final BigDecimal marginSell; 43 | 44 | public final BigDecimal priceHighLimit; 45 | public final BigDecimal priceLowLimit; 46 | 47 | @JsonCreator 48 | @Builder 49 | public RestApiAddSymbol( 50 | @JsonProperty("symbolCode") String symbolCode, 51 | @JsonProperty("symbolId") int symbolId, 52 | @JsonProperty("symbolType") SymbolType symbolType, 53 | @JsonProperty("baseAsset") String baseAsset, 54 | @JsonProperty("quoteCurrency") String quoteCurrency, 55 | @JsonProperty("lotSize") BigDecimal lotSize, 56 | @JsonProperty("stepSize") BigDecimal stepSize, 57 | @JsonProperty("takerFee") BigDecimal takerFee, 58 | @JsonProperty("makerFee") BigDecimal makerFee, 59 | @JsonProperty("marginBuy") BigDecimal marginBuy, 60 | @JsonProperty("marginSell") BigDecimal marginSell, 61 | @JsonProperty("priceHighLimit") BigDecimal priceHighLimit, 62 | @JsonProperty("priceLowLimit") BigDecimal priceLowLimit) { 63 | 64 | this.symbolCode = symbolCode; 65 | this.symbolId = symbolId; 66 | 67 | this.symbolType = symbolType; 68 | 69 | this.baseAsset = baseAsset; 70 | this.quoteCurrency = quoteCurrency; 71 | this.lotSize = lotSize; 72 | this.stepSize = stepSize; 73 | this.takerFee = takerFee; 74 | this.makerFee = makerFee; 75 | 76 | this.marginBuy = marginBuy; 77 | this.marginSell = marginSell; 78 | this.priceHighLimit = priceHighLimit; 79 | this.priceLowLimit = priceLowLimit; 80 | } 81 | 82 | @Override 83 | public String toString() { 84 | return "[ADDSYMBOL " + symbolCode + " " + symbolId + "]"; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/commands/admin/RestApiAddUser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.commands.admin; 17 | 18 | 19 | import com.fasterxml.jackson.annotation.JsonCreator; 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | import lombok.Getter; 22 | 23 | @Getter 24 | public final class RestApiAddUser { 25 | 26 | private final long uid; 27 | 28 | @JsonCreator 29 | public RestApiAddUser(@JsonProperty("uid") long uid) { 30 | 31 | this.uid = uid; 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return "[ADDUSER " + uid + "]"; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/commands/admin/RestApiAdminAsset.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.commands.admin; 17 | 18 | 19 | import com.fasterxml.jackson.annotation.JsonCreator; 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | 22 | public final class RestApiAdminAsset { 23 | 24 | public final String assetCode; 25 | public final int assetId; 26 | public final int scale; // asset scale - 2 for most currencies, 8 for BTC, 0 for JPY, etc 27 | 28 | @JsonCreator 29 | public RestApiAdminAsset( 30 | @JsonProperty("assetCode") String assetCode, 31 | @JsonProperty("assetId") int assetId, 32 | @JsonProperty("scale") int scale) { 33 | 34 | this.assetCode = assetCode; 35 | this.assetId = assetId; 36 | this.scale = scale; 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | return "[ASSET " + assetCode + " " + assetId + "]"; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/commands/admin/RestApiRequestMarketData.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.commands.admin; 17 | 18 | 19 | import com.fasterxml.jackson.annotation.JsonCreator; 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | import lombok.Getter; 22 | 23 | @Getter 24 | public final class RestApiRequestMarketData { 25 | 26 | private final String symbolName; 27 | 28 | @JsonCreator 29 | public RestApiRequestMarketData(@JsonProperty("symbolName") String symbolName) { 30 | 31 | this.symbolName = symbolName; 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return "[REQ_MARKET_DATA " + symbolName + "]"; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/commands/admin/StompApiNotificationMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.commands.admin; 17 | 18 | public class StompApiNotificationMessage { 19 | 20 | private String content; 21 | 22 | public StompApiNotificationMessage() { 23 | } 24 | 25 | public StompApiNotificationMessage(String content) { 26 | this.content = content; 27 | } 28 | 29 | public String getContent() { 30 | return content; 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/commands/util/ArithmeticHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.commands.util; 17 | 18 | import lombok.extern.slf4j.Slf4j; 19 | import exchange.core2.rest.model.internal.GatewayAssetSpec; 20 | import exchange.core2.rest.model.internal.GatewaySymbolSpec; 21 | 22 | import java.math.BigDecimal; 23 | 24 | @Slf4j 25 | public class ArithmeticHelper { 26 | 27 | /** 28 | * Convert price for one lot (order book price) from core long to BigDecimal. 29 | * 30 | * @param price core price 31 | * @param symbolSpec gateway symbol spec 32 | * @return big decimal value 33 | */ 34 | public static BigDecimal fromLongPrice(long price, GatewaySymbolSpec symbolSpec) { 35 | 36 | //log.debug("symbolSpec.quoteCurrency.scale={}",symbolSpec.quoteCurrency.scale); 37 | //log.debug("symbolSpec.stepSize={}",symbolSpec.stepSize); 38 | //BigDecimal res = BigDecimal.valueOf(price).scaleByPowerOfTen(symbolSpec.quoteCurrency.scale).multiply(symbolSpec.stepSize); 39 | BigDecimal res = BigDecimal.valueOf(price).scaleByPowerOfTen(-symbolSpec.quoteCurrency.scale); 40 | //log.debug("res={}",res); 41 | return res; 42 | } 43 | 44 | public static BigDecimal fromLongPrice(long price, GatewayAssetSpec assetSpecSpec) { 45 | return BigDecimal.valueOf(price).scaleByPowerOfTen(-assetSpecSpec.scale); 46 | } 47 | 48 | public static BigDecimal toBaseUnits(BigDecimal price, GatewayAssetSpec assetSpec) { 49 | return price.scaleByPowerOfTen(assetSpec.scale); 50 | } 51 | 52 | /** 53 | * Check if BigDecimal is integer value and >=0 54 | * 55 | * @param value - value to examine 56 | * @return tre if it is integer and not negative 57 | */ 58 | public static boolean isIntegerNotNegativeValue(BigDecimal value) { 59 | //log.debug("value={} scale()={}", value, value.stripTrailingZeros().scale()); 60 | return value.compareTo(BigDecimal.ZERO) >= 0 && value.stripTrailingZeros().scale() <= 0; 61 | } 62 | 63 | /** 64 | * Check if BigDecimal is integer value and >0 65 | * 66 | * @param value - value to examine 67 | * @return tre if it is integer and positive 68 | */ 69 | public static boolean isIntegerPositiveNotZeroValue(BigDecimal value) { 70 | //log.debug("value={} scale()={}", value, value.stripTrailingZeros().scale()); 71 | return value.compareTo(BigDecimal.ZERO) > 0 && value.stripTrailingZeros().scale() <= 0; 72 | } 73 | 74 | public static boolean isZero(BigDecimal value) { 75 | return value.compareTo(BigDecimal.ZERO) == 0; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/controllers/GreetingController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.controllers; 17 | 18 | 19 | import exchange.core2.rest.commands.HelloMessage; 20 | import exchange.core2.rest.commands.admin.StompApiNotificationMessage; 21 | import exchange.core2.rest.model.api.StompApiTick; 22 | import lombok.extern.slf4j.Slf4j; 23 | import org.springframework.beans.factory.annotation.Autowired; 24 | import org.springframework.messaging.handler.annotation.MessageMapping; 25 | import org.springframework.messaging.handler.annotation.SendTo; 26 | import org.springframework.messaging.simp.SimpMessagingTemplate; 27 | import org.springframework.stereotype.Controller; 28 | import org.springframework.web.util.HtmlUtils; 29 | 30 | import javax.annotation.PostConstruct; 31 | import java.math.BigDecimal; 32 | import java.time.Instant; 33 | import java.util.Random; 34 | import java.util.concurrent.CompletableFuture; 35 | import java.util.concurrent.ThreadLocalRandom; 36 | 37 | @Slf4j 38 | @Controller 39 | public class GreetingController { 40 | 41 | @Autowired 42 | private SimpMessagingTemplate simpMessagingTemplate; 43 | 44 | @MessageMapping("/hello") 45 | @SendTo("/topic/notifications") 46 | public StompApiNotificationMessage greeting(HelloMessage message) throws Exception { 47 | log.debug("Greeting 1 {}", message); 48 | Thread.sleep(200); // simulated delay 49 | log.debug("Greeting 2 {}", message); 50 | return new StompApiNotificationMessage("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"); 51 | } 52 | 53 | // @PostConstruct 54 | // public void start() { 55 | // CompletableFuture.supplyAsync(() -> { 56 | // 57 | // while (true) { 58 | // try { 59 | // Thread.sleep(2000); 60 | // } catch (InterruptedException e) { 61 | // e.printStackTrace(); 62 | // } 63 | // log.debug("Sending heartbit..."); 64 | // simpMessagingTemplate.convertAndSend("/topic/notifications", new StompApiNotificationMessage(Instant.now().toString())); 65 | // 66 | // Random rnd = ThreadLocalRandom.current(); 67 | // simpMessagingTemplate.convertAndSend( 68 | // "/topic/ticks/XBTCUSDT", 69 | // new StompApiTick(BigDecimal.valueOf(rnd.nextFloat()), rnd.nextInt(100), System.currentTimeMillis())); 70 | // } 71 | // 72 | // }); 73 | // 74 | // } 75 | 76 | } -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/controllers/RestControllerHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.controllers; 17 | 18 | import lombok.extern.slf4j.Slf4j; 19 | import exchange.core2.core.common.cmd.CommandResultCode; 20 | import exchange.core2.core.common.cmd.OrderCommand; 21 | import exchange.core2.rest.commands.ApiErrorCodes; 22 | import exchange.core2.rest.events.RestGenericResponse; 23 | import org.springframework.http.HttpStatus; 24 | import org.springframework.http.ResponseEntity; 25 | 26 | import java.util.function.Supplier; 27 | 28 | @Slf4j 29 | public class RestControllerHelper { 30 | 31 | public static ResponseEntity errorResponse(ApiErrorCodes errMessage, String... args) { 32 | 33 | RestGenericResponse response = RestGenericResponse.builder() 34 | .ticket(0) 35 | .gatewayResultCode(errMessage.gatewayErrorCode) 36 | .coreResultCode(0) 37 | .description(String.format(errMessage.errorDescription, (Object[]) args)) 38 | .build(); 39 | 40 | log.info("return error: " + response); 41 | 42 | return ResponseEntity.status(errMessage.httpStatus).body(response); 43 | } 44 | 45 | public static ResponseEntity coreResponse(OrderCommand cmd, Supplier successMapper, HttpStatus successCode) { 46 | CommandResultCode resultCode = cmd.resultCode; 47 | return ResponseEntity 48 | .status(resultCode == CommandResultCode.SUCCESS ? successCode : HttpStatus.BAD_REQUEST) 49 | .body(RestGenericResponse.builder() 50 | .ticket(0) 51 | .gatewayResultCode(0) 52 | .coreResultCode(resultCode.getCode()) 53 | .data(successMapper.get()) 54 | .description(resultCode.toString()) 55 | .build()); 56 | } 57 | 58 | public static ResponseEntity successResponse(Object data, HttpStatus code) { 59 | return ResponseEntity 60 | .status(code) 61 | .body(RestGenericResponse.builder() 62 | .ticket(0) 63 | .gatewayResultCode(0) 64 | .coreResultCode(0) 65 | .data(data) 66 | //.description(null) 67 | .build()); 68 | 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/controllers/SyncAdminApiAccountsController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.controllers; 17 | 18 | import exchange.core2.core.common.BalanceAdjustmentType; 19 | import java.util.function.Consumer; 20 | import lombok.extern.slf4j.Slf4j; 21 | import exchange.core2.core.common.cmd.OrderCommand; 22 | import exchange.core2.core.ExchangeApi; 23 | import exchange.core2.core.ExchangeCore; 24 | import exchange.core2.rest.GatewayState; 25 | import exchange.core2.rest.commands.ApiErrorCodes; 26 | import exchange.core2.rest.commands.admin.RestApiAccountBalanceAdjustment; 27 | import exchange.core2.rest.commands.admin.RestApiAddUser; 28 | import exchange.core2.rest.events.RestGenericResponse; 29 | import exchange.core2.rest.model.internal.GatewayAssetSpec; 30 | import org.springframework.beans.factory.annotation.Autowired; 31 | import org.springframework.http.HttpStatus; 32 | import org.springframework.http.MediaType; 33 | import org.springframework.http.ResponseEntity; 34 | import org.springframework.web.bind.annotation.*; 35 | 36 | import java.math.BigDecimal; 37 | import java.util.concurrent.CompletableFuture; 38 | import java.util.concurrent.ExecutionException; 39 | 40 | @Slf4j 41 | @RestController 42 | @RequestMapping(value = "syncAdminApi/v1/", produces = MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8") 43 | public class SyncAdminApiAccountsController { 44 | 45 | @Autowired 46 | private ExchangeCore exchangeCore; 47 | 48 | @Autowired 49 | private GatewayState gatewayState; 50 | 51 | 52 | // @RequestMapping(value = "users2", method = RequestMethod.POST) 53 | // @ResponseStatus(HttpStatus.CREATED) 54 | // public CompletableFuture> createUser2(@Valid @RequestBody RestApiAddUser request) throws Exception { 55 | // 56 | // 57 | // log.info("ADD USER >>> {}", request); 58 | // 59 | //// if (false) { 60 | //// return errorResponse(req.response(), ApiErrorCodes.UNKNOWN_BASE_ASSET); 61 | //// } 62 | // 63 | // CompletableFuture> future = new CompletableFuture<>(); 64 | // 65 | // final ExchangeApi api = exchangeCore.getApi(); 66 | // 67 | // api.createUser(request.getUid(), cmd2 -> { 68 | // log.info("<<< ADD USER {}", cmd2); 69 | // 70 | // RestGenericResponse resp = RestGenericResponse.builder() 71 | // .ticket(0) 72 | // .gatewayResultCode(0) 73 | // .coreResultCode(0) 74 | // .data(cmd2.uid) 75 | // .description(cmd2.resultCode.toString()) 76 | // .build(); 77 | // 78 | // future.complete(resp); 79 | // }); 80 | // return future; 81 | // } 82 | 83 | @RequestMapping(value = "users", method = RequestMethod.POST) 84 | @ResponseStatus(HttpStatus.CREATED) 85 | public RestGenericResponse createUser(@RequestBody RestApiAddUser request) throws ExecutionException, InterruptedException { 86 | 87 | log.info("ADD USER >>> {}", request); 88 | 89 | ExchangeApi api = exchangeCore.getApi(); 90 | CompletableFuture future = new CompletableFuture<>(); 91 | api.createUser(request.getUid(), future::complete); 92 | 93 | OrderCommand cmd = future.get(); 94 | log.info("<<< ADD USER {}", cmd); 95 | 96 | return RestGenericResponse.builder() 97 | .ticket(0) 98 | .gatewayResultCode(0) 99 | .coreResultCode(cmd.resultCode.getCode()) 100 | .data(cmd.uid) 101 | .description(cmd.resultCode.toString()) 102 | .build(); 103 | 104 | } 105 | 106 | @RequestMapping(value = "users/{uid}/accounts", method = RequestMethod.POST) 107 | @ResponseStatus(HttpStatus.OK) 108 | public ResponseEntity adjustBalance( 109 | @PathVariable long uid, 110 | @RequestBody RestApiAccountBalanceAdjustment request) throws ExecutionException, InterruptedException { 111 | 112 | log.info("ADD BALANCE >>> {} {}", uid, request); 113 | 114 | // TODO currency conversion 115 | 116 | final GatewayAssetSpec currency = gatewayState.getAssetSpec(request.currency); 117 | if (currency == null) { 118 | return RestControllerHelper.errorResponse(ApiErrorCodes.UNKNOWN_CURRENCY); 119 | } 120 | 121 | 122 | final BigDecimal amount = request.getAmount().scaleByPowerOfTen(currency.scale).stripTrailingZeros(); 123 | if (amount.scale() > 0) { 124 | return RestControllerHelper.errorResponse(ApiErrorCodes.PRECISION_IS_TOO_HIGH); 125 | } 126 | 127 | final long longAmount = amount.longValue(); 128 | 129 | ExchangeApi api = exchangeCore.getApi(); 130 | CompletableFuture future = new CompletableFuture<>(); 131 | api.balanceAdjustment(uid, request.getTransactionId(), currency.assetId, longAmount, BalanceAdjustmentType.ADJUSTMENT, future::complete); 132 | 133 | OrderCommand orderCommand = future.get(); 134 | log.info("<<< ADD BALANCE {}", orderCommand); 135 | //asyncCoreResponseOk(asyncResponse, cmd, cmd2.resultCode); 136 | 137 | return RestControllerHelper.coreResponse(orderCommand, () -> uid, HttpStatus.CREATED); 138 | } 139 | 140 | 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/controllers/SyncAdminApiSymbolsController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.controllers; 17 | 18 | import exchange.core2.core.ExchangeApi; 19 | import exchange.core2.core.ExchangeCore; 20 | import exchange.core2.core.common.CoreSymbolSpecification; 21 | import exchange.core2.core.common.SymbolType; 22 | import exchange.core2.core.common.api.binary.BatchAddSymbolsCommand; 23 | import exchange.core2.core.common.cmd.OrderCommand; 24 | import exchange.core2.rest.GatewayState; 25 | import exchange.core2.rest.commands.ApiErrorCodes; 26 | import exchange.core2.rest.commands.admin.RestApiAddSymbol; 27 | import exchange.core2.rest.commands.admin.RestApiAdminAsset; 28 | import exchange.core2.rest.commands.util.ArithmeticHelper; 29 | import exchange.core2.rest.events.RestGenericResponse; 30 | import exchange.core2.rest.model.internal.GatewayAssetSpec; 31 | import exchange.core2.rest.model.internal.GatewaySymbolSpec; 32 | import lombok.extern.slf4j.Slf4j; 33 | import org.springframework.beans.factory.annotation.Autowired; 34 | import org.springframework.http.HttpStatus; 35 | import org.springframework.http.MediaType; 36 | import org.springframework.http.ResponseEntity; 37 | import org.springframework.web.bind.annotation.RequestBody; 38 | import org.springframework.web.bind.annotation.RequestMapping; 39 | import org.springframework.web.bind.annotation.RequestMethod; 40 | import org.springframework.web.bind.annotation.RestController; 41 | 42 | import java.math.BigDecimal; 43 | import java.util.concurrent.CompletableFuture; 44 | import java.util.concurrent.ExecutionException; 45 | 46 | @Slf4j 47 | @RestController 48 | @RequestMapping(value = "syncAdminApi/v1/", produces = MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8") 49 | public class SyncAdminApiSymbolsController { 50 | 51 | @Autowired 52 | private ExchangeCore exchangeCore; 53 | 54 | @Autowired 55 | private GatewayState gatewayState; 56 | 57 | 58 | @RequestMapping(value = "assets", method = RequestMethod.POST) 59 | public ResponseEntity createAsset(@RequestBody RestApiAdminAsset request) throws ExecutionException, InterruptedException { 60 | 61 | log.info(">>> ADD ASSET {}", request); 62 | 63 | // TODO Publish through bus 64 | final GatewayAssetSpec spec = GatewayAssetSpec.builder() 65 | .assetCode(request.assetCode) 66 | .assetId(request.assetId) 67 | .scale(request.scale) 68 | .active(true) // TODO set to INACTIVE 69 | .build(); 70 | 71 | if (!gatewayState.registerNewAsset(spec)) { 72 | log.warn("Can not add asset, already exists"); 73 | return RestControllerHelper.errorResponse(ApiErrorCodes.ASSET_ALREADY_EXISTS); 74 | } else { 75 | return RestControllerHelper.successResponse(request, HttpStatus.CREATED); 76 | } 77 | } 78 | 79 | @RequestMapping(value = "symbols", method = RequestMethod.POST) 80 | public ResponseEntity createSymbol(@RequestBody RestApiAddSymbol request) throws ExecutionException, InterruptedException { 81 | 82 | log.info("ADD SYMBOL >>> {}", request); 83 | 84 | // TODO Publish through bus 85 | 86 | final GatewayAssetSpec baseAsset = gatewayState.getAssetSpec(request.baseAsset); 87 | if (baseAsset == null) { 88 | log.warn("UNKNOWN_BASE_ASSET : " + request.baseAsset); 89 | return RestControllerHelper.errorResponse(ApiErrorCodes.UNKNOWN_BASE_ASSET); 90 | } 91 | 92 | final GatewayAssetSpec quoteCurrency = gatewayState.getAssetSpec(request.quoteCurrency); 93 | if (quoteCurrency == null) { 94 | log.warn("UNKNOWN_QUOTE_CURRENCY : " + request.quoteCurrency); 95 | return RestControllerHelper.errorResponse(ApiErrorCodes.UNKNOWN_QUOTE_CURRENCY); 96 | } 97 | 98 | // TODO validations 99 | final int symbolId = request.symbolId; 100 | 101 | // lot size in base asset units 102 | final BigDecimal lotSizeInBaseAssetUnits = request.lotSize.scaleByPowerOfTen(baseAsset.scale); 103 | if (!ArithmeticHelper.isIntegerPositiveNotZeroValue(lotSizeInBaseAssetUnits)) { 104 | return RestControllerHelper.errorResponse( 105 | ApiErrorCodes.INVALID_CONFIGURATION, 106 | "lot size must be integer and positive when converted to base asset units: " + lotSizeInBaseAssetUnits); 107 | } 108 | 109 | // step size in quote currency units 110 | final BigDecimal stepSizeInQuoteCurrencyUnits = request.stepSize.scaleByPowerOfTen(quoteCurrency.scale); 111 | if (!ArithmeticHelper.isIntegerPositiveNotZeroValue(stepSizeInQuoteCurrencyUnits)) { 112 | return RestControllerHelper.errorResponse( 113 | ApiErrorCodes.INVALID_CONFIGURATION, 114 | "step size must be integer and positive when converted to quote currency units: " + stepSizeInQuoteCurrencyUnits); 115 | } 116 | 117 | // taker fee in quote currency units 118 | final BigDecimal takerFeeInQuoteCurrencyUnits = request.takerFee.scaleByPowerOfTen(quoteCurrency.scale); 119 | if (!ArithmeticHelper.isIntegerNotNegativeValue(takerFeeInQuoteCurrencyUnits)) { 120 | return RestControllerHelper.errorResponse( 121 | ApiErrorCodes.INVALID_CONFIGURATION, 122 | "taker fee must be integer and not negative when converted to quote currency units: " + takerFeeInQuoteCurrencyUnits); 123 | } 124 | 125 | // maker fee in quote currency units 126 | final BigDecimal makerFeeInQuoteCurrencyUnits = request.makerFee.scaleByPowerOfTen(quoteCurrency.scale); 127 | if (!ArithmeticHelper.isIntegerNotNegativeValue(makerFeeInQuoteCurrencyUnits)) { 128 | return RestControllerHelper.errorResponse( 129 | ApiErrorCodes.INVALID_CONFIGURATION, 130 | "maker fee must be integer and not negative when converted to quote currency units: " + makerFeeInQuoteCurrencyUnits); 131 | } 132 | 133 | // maker/taker fee invariant validation 134 | if (takerFeeInQuoteCurrencyUnits.longValue() < makerFeeInQuoteCurrencyUnits.longValue()) { 135 | return RestControllerHelper.errorResponse( 136 | ApiErrorCodes.INVALID_CONFIGURATION, 137 | "taker fee " + takerFeeInQuoteCurrencyUnits + " can not be less than maker fee " + makerFeeInQuoteCurrencyUnits); 138 | } 139 | 140 | // margin buy in quote currency units 141 | final BigDecimal marginBuyInQuoteCurrencyUnits = request.marginBuy.scaleByPowerOfTen(quoteCurrency.scale); 142 | final BigDecimal marginSellInQuoteCurrencyUnits = request.marginSell.scaleByPowerOfTen(quoteCurrency.scale); 143 | 144 | log.debug("MARGIN {} {}", marginBuyInQuoteCurrencyUnits, marginSellInQuoteCurrencyUnits); 145 | if (request.symbolType == SymbolType.CURRENCY_EXCHANGE_PAIR) { 146 | // margin must be zero in exchange mode 147 | if (!ArithmeticHelper.isZero(marginBuyInQuoteCurrencyUnits) || !ArithmeticHelper.isZero(marginSellInQuoteCurrencyUnits)) { 148 | return RestControllerHelper.errorResponse(ApiErrorCodes.INVALID_CONFIGURATION, "margin must be zero in exchange mode"); 149 | } 150 | 151 | } else { 152 | 153 | if (!ArithmeticHelper.isIntegerPositiveNotZeroValue(marginBuyInQuoteCurrencyUnits)) { 154 | return RestControllerHelper.errorResponse( 155 | ApiErrorCodes.INVALID_CONFIGURATION, 156 | "buy margin must be integer and positive when converted to quote currency units: " + marginBuyInQuoteCurrencyUnits); 157 | } 158 | if (!ArithmeticHelper.isIntegerPositiveNotZeroValue(marginSellInQuoteCurrencyUnits)) { 159 | return RestControllerHelper.errorResponse( 160 | ApiErrorCodes.INVALID_CONFIGURATION, 161 | "sell margin must be integer and positive when converted to quote currency units: " + marginSellInQuoteCurrencyUnits); 162 | } 163 | } 164 | 165 | final GatewaySymbolSpec spec = GatewaySymbolSpec.builder() 166 | .symbolId(symbolId) 167 | .symbolCode(request.symbolCode) 168 | .symbolType(request.symbolType) 169 | .baseAsset(baseAsset) 170 | .quoteCurrency(quoteCurrency) 171 | .lotSize(request.lotSize) 172 | .stepSize(request.stepSize) 173 | .takerFee(request.takerFee) 174 | .makerFee(request.makerFee) 175 | .marginBuy(request.marginBuy) 176 | .marginSell(request.marginSell) 177 | .priceHighLimit(request.priceHighLimit) 178 | .priceLowLimit(request.priceLowLimit) 179 | .status(GatewaySymbolSpec.GatewaySymbolLifecycle.NEW) 180 | .build(); 181 | 182 | if (!gatewayState.registerNewSymbol(spec)) { 183 | log.warn("SYMBOL_ALREADY_EXISTS : id={} code={}", symbolId, request.symbolCode); 184 | return RestControllerHelper.errorResponse(ApiErrorCodes.SYMBOL_ALREADY_EXISTS); 185 | } 186 | 187 | final CoreSymbolSpecification coreSpec = CoreSymbolSpecification.builder() 188 | .symbolId(symbolId) 189 | .type(request.symbolType) 190 | .baseCurrency(baseAsset.assetId) 191 | .quoteCurrency(quoteCurrency.assetId) 192 | .baseScaleK(lotSizeInBaseAssetUnits.longValue()) 193 | .quoteScaleK(stepSizeInQuoteCurrencyUnits.longValue()) 194 | .takerFee(takerFeeInQuoteCurrencyUnits.longValue()) 195 | .makerFee(makerFeeInQuoteCurrencyUnits.longValue()) 196 | .marginBuy(marginBuyInQuoteCurrencyUnits.longValue()) 197 | .marginSell(marginSellInQuoteCurrencyUnits.longValue()) 198 | .build(); 199 | 200 | log.debug("Adding symbol {}", coreSpec); 201 | 202 | BatchAddSymbolsCommand batchAddSymbols = new BatchAddSymbolsCommand(coreSpec); 203 | 204 | ExchangeApi api = exchangeCore.getApi(); 205 | CompletableFuture future = new CompletableFuture<>(); 206 | 207 | api.submitBinaryCommandAsync(batchAddSymbols, 123567, future::complete); 208 | 209 | //asyncCoreResponseCreated(asyncResponse, newSpec, cmd2.resultCode); 210 | 211 | OrderCommand orderCommand = future.get(); 212 | 213 | log.info("<<< ADD SYMBOL {}", orderCommand); 214 | 215 | GatewaySymbolSpec newSpec = gatewayState.activateSymbol(symbolId); 216 | 217 | return RestControllerHelper.coreResponse(orderCommand, () -> newSpec, HttpStatus.CREATED); 218 | 219 | } 220 | 221 | 222 | } 223 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/controllers/SyncTradeAccountApiController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.controllers; 17 | 18 | import exchange.core2.core.ExchangeApi; 19 | import exchange.core2.core.ExchangeCore; 20 | import exchange.core2.core.common.Order; 21 | import exchange.core2.core.common.OrderType; 22 | import exchange.core2.core.common.api.reports.SingleUserReportQuery; 23 | import exchange.core2.core.common.api.reports.SingleUserReportResult; 24 | import exchange.core2.core.common.api.reports.SingleUserReportResult.QueryExecutionStatus; 25 | import exchange.core2.rest.GatewayState; 26 | import exchange.core2.rest.commands.ApiErrorCodes; 27 | import exchange.core2.rest.commands.util.ArithmeticHelper; 28 | import exchange.core2.rest.events.RestGenericResponse; 29 | import exchange.core2.rest.model.api.*; 30 | import exchange.core2.rest.model.internal.GatewayAssetSpec; 31 | import exchange.core2.rest.model.internal.GatewaySymbolSpec; 32 | import lombok.extern.slf4j.Slf4j; 33 | import org.eclipse.collections.impl.map.mutable.primitive.IntLongHashMap; 34 | import org.springframework.beans.factory.annotation.Autowired; 35 | import org.springframework.http.HttpStatus; 36 | import org.springframework.http.MediaType; 37 | import org.springframework.http.ResponseEntity; 38 | import org.springframework.web.bind.annotation.PathVariable; 39 | import org.springframework.web.bind.annotation.RequestMapping; 40 | import org.springframework.web.bind.annotation.RequestMethod; 41 | import org.springframework.web.bind.annotation.RestController; 42 | 43 | import java.util.*; 44 | import java.util.concurrent.ExecutionException; 45 | import java.util.stream.Collectors; 46 | import java.util.stream.Stream; 47 | 48 | @Slf4j 49 | @RestController 50 | @RequestMapping(value = "syncTradeApi/v1/", produces = MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8") 51 | public class SyncTradeAccountApiController { 52 | @Autowired 53 | private ExchangeCore exchangeCore; 54 | 55 | @Autowired 56 | private GatewayState gatewayState; 57 | 58 | @RequestMapping(value = "users/{uid}/state", method = RequestMethod.GET) 59 | public ResponseEntity getUserState( 60 | @PathVariable Long uid) throws ExecutionException, InterruptedException { 61 | log.info("USER REPORT >>> {}", uid); 62 | final ExchangeApi api = exchangeCore.getApi(); 63 | final SingleUserReportResult reportResult = api.processReport(new SingleUserReportQuery(uid), 123456).get(); 64 | 65 | log.debug("{}", reportResult); 66 | 67 | if (reportResult.getQueryExecutionStatus() == QueryExecutionStatus.OK) { 68 | 69 | final List activeOrders = new ArrayList<>(); 70 | 71 | Stream ordersStream = reportResult.getOrders().stream().flatMap(Collection::stream); 72 | 73 | final Map userCookies = gatewayState.getOrCreateUserProfile(uid).findUserCookies(ordersStream); 74 | 75 | reportResult.getOrders().forEachKeyValue((symbolId, ordersList) -> { 76 | 77 | final GatewaySymbolSpec symbolSpec = gatewayState.getSymbolSpec(symbolId); 78 | ordersList.forEach(coreOrder -> activeOrders.add(RestApiOrder.builder() 79 | .orderId(coreOrder.orderId) 80 | .size(coreOrder.size) 81 | .filled(coreOrder.filled) 82 | .price(ArithmeticHelper.fromLongPrice(coreOrder.price, symbolSpec)) 83 | .state(GatewayOrderState.ACTIVE) 84 | .action(coreOrder.action) 85 | .orderType(OrderType.GTC) 86 | .symbol(symbolSpec.symbolCode) 87 | .deals(Collections.emptyList()) // TODO add deals 88 | .userCookie(userCookies.get(coreOrder.orderId)) 89 | .build())); 90 | }); 91 | 92 | final IntLongHashMap profileAccounts = reportResult.getAccounts(); 93 | 94 | final List accounts = new ArrayList<>(profileAccounts.size()); 95 | profileAccounts.forEachKeyValue((assetId, balance) -> { 96 | final GatewayAssetSpec assetSpec = gatewayState.getAssetSpec(assetId); 97 | accounts.add(RestApiAccountState.builder() 98 | .currency(assetSpec.assetCode) 99 | .balance(ArithmeticHelper.fromLongPrice(balance, assetSpec)) 100 | .build()); 101 | }); 102 | final RestApiUserState state = RestApiUserState.builder() 103 | .accounts(accounts) 104 | .activeOrders(activeOrders) 105 | .uid(uid) 106 | .build(); 107 | 108 | return RestControllerHelper.successResponse(state, HttpStatus.OK); 109 | 110 | } else { 111 | return RestControllerHelper.errorResponse(ApiErrorCodes.UNKNOWN_USER_404); 112 | } 113 | } 114 | 115 | 116 | @RequestMapping(value = "users/{uid}/history", method = RequestMethod.GET) 117 | public ResponseEntity getUserTradesHistory( 118 | @PathVariable Long uid) { 119 | log.info("TRADES HISTORY >>> {}", uid); 120 | 121 | return gatewayState.getUserProfile(uid).map(up -> 122 | RestControllerHelper.successResponse(RestApiAccountTradeHistory.builder() 123 | .orders(up.mapHistoryOrders(coreOrder -> 124 | RestApiOrder.builder() 125 | .orderId(coreOrder.getOrderId()) 126 | .size(coreOrder.getSize()) 127 | .filled(coreOrder.getFilled()) 128 | .price(coreOrder.getPrice()) 129 | .state(coreOrder.getState()) 130 | .action(coreOrder.getAction()) 131 | .orderType(coreOrder.getOrderType()) 132 | .symbol(coreOrder.getSymbol()) 133 | .deals(coreOrder.getDeals().stream().map(deal -> RestApiDeal.builder() 134 | .party(deal.getMatchingRole()) 135 | .price(deal.getPrice()) 136 | .size(deal.getSize()) 137 | .build()).collect(Collectors.toList())) 138 | .userCookie(coreOrder.getUserCookie()) 139 | .build())) 140 | .uid(uid) 141 | .build(), HttpStatus.OK)) 142 | .orElseGet(() -> RestControllerHelper.errorResponse(ApiErrorCodes.UNKNOWN_USER_404)); 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/controllers/SyncTradeChartsApiController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.controllers; 17 | 18 | import exchange.core2.rest.GatewayState; 19 | import exchange.core2.rest.commands.ApiErrorCodes; 20 | import exchange.core2.rest.events.RestGenericResponse; 21 | import exchange.core2.rest.model.api.RestApiBar; 22 | import exchange.core2.rest.model.api.TimeFrame; 23 | import exchange.core2.rest.model.internal.GatewayBarStatic; 24 | import lombok.extern.slf4j.Slf4j; 25 | import org.springframework.beans.factory.annotation.Autowired; 26 | import org.springframework.http.HttpStatus; 27 | import org.springframework.http.MediaType; 28 | import org.springframework.http.ResponseEntity; 29 | import org.springframework.web.bind.annotation.PathVariable; 30 | import org.springframework.web.bind.annotation.RequestMapping; 31 | import org.springframework.web.bind.annotation.RequestMethod; 32 | import org.springframework.web.bind.annotation.RestController; 33 | 34 | import java.util.stream.Collectors; 35 | 36 | @Slf4j 37 | @RestController 38 | @RequestMapping(value = "syncTradeApi/v1/", produces = MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8") 39 | public class SyncTradeChartsApiController { 40 | 41 | @Autowired 42 | private GatewayState gatewayState; 43 | 44 | @RequestMapping(value = "symbols/{symbol}/bars/{timeFrame}", method = RequestMethod.GET) 45 | public ResponseEntity getBars( 46 | @PathVariable String symbol, 47 | @PathVariable TimeFrame timeFrame) { 48 | log.info("GET BARS >>> {} - {}", symbol, timeFrame); 49 | 50 | return gatewayState.getBars(symbol, 100, timeFrame) 51 | .map(bars -> RestControllerHelper.successResponse( 52 | bars.stream() 53 | .map((GatewayBarStatic bar) -> new RestApiBar( 54 | bar.getOpen(), 55 | bar.getHigh(), 56 | bar.getLow(), 57 | bar.getClose(), 58 | bar.getVolume(), 59 | bar.getTimestamp())) 60 | .collect(Collectors.toList()), 61 | HttpStatus.OK)) 62 | .orElseGet(() -> RestControllerHelper.errorResponse(ApiErrorCodes.UNKNOWN_SYMBOL_404)); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/controllers/SyncTradeMiscApiController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.controllers; 17 | 18 | import exchange.core2.core.ExchangeCore; 19 | import exchange.core2.rest.GatewayState; 20 | import exchange.core2.rest.events.RestGenericResponse; 21 | import exchange.core2.rest.model.api.RestApiAsset; 22 | import exchange.core2.rest.model.api.RestApiExchangeInfo; 23 | import exchange.core2.rest.model.api.RestApiSymbol; 24 | import exchange.core2.rest.model.api.RestApiTime; 25 | import lombok.extern.slf4j.Slf4j; 26 | import org.jetbrains.annotations.NotNull; 27 | import org.springframework.beans.factory.annotation.Autowired; 28 | import org.springframework.http.HttpStatus; 29 | import org.springframework.http.MediaType; 30 | import org.springframework.http.ResponseEntity; 31 | import org.springframework.web.bind.annotation.RequestMapping; 32 | import org.springframework.web.bind.annotation.RequestMethod; 33 | import org.springframework.web.bind.annotation.RestController; 34 | 35 | import java.time.Instant; 36 | import java.time.format.DateTimeFormatter; 37 | import java.util.List; 38 | 39 | @Slf4j 40 | @RestController 41 | @RequestMapping(value = "syncTradeApi/v1/", produces = MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8") 42 | public class SyncTradeMiscApiController { 43 | 44 | @Autowired 45 | private ExchangeCore exchangeCore; 46 | 47 | @Autowired 48 | private GatewayState gatewayState; 49 | 50 | @RequestMapping(value = "ping", method = RequestMethod.GET) 51 | public ResponseEntity getPing() { 52 | log.info("PING >>>"); 53 | return RestControllerHelper.successResponse(null, HttpStatus.OK); 54 | } 55 | 56 | @RequestMapping(value = "time", method = RequestMethod.GET) 57 | public ResponseEntity getTime() { 58 | log.info("TIME >>>"); 59 | return RestControllerHelper.successResponse( 60 | getRestApiTime(), 61 | HttpStatus.OK); 62 | } 63 | 64 | @NotNull 65 | private RestApiTime getRestApiTime() { 66 | final Instant now = Instant.now(); 67 | return new RestApiTime(DateTimeFormatter.ISO_INSTANT.format(now), now.toEpochMilli()); 68 | } 69 | 70 | @RequestMapping(value = "info", method = RequestMethod.GET) 71 | public ResponseEntity getExchangeInfo() { 72 | log.info("EXCHANGE INFO >>>"); 73 | 74 | final List activeAssets = gatewayState.getActiveAssets(c -> new RestApiAsset(c.assetCode, c.scale)); 75 | 76 | log.info("EXCHANGE activeAssets >>>", activeAssets); 77 | 78 | final List activeSymbols = gatewayState.getActiveSymbols(s -> new RestApiSymbol( 79 | s.symbolCode, 80 | s.symbolType, 81 | s.baseAsset.assetCode, 82 | s.quoteCurrency.assetCode, 83 | s.lotSize, 84 | s.stepSize, 85 | s.takerFee, 86 | s.makerFee, 87 | s.marginBuy, 88 | s.marginSell, 89 | s.priceHighLimit, 90 | s.priceLowLimit 91 | )); 92 | 93 | final RestApiExchangeInfo restApiExchangeInfo = RestApiExchangeInfo.builder() 94 | .assets(activeAssets) 95 | .symbols(activeSymbols) 96 | .serverTime(getRestApiTime()) 97 | .build(); 98 | 99 | return RestControllerHelper.successResponse( 100 | restApiExchangeInfo, 101 | HttpStatus.OK); 102 | } 103 | 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/controllers/SyncTradeOrdersApiController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.controllers; 17 | 18 | import exchange.core2.core.ExchangeApi; 19 | import exchange.core2.core.ExchangeCore; 20 | import exchange.core2.core.common.L2MarketData; 21 | import exchange.core2.core.common.cmd.OrderCommand; 22 | import exchange.core2.rest.GatewayState; 23 | import exchange.core2.rest.commands.ApiErrorCodes; 24 | import exchange.core2.rest.commands.RestApiMoveOrder; 25 | import exchange.core2.rest.commands.RestApiPlaceOrder; 26 | import exchange.core2.rest.commands.util.ArithmeticHelper; 27 | import exchange.core2.rest.events.RestGenericResponse; 28 | import exchange.core2.rest.model.api.GatewayOrderState; 29 | import exchange.core2.rest.model.api.RestApiOrder; 30 | import exchange.core2.rest.model.api.RestApiOrderBook; 31 | import exchange.core2.rest.model.internal.GatewaySymbolSpec; 32 | import lombok.extern.slf4j.Slf4j; 33 | import org.springframework.beans.factory.annotation.Autowired; 34 | import org.springframework.http.HttpStatus; 35 | import org.springframework.http.MediaType; 36 | import org.springframework.http.ResponseEntity; 37 | import org.springframework.web.bind.annotation.*; 38 | 39 | import java.math.BigDecimal; 40 | import java.util.Arrays; 41 | import java.util.Collections; 42 | import java.util.concurrent.CompletableFuture; 43 | import java.util.concurrent.ExecutionException; 44 | import java.util.stream.Collectors; 45 | 46 | @Slf4j 47 | @RestController 48 | @RequestMapping(value = "syncTradeApi/v1/", produces = MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8") 49 | public class SyncTradeOrdersApiController { 50 | 51 | @Autowired 52 | private ExchangeCore exchangeCore; 53 | 54 | @Autowired 55 | private GatewayState gatewayState; 56 | 57 | // TODO per user 58 | //private ConcurrentHashMap userCookies = new ConcurrentHashMap<>(); 59 | 60 | @RequestMapping(value = "symbols/{symbol}/orderbook", method = RequestMethod.GET) 61 | public ResponseEntity getOrderBook( 62 | @PathVariable String symbol, 63 | @RequestParam Integer depth) throws ExecutionException, InterruptedException { 64 | log.info("ORDERBOOK >>> {} {}", symbol, depth); 65 | 66 | GatewaySymbolSpec symbolSpec = gatewayState.getSymbolSpec(symbol); 67 | if (symbolSpec == null) { 68 | return RestControllerHelper.errorResponse(ApiErrorCodes.UNKNOWN_SYMBOL_404); 69 | } 70 | 71 | ExchangeApi api = exchangeCore.getApi(); 72 | CompletableFuture> future = new CompletableFuture<>(); 73 | api.orderBookRequest(symbolSpec.symbolId, depth, orderCommand -> { 74 | L2MarketData marketData = orderCommand.marketData; 75 | future.complete(RestControllerHelper.coreResponse( 76 | orderCommand, 77 | () -> RestApiOrderBook.builder() 78 | .symbol(symbol) 79 | .askPrices(Arrays.stream(marketData.askPrices).mapToObj(p -> ArithmeticHelper.fromLongPrice(p, symbolSpec)).collect(Collectors.toList())) 80 | .bidPrices(Arrays.stream(marketData.bidPrices).mapToObj(p -> ArithmeticHelper.fromLongPrice(p, symbolSpec)).collect(Collectors.toList())) 81 | .askVolumes(Arrays.stream(marketData.askVolumes).boxed().collect(Collectors.toList())) 82 | .bidVolumes(Arrays.stream(marketData.bidVolumes).boxed().collect(Collectors.toList())) 83 | .build(), 84 | HttpStatus.OK)); 85 | }); 86 | 87 | ResponseEntity response = future.get(); 88 | log.info("<<< ORDERBOOK {}", response); 89 | 90 | return response; 91 | } 92 | 93 | 94 | @RequestMapping(value = "symbols/{symbol}/trade/{uid}/orders", method = RequestMethod.POST) 95 | public ResponseEntity placeOrder( 96 | @PathVariable long uid, 97 | @PathVariable String symbol, 98 | @RequestBody RestApiPlaceOrder placeOrder) throws ExecutionException, InterruptedException { 99 | 100 | log.info("PLACE ORDER >>> {}", placeOrder); 101 | 102 | GatewaySymbolSpec symbolSpec = gatewayState.getSymbolSpec(symbol); 103 | if (symbolSpec == null) { 104 | return RestControllerHelper.errorResponse(ApiErrorCodes.UNKNOWN_SYMBOL_404); 105 | } 106 | 107 | final BigDecimal priceInQuoteCurrencyUnits = ArithmeticHelper.toBaseUnits(placeOrder.getPrice(), symbolSpec.quoteCurrency); 108 | if (!ArithmeticHelper.isIntegerNotNegativeValue(priceInQuoteCurrencyUnits)) { 109 | return RestControllerHelper.errorResponse(ApiErrorCodes.INVALID_PRICE); 110 | } 111 | 112 | final long price = priceInQuoteCurrencyUnits.longValue(); 113 | 114 | // TODO perform conversions 115 | 116 | ExchangeApi api = exchangeCore.getApi(); 117 | CompletableFuture future = new CompletableFuture<>(); 118 | long orderId = api.placeNewOrder( 119 | placeOrder.getUserCookie(), 120 | price, 121 | price, // same price (can not move bids up in exchange mode) 122 | placeOrder.getSize(), 123 | placeOrder.getAction(), 124 | placeOrder.getOrderType(), 125 | symbolSpec.symbolId, 126 | uid, 127 | future::complete); 128 | log.info("placing orderId {}", orderId); 129 | 130 | // TODO can be inserted after events - insert into cookie-based queue first? 131 | gatewayState.getOrCreateUserProfile(uid).addNewOrder(orderId, symbol, placeOrder); 132 | 133 | OrderCommand orderCommand = future.get(); 134 | log.info("<<< PLACE ORDER {}", orderCommand); 135 | 136 | // TODO extract method and fix values 137 | RestApiOrder result = RestApiOrder.builder() 138 | .orderId(orderCommand.orderId) 139 | .size(orderCommand.size) 140 | .filled(0) 141 | .state(GatewayOrderState.NEW) 142 | .userCookie(orderCommand.userCookie) 143 | .action(orderCommand.action) 144 | .orderType(orderCommand.orderType) 145 | .symbol(gatewayState.getSymbolSpec(orderCommand.symbol).symbolCode) 146 | .deals(Collections.emptyList()) 147 | .build(); 148 | 149 | return RestControllerHelper.coreResponse(orderCommand, () -> result, HttpStatus.CREATED); 150 | } 151 | 152 | 153 | @RequestMapping(value = "symbols/{symbol}/trade/{uid}/orders/{orderId}", method = RequestMethod.PUT) 154 | public ResponseEntity moveOrder( 155 | @PathVariable long uid, 156 | @PathVariable String symbol, 157 | @PathVariable long orderId, 158 | @RequestBody RestApiMoveOrder moveOrder) throws ExecutionException, InterruptedException { 159 | 160 | log.info("MOVE ORDER >>> {} uid={} {}", orderId, uid, moveOrder); 161 | 162 | GatewaySymbolSpec symbolSpec = gatewayState.getSymbolSpec(symbol); 163 | if (symbolSpec == null) { 164 | return RestControllerHelper.errorResponse(ApiErrorCodes.UNKNOWN_SYMBOL_404); 165 | } 166 | 167 | final BigDecimal priceInQuoteCurrencyUnits = ArithmeticHelper.toBaseUnits(moveOrder.getPrice(), symbolSpec.quoteCurrency); 168 | if (!ArithmeticHelper.isIntegerNotNegativeValue(priceInQuoteCurrencyUnits)) { 169 | return RestControllerHelper.errorResponse(ApiErrorCodes.INVALID_PRICE); 170 | } 171 | 172 | ExchangeApi api = exchangeCore.getApi(); 173 | CompletableFuture future = new CompletableFuture<>(); 174 | api.moveOrder( 175 | priceInQuoteCurrencyUnits.longValue(), 176 | orderId, 177 | symbolSpec.symbolId, 178 | uid, 179 | future::complete); 180 | 181 | OrderCommand orderCommand = future.get(); 182 | log.info("<<< MOVE ORDER {}", orderCommand); 183 | 184 | // TODO extract method and fix values 185 | RestApiOrder result = RestApiOrder.builder() 186 | .orderId(orderCommand.orderId) 187 | .size(orderCommand.size) 188 | .filled(-1) 189 | .state(GatewayOrderState.ACTIVE) 190 | .userCookie(orderCommand.userCookie) 191 | .action(orderCommand.action) 192 | .orderType(orderCommand.orderType) 193 | .symbol(gatewayState.getSymbolSpec(orderCommand.symbol).symbolCode) 194 | .deals(Collections.emptyList()) 195 | .build(); 196 | 197 | return RestControllerHelper.coreResponse(orderCommand, () -> result, HttpStatus.OK); 198 | } 199 | 200 | @RequestMapping(value = "symbols/{symbol}/trade/{uid}/orders/{orderId}", method = RequestMethod.DELETE) 201 | public ResponseEntity cancelOrder( 202 | @PathVariable long uid, 203 | @PathVariable String symbol, 204 | @PathVariable long orderId) throws ExecutionException, InterruptedException { 205 | 206 | log.info("CANCEL ORDER >>> {}", orderId); 207 | 208 | GatewaySymbolSpec specification = gatewayState.getSymbolSpec(symbol); 209 | if (specification == null) { 210 | return RestControllerHelper.errorResponse(ApiErrorCodes.UNKNOWN_SYMBOL_404); 211 | } 212 | 213 | ExchangeApi api = exchangeCore.getApi(); 214 | CompletableFuture future = new CompletableFuture<>(); 215 | api.cancelOrder( 216 | orderId, 217 | specification.symbolId, 218 | uid, 219 | future::complete); 220 | 221 | OrderCommand orderCommand = future.get(); 222 | log.info("<<< CANCEL ORDER {}", orderCommand); 223 | 224 | // TODO extract method and fix values 225 | RestApiOrder result = RestApiOrder.builder() 226 | .orderId(orderCommand.orderId) 227 | .size(orderCommand.size) 228 | .filled(-1) 229 | .state(GatewayOrderState.CANCELLED) 230 | .userCookie(orderCommand.userCookie) 231 | .action(orderCommand.action) 232 | .orderType(orderCommand.orderType) 233 | .symbol(gatewayState.getSymbolSpec(orderCommand.symbol).symbolCode) 234 | .deals(Collections.emptyList()) 235 | .build(); 236 | 237 | return RestControllerHelper.coreResponse(orderCommand, () -> result, HttpStatus.OK); 238 | } 239 | 240 | } 241 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/events/MatchingRole.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.events; 17 | 18 | public enum MatchingRole { 19 | MAKER, 20 | TAKER 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/events/NewTradeRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.events; 17 | 18 | 19 | import lombok.AllArgsConstructor; 20 | import lombok.Builder; 21 | import lombok.Getter; 22 | 23 | /** 24 | * Trade fill 25 | */ 26 | 27 | @Getter 28 | @Builder 29 | @AllArgsConstructor 30 | public final class NewTradeRecord implements OrderSizeChangeRecord { 31 | private final String type = "trade"; 32 | private final long filledSize; 33 | private final long fillPrice; 34 | private final MatchingRole matchingRole; 35 | 36 | @Override 37 | public long getAffectedSize() { 38 | return filledSize; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/events/OrderBookEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.events; 17 | 18 | import lombok.AllArgsConstructor; 19 | import lombok.Builder; 20 | import lombok.Getter; 21 | import lombok.Setter; 22 | 23 | 24 | @Getter 25 | @Setter 26 | @Builder 27 | @AllArgsConstructor 28 | public final class OrderBookEvent { 29 | private final String msgType = "orderbook"; 30 | private String symbol; 31 | private long timestamp; 32 | private long[] askPrices; 33 | private long[] askVolumes; 34 | private long[] bidPrices; 35 | private long[] bidVolumes; 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/events/OrderSizeChangeRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.events; 17 | 18 | /** 19 | * Size of the order has been reduced due one of the following reasons: 20 | * - trade matching, 21 | * - reduce (cancel or update) or 22 | * - rejection (no liquidity for matching order) 23 | *

24 | * Size of the order can never increase. 25 | */ 26 | 27 | public interface OrderSizeChangeRecord { 28 | 29 | long getAffectedSize(); 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/events/OrderUpdateEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.events; 17 | 18 | 19 | import lombok.AllArgsConstructor; 20 | import lombok.Builder; 21 | import lombok.Getter; 22 | 23 | import java.util.List; 24 | 25 | @Getter 26 | @Builder 27 | @AllArgsConstructor 28 | public final class OrderUpdateEvent { 29 | 30 | private final long orderId; 31 | private final long activeSize; 32 | private final long price; 33 | // todo add status? 34 | 35 | private final List trades; 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/events/ReduceRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.events; 17 | 18 | 19 | import lombok.AllArgsConstructor; 20 | import lombok.Builder; 21 | import lombok.Getter; 22 | 23 | /** 24 | * Reduced (update or cancel) 25 | */ 26 | @Getter 27 | @Builder 28 | @AllArgsConstructor 29 | public final class ReduceRecord implements OrderSizeChangeRecord { 30 | private final String type = "reduce"; 31 | private final long reducedSize; 32 | 33 | @Override 34 | public long getAffectedSize() { 35 | return reducedSize; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/events/RejectionRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.events; 17 | 18 | 19 | import lombok.AllArgsConstructor; 20 | import lombok.Builder; 21 | import lombok.Getter; 22 | 23 | /** 24 | * Rejected (no liquidity) 25 | */ 26 | @Getter 27 | @Builder 28 | @AllArgsConstructor 29 | public final class RejectionRecord implements OrderSizeChangeRecord { 30 | private final String type = "reject"; 31 | private final long rejectedSize; 32 | 33 | @Override 34 | public long getAffectedSize() { 35 | return rejectedSize; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/events/RestGenericResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.events; 17 | 18 | 19 | import com.fasterxml.jackson.annotation.JsonProperty; 20 | import lombok.Builder; 21 | import lombok.Getter; 22 | 23 | @Getter 24 | @Builder 25 | public final class RestGenericResponse { 26 | 27 | private final long ticket; 28 | private final int gatewayResultCode; 29 | private final int coreResultCode; 30 | private final String description; 31 | private final T data; 32 | 33 | public RestGenericResponse( 34 | @JsonProperty("ticket") long ticket, 35 | @JsonProperty("gatewayResultCode") int gatewayResultCode, 36 | @JsonProperty("coreResultCode") int coreResultCode, 37 | @JsonProperty("description") String description, 38 | @JsonProperty("data") T data) { 39 | 40 | this.ticket = ticket; 41 | this.gatewayResultCode = gatewayResultCode; 42 | this.coreResultCode = coreResultCode; 43 | this.description = description; 44 | this.data = data; 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | return "[RESPONSE T:" + ticket + " RES:" + gatewayResultCode + " " + coreResultCode + " " + description + "]"; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/events/admin/SymbolUpdateAdminEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.events.admin; 17 | 18 | import lombok.AllArgsConstructor; 19 | import lombok.Builder; 20 | import lombok.Getter; 21 | import lombok.Setter; 22 | 23 | 24 | @Getter 25 | @Setter 26 | @Builder 27 | @AllArgsConstructor 28 | public final class SymbolUpdateAdminEvent { 29 | private final String msgType = "adm_symbol_update"; 30 | 31 | private final int symbolId; 32 | private final String symbolCode; 33 | 34 | // unmodifiable properties (gateway level) 35 | private final int priceStep; // price % priceStep == 0 36 | private final int priceScale; // decimal point position 37 | private final int lotSize; 38 | 39 | 40 | // modifiable properties (core level) 41 | // deposit settings 42 | private final long depositBuy; 43 | private final long depositSell; 44 | 45 | // order book limits 46 | private final long priceHighLimit; 47 | private final long priceLowLimit; 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/events/admin/UserBalanceAdjustmentAdminEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.events.admin; 17 | 18 | import lombok.AllArgsConstructor; 19 | import lombok.Builder; 20 | import lombok.Getter; 21 | import lombok.Setter; 22 | 23 | 24 | @Getter 25 | @Setter 26 | @Builder 27 | @AllArgsConstructor 28 | public final class UserBalanceAdjustmentAdminEvent { 29 | private final String msgType = "user_balance_updated"; 30 | 31 | private final long uid; 32 | private final long transactionId; 33 | private final long amount; 34 | private final long balance; 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/events/admin/UserCreatedAdminEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.events.admin; 17 | 18 | import lombok.AllArgsConstructor; 19 | import lombok.Builder; 20 | import lombok.Getter; 21 | import lombok.Setter; 22 | 23 | 24 | @Getter 25 | @Setter 26 | @Builder 27 | @AllArgsConstructor 28 | public final class UserCreatedAdminEvent { 29 | private final String msgType = "adm_user_created"; 30 | 31 | private final long uid; 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/api/GatewayOrderState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.api; 17 | 18 | import lombok.AllArgsConstructor; 19 | 20 | @AllArgsConstructor 21 | public enum GatewayOrderState { 22 | 23 | NEW, 24 | //PENDING, 25 | ACTIVE, // new or partiallyFiled 26 | PARTIALLY_FILLED, // filled < size 27 | COMPLETED, // filled = size 28 | CANCELLED, // can be partially filled before cancelled, always check filled value 29 | REJECTED // can be partially filled before rejected, always check filled value 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/api/RestApiAccountState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.api; 17 | 18 | import com.fasterxml.jackson.annotation.JsonCreator; 19 | import com.fasterxml.jackson.annotation.JsonProperty; 20 | import lombok.Builder; 21 | import lombok.Getter; 22 | 23 | import java.math.BigDecimal; 24 | 25 | @Getter 26 | public class RestApiAccountState { 27 | 28 | // public final BigDecimal balanceHold; 29 | // public final BigDecimal balanceAvailable; 30 | public final String currency; 31 | public final BigDecimal balance; 32 | 33 | 34 | @JsonCreator 35 | @Builder 36 | public RestApiAccountState( 37 | @JsonProperty("currency") String currency, 38 | @JsonProperty("balance") BigDecimal balance) { 39 | 40 | this.balance = balance; 41 | this.currency = currency; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/api/RestApiAccountTradeHistory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.api; 17 | 18 | import com.fasterxml.jackson.annotation.JsonCreator; 19 | import com.fasterxml.jackson.annotation.JsonProperty; 20 | import lombok.Builder; 21 | import lombok.Getter; 22 | 23 | import java.util.List; 24 | 25 | @Getter 26 | public class RestApiAccountTradeHistory { 27 | 28 | public final long uid; 29 | public final List orders; 30 | 31 | @JsonCreator 32 | @Builder 33 | public RestApiAccountTradeHistory( 34 | @JsonProperty("uid") long uid, 35 | @JsonProperty("orders") List orders) { 36 | 37 | this.uid = uid; 38 | this.orders = orders; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/api/RestApiAsset.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.api; 17 | 18 | import com.fasterxml.jackson.annotation.JsonCreator; 19 | import com.fasterxml.jackson.annotation.JsonProperty; 20 | import lombok.Builder; 21 | import lombok.Getter; 22 | import lombok.ToString; 23 | 24 | @ToString 25 | @Getter 26 | public class RestApiAsset { 27 | 28 | private final String assetCode; 29 | private final int scale; // asset scale - normally 2 for currencies, 8 for BTC, etc 30 | 31 | @JsonCreator 32 | @Builder 33 | public RestApiAsset(@JsonProperty("assetCode") String assetCode, 34 | @JsonProperty("scale") int scale) { 35 | this.assetCode = assetCode; 36 | this.scale = scale; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/api/RestApiBar.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.api; 17 | 18 | import com.fasterxml.jackson.annotation.JsonCreator; 19 | import com.fasterxml.jackson.annotation.JsonProperty; 20 | import lombok.Builder; 21 | import lombok.Getter; 22 | import lombok.ToString; 23 | 24 | import java.math.BigDecimal; 25 | 26 | @ToString 27 | @Getter 28 | public class RestApiBar { 29 | 30 | private final BigDecimal open; 31 | private final BigDecimal high; 32 | private final BigDecimal low; 33 | private final BigDecimal close; 34 | private final long volume; 35 | // private final int index; 36 | private final long timestamp; 37 | 38 | @JsonCreator 39 | @Builder 40 | public RestApiBar( 41 | @JsonProperty("open") BigDecimal open, 42 | @JsonProperty("high") BigDecimal high, 43 | @JsonProperty("low") BigDecimal low, 44 | @JsonProperty("close") BigDecimal close, 45 | @JsonProperty("volume") long volume, 46 | // @JsonProperty("index") int index, 47 | @JsonProperty("timestamp") long timestamp) { 48 | 49 | this.open = open; 50 | this.high = high; 51 | this.low = low; 52 | this.close = close; 53 | this.volume = volume; 54 | // this.index = index; 55 | this.timestamp = timestamp; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/api/RestApiDeal.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.api; 17 | 18 | 19 | import com.fasterxml.jackson.annotation.JsonCreator; 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | import exchange.core2.rest.events.MatchingRole; 22 | import lombok.Builder; 23 | import lombok.Getter; 24 | 25 | import java.math.BigDecimal; 26 | 27 | @Getter 28 | public final class RestApiDeal { 29 | 30 | private final BigDecimal price; 31 | private final long size; 32 | private final MatchingRole party; 33 | 34 | 35 | // TODO add more fields 36 | 37 | @JsonCreator 38 | @Builder 39 | public RestApiDeal( 40 | @JsonProperty("price") BigDecimal price, 41 | @JsonProperty("size") long size, 42 | @JsonProperty("party") MatchingRole party) { 43 | 44 | this.price = price; 45 | this.size = size; 46 | this.party = party; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/api/RestApiExchangeInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.api; 17 | 18 | import com.fasterxml.jackson.annotation.JsonCreator; 19 | import com.fasterxml.jackson.annotation.JsonProperty; 20 | import lombok.Builder; 21 | import lombok.Getter; 22 | 23 | import java.util.List; 24 | 25 | @Getter 26 | public final class RestApiExchangeInfo { 27 | 28 | public final RestApiTime serverTime; 29 | public final List assets; 30 | public final List symbols; 31 | 32 | @JsonCreator 33 | @Builder 34 | public RestApiExchangeInfo( 35 | @JsonProperty("serverTime") RestApiTime serverTime, 36 | @JsonProperty("assets") List assets, 37 | @JsonProperty("symbols") List symbols) { 38 | 39 | this.serverTime = serverTime; 40 | this.assets = assets; 41 | this.symbols = symbols; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/api/RestApiOrder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.api; 17 | 18 | 19 | import com.fasterxml.jackson.annotation.JsonCreator; 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | import exchange.core2.core.common.OrderAction; 22 | import exchange.core2.core.common.OrderType; 23 | import lombok.Builder; 24 | import lombok.Getter; 25 | 26 | import java.math.BigDecimal; 27 | import java.util.List; 28 | 29 | @Getter 30 | public final class RestApiOrder { 31 | 32 | private final long orderId; 33 | 34 | private final BigDecimal price; 35 | private final long size; 36 | private final long filled; 37 | 38 | private final GatewayOrderState state; 39 | 40 | private final int userCookie; 41 | 42 | private final OrderAction action; 43 | private final OrderType orderType; 44 | 45 | private final String symbol; 46 | 47 | private final List deals; 48 | 49 | // TODO add more fields 50 | 51 | @JsonCreator 52 | @Builder 53 | public RestApiOrder( 54 | @JsonProperty("orderId") long orderId, 55 | @JsonProperty("price") BigDecimal price, 56 | @JsonProperty("size") long size, 57 | @JsonProperty("filled") long filled, 58 | @JsonProperty("userCookie") int userCookie, 59 | @JsonProperty("state") GatewayOrderState state, 60 | @JsonProperty("action") OrderAction action, 61 | @JsonProperty("orderType") OrderType orderType, 62 | @JsonProperty("symbol") String symbol, 63 | @JsonProperty("deals") List deals) { 64 | 65 | this.orderId = orderId; 66 | this.price = price; 67 | this.size = size; 68 | this.filled = filled; 69 | this.userCookie = userCookie; 70 | this.state = state; 71 | this.action = action; 72 | this.orderType = orderType; 73 | this.symbol = symbol; 74 | this.deals = deals; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/api/RestApiOrderBook.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.api; 17 | 18 | 19 | import com.fasterxml.jackson.annotation.JsonCreator; 20 | import com.fasterxml.jackson.annotation.JsonIgnore; 21 | import com.fasterxml.jackson.annotation.JsonProperty; 22 | import lombok.Builder; 23 | import lombok.EqualsAndHashCode; 24 | import lombok.Getter; 25 | import lombok.experimental.Wither; 26 | 27 | import java.math.BigDecimal; 28 | import java.util.List; 29 | 30 | @Getter 31 | @Wither 32 | @EqualsAndHashCode 33 | public final class RestApiOrderBook { 34 | 35 | //private final long timestamp; 36 | 37 | private final String symbol; 38 | 39 | private final List askPrices; 40 | private final List askVolumes; 41 | private final List bidPrices; 42 | private final List bidVolumes; 43 | 44 | @JsonCreator 45 | @Builder 46 | public RestApiOrderBook( 47 | @JsonProperty("symbol") String symbol, 48 | @JsonProperty("askPrices") List askPrices, 49 | @JsonProperty("askVolumes") List askVolumes, 50 | @JsonProperty("bidPrices") List bidPrices, 51 | @JsonProperty("bidVolumes") List bidVolumes) { 52 | 53 | this.symbol = symbol; 54 | 55 | this.askPrices = askPrices; 56 | this.askVolumes = askVolumes; 57 | this.bidPrices = bidPrices; 58 | this.bidVolumes = bidVolumes; 59 | } 60 | 61 | @JsonIgnore 62 | public boolean isEmpty() { 63 | return askPrices.isEmpty() && askVolumes.isEmpty() && bidPrices.isEmpty() && bidVolumes.isEmpty(); 64 | } 65 | 66 | @Override 67 | public String toString() { 68 | return "RestApiOrderBook{" + 69 | "symbol='" + symbol + '\'' + 70 | ", askPrices=" + askPrices + 71 | ", askVolumes=" + askVolumes + 72 | ", bidPrices=" + bidPrices + 73 | ", bidVolumes=" + bidVolumes + 74 | '}'; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/api/RestApiSymbol.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.api; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | import exchange.core2.core.common.SymbolType; 20 | import lombok.Getter; 21 | import lombok.ToString; 22 | 23 | import java.math.BigDecimal; 24 | 25 | @ToString 26 | @Getter 27 | public class RestApiSymbol { 28 | 29 | private final String symbolCode; 30 | 31 | private final SymbolType symbolType; 32 | 33 | private final String baseAsset; // base asset 34 | private final String quoteCurrency; // quote/counter currency (OR futures contract currency) 35 | private final BigDecimal lotSize; 36 | private final BigDecimal stepSize; 37 | 38 | private final BigDecimal takerFee; 39 | private final BigDecimal makerFee; 40 | 41 | private final BigDecimal marginBuy; 42 | private final BigDecimal marginSell; 43 | 44 | private final BigDecimal priceHighLimit; 45 | private final BigDecimal priceLowLimit; 46 | 47 | public RestApiSymbol( 48 | @JsonProperty("symbolCode") String symbolCode, 49 | @JsonProperty("symbolType") SymbolType symbolType, 50 | @JsonProperty("baseAsset") String baseAsset, 51 | @JsonProperty("quoteCurrency") String quoteCurrency, 52 | @JsonProperty("lotSize") BigDecimal lotSize, 53 | @JsonProperty("stepSize") BigDecimal stepSize, 54 | @JsonProperty("takerFee") BigDecimal takerFee, 55 | @JsonProperty("makerFee") BigDecimal makerFee, 56 | @JsonProperty("marginBuy") BigDecimal marginBuy, 57 | @JsonProperty("marginSell") BigDecimal marginSell, 58 | @JsonProperty("priceHighLimit") BigDecimal priceHighLimit, 59 | @JsonProperty("priceLowLimit") BigDecimal priceLowLimit) { 60 | 61 | this.symbolCode = symbolCode; 62 | this.symbolType = symbolType; 63 | this.baseAsset = baseAsset; 64 | this.quoteCurrency = quoteCurrency; 65 | this.lotSize = lotSize; 66 | this.stepSize = stepSize; 67 | this.takerFee = takerFee; 68 | this.makerFee = makerFee; 69 | this.marginBuy = marginBuy; 70 | this.marginSell = marginSell; 71 | this.priceHighLimit = priceHighLimit; 72 | this.priceLowLimit = priceLowLimit; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/api/RestApiTime.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.api; 17 | 18 | import com.fasterxml.jackson.annotation.JsonCreator; 19 | import com.fasterxml.jackson.annotation.JsonProperty; 20 | import lombok.Builder; 21 | import lombok.Getter; 22 | import lombok.ToString; 23 | 24 | @ToString 25 | @Getter 26 | public class RestApiTime { 27 | private final String isoTime; 28 | private final long epoch; 29 | 30 | @JsonCreator 31 | @Builder 32 | public RestApiTime( 33 | @JsonProperty("isoTime") String isoTime, 34 | @JsonProperty("epoch") long epoch) { 35 | 36 | this.isoTime = isoTime; 37 | this.epoch = epoch; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/api/RestApiUserState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.api; 17 | 18 | import com.fasterxml.jackson.annotation.JsonCreator; 19 | import com.fasterxml.jackson.annotation.JsonProperty; 20 | import lombok.Builder; 21 | import lombok.Getter; 22 | 23 | import java.util.List; 24 | 25 | @Getter 26 | public final class RestApiUserState { 27 | 28 | public final long uid; 29 | public final List activeOrders; 30 | public final List accounts; 31 | 32 | @JsonCreator 33 | @Builder 34 | public RestApiUserState( 35 | @JsonProperty("uid") long uid, 36 | @JsonProperty("activeOrders") List activeOrders, 37 | @JsonProperty("accounts") List accounts) { 38 | 39 | this.uid = uid; 40 | this.activeOrders = activeOrders; 41 | this.accounts = accounts; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/api/RestApiUserTradesHistory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.api; 17 | 18 | import com.fasterxml.jackson.annotation.JsonCreator; 19 | import com.fasterxml.jackson.annotation.JsonProperty; 20 | import lombok.Builder; 21 | import lombok.Getter; 22 | 23 | import java.util.List; 24 | 25 | @Getter 26 | public final class RestApiUserTradesHistory { 27 | 28 | public final long uid; 29 | public final List orders; 30 | 31 | @JsonCreator 32 | @Builder 33 | public RestApiUserTradesHistory( 34 | @JsonProperty("uid") long uid, 35 | @JsonProperty("activeOrders") List activeOrders) { 36 | 37 | this.uid = uid; 38 | this.orders = activeOrders; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/api/StompAccountUpdate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.api; 17 | 18 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 19 | import lombok.Builder; 20 | import lombok.Value; 21 | 22 | import java.math.BigDecimal; 23 | 24 | // TODO design off-core balance calculation support 25 | 26 | @JsonDeserialize(builder = StompAccountUpdate.StompAccountUpdateBuilder.class) 27 | @Value 28 | @Builder 29 | public class StompAccountUpdate { 30 | 31 | private final long uid; 32 | private final String currencyCode; 33 | private final BigDecimal amount; 34 | private final long timestamp; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/api/StompApiTick.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.api; 17 | 18 | import com.fasterxml.jackson.annotation.JsonCreator; 19 | import com.fasterxml.jackson.annotation.JsonProperty; 20 | import lombok.Builder; 21 | import lombok.Getter; 22 | import lombok.ToString; 23 | 24 | import java.math.BigDecimal; 25 | 26 | @ToString 27 | @Getter 28 | public class StompApiTick { 29 | 30 | private final BigDecimal price; 31 | private final long volume; 32 | private final long timestamp; 33 | 34 | @JsonCreator 35 | @Builder 36 | public StompApiTick( 37 | @JsonProperty("price") BigDecimal price, 38 | @JsonProperty("volume") long volume, 39 | @JsonProperty("timestamp") long timestamp) { 40 | 41 | this.price = price; 42 | this.volume = volume; 43 | this.timestamp = timestamp; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/api/StompOrderUpdate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.api; 17 | 18 | import com.fasterxml.jackson.annotation.JsonCreator; 19 | import com.fasterxml.jackson.annotation.JsonProperty; 20 | import exchange.core2.core.common.OrderAction; 21 | import exchange.core2.core.common.OrderType; 22 | import lombok.Builder; 23 | import lombok.Getter; 24 | import lombok.ToString; 25 | 26 | import java.math.BigDecimal; 27 | 28 | //@JsonDeserialize(builder = StompOrderUpdate.StompOrderUpdateBuilder.class) 29 | @Getter 30 | @ToString 31 | public class StompOrderUpdate { 32 | 33 | // TODO remove immutable field 34 | 35 | private final long uid; 36 | private final long orderId; 37 | 38 | private final BigDecimal price; 39 | private final long size; // immutable 40 | private final long filled; 41 | 42 | private final GatewayOrderState state; 43 | 44 | private final int userCookie; 45 | 46 | private final OrderAction action; // immutable 47 | private final OrderType orderType; // immutable 48 | private final String symbol; // immutable 49 | 50 | @JsonCreator 51 | @Builder 52 | public StompOrderUpdate( 53 | @JsonProperty("uid") long uid, 54 | @JsonProperty("orderId") long orderId, 55 | @JsonProperty("price") BigDecimal price, 56 | @JsonProperty("size") long size, 57 | @JsonProperty("filled") long filled, 58 | @JsonProperty("state") GatewayOrderState state, 59 | @JsonProperty("userCookie") int userCookie, 60 | @JsonProperty("action") OrderAction action, 61 | @JsonProperty("orderType") OrderType orderType, 62 | @JsonProperty("symbol") String symbol) { 63 | 64 | this.uid = uid; 65 | this.orderId = orderId; 66 | this.price = price; 67 | this.size = size; 68 | this.filled = filled; 69 | this.state = state; 70 | this.userCookie = userCookie; 71 | this.action = action; 72 | this.orderType = orderType; 73 | this.symbol = symbol; 74 | } 75 | 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/api/TimeFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.api; 17 | 18 | import lombok.AllArgsConstructor; 19 | import lombok.Getter; 20 | 21 | import java.time.Duration; 22 | import java.time.temporal.ChronoUnit; 23 | import java.time.temporal.TemporalUnit; 24 | 25 | @AllArgsConstructor 26 | @Getter 27 | public enum TimeFrame { 28 | 29 | // TODO fix 5,15,4 values 30 | M1(Duration.ofMinutes(1), ChronoUnit.MINUTES), 31 | M5(Duration.ofMinutes(5), ChronoUnit.MINUTES), 32 | M15(Duration.ofMinutes(15), ChronoUnit.MINUTES), 33 | H1(Duration.ofHours(1), ChronoUnit.HOURS), 34 | H4(Duration.ofHours(4), ChronoUnit.HOURS), 35 | D1(Duration.ofDays(1), ChronoUnit.DAYS), 36 | W1(Duration.ofDays(7), ChronoUnit.WEEKS); 37 | 38 | private final Duration duration; 39 | private final TemporalUnit truncateUnit; 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/internal/BarsData.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.internal; 17 | 18 | import exchange.core2.rest.model.api.TimeFrame; 19 | import lombok.extern.slf4j.Slf4j; 20 | 21 | import java.math.BigDecimal; 22 | import java.time.Duration; 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | 26 | /** 27 | * Not thread safe 28 | */ 29 | @Slf4j 30 | public class BarsData { 31 | 32 | private final TimeFrame timeFrame; 33 | 34 | private final List bars = new ArrayList<>(); 35 | 36 | private long volume = 0; 37 | 38 | private BigDecimal open; 39 | private BigDecimal high; 40 | private BigDecimal low; 41 | private BigDecimal close; 42 | 43 | private long startTimestamp; 44 | private long endTimestamp; 45 | 46 | 47 | public BarsData(TimeFrame timeFrame) { 48 | this.timeFrame = timeFrame; 49 | } 50 | 51 | public void addTick(BigDecimal price, long size, long timestamp) { 52 | 53 | log.debug("{} < {}", timestamp, endTimestamp); 54 | if (timestamp < endTimestamp) { 55 | // just update values 56 | 57 | if (price.compareTo(high) > 0) { 58 | high = price; 59 | } 60 | if (price.compareTo(low) > 0) { 61 | low = price; 62 | } 63 | close = price; 64 | volume += size; 65 | 66 | } else { 67 | 68 | // time to close last bar and create a new one 69 | if (volume > 0) { 70 | bars.add(new GatewayBarStatic(open, high, low, close, volume, startTimestamp)); 71 | } 72 | 73 | // todo implement correct rounding 74 | Duration duration = timeFrame.getDuration(); 75 | long dur = duration.getSeconds() * 1000; 76 | startTimestamp = (timestamp / dur) * dur; 77 | 78 | endTimestamp = startTimestamp + timeFrame.getDuration().toMillis(); 79 | 80 | open = price; 81 | high = price; 82 | low = price; 83 | close = price; 84 | volume = size; 85 | } 86 | } 87 | 88 | public List getRecentBars(int numBars) { 89 | final int size = bars.size(); 90 | final int startFrom = Math.max(0, size - numBars); 91 | List result = new ArrayList<>(Math.min(size, numBars)); 92 | for (int i = startFrom; i < size; i++) { 93 | result.add(bars.get(i)); 94 | } 95 | 96 | // add last bar 97 | if (volume > 0) { 98 | result.add(new GatewayBarStatic(open, high, low, close, volume, startTimestamp)); 99 | } 100 | 101 | return result; 102 | } 103 | 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/internal/ChartData.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.internal; 17 | 18 | import exchange.core2.rest.model.api.TimeFrame; 19 | import lombok.NoArgsConstructor; 20 | import lombok.extern.slf4j.Slf4j; 21 | 22 | import java.util.List; 23 | import java.util.concurrent.LinkedBlockingQueue; 24 | 25 | 26 | @NoArgsConstructor 27 | @Slf4j 28 | public class ChartData { 29 | 30 | private final BarsData bars = new BarsData(TimeFrame.M1); 31 | 32 | private final LinkedBlockingQueue ticksQueue = new LinkedBlockingQueue<>(); 33 | 34 | private volatile boolean flushingTicks = false; 35 | 36 | /** 37 | * Thread safe 38 | */ 39 | public void addTicks(List ticksToAdd) { 40 | ticksQueue.addAll(ticksToAdd); 41 | } 42 | 43 | /** 44 | * Thread safe 45 | */ 46 | public List getBarsData(int barsNum, TimeFrame timeFrame) { 47 | 48 | // TODO can return older result for second requester 49 | if (!ticksQueue.isEmpty() && !flushingTicks) { 50 | 51 | log.debug("Flushing ticks..."); 52 | 53 | // flush ticks queue if needed 54 | flushingTicks = true; 55 | synchronized (bars) { 56 | ticksQueue.forEach(tick -> { 57 | log.debug("Tick: {}", tick); 58 | bars.addTick(tick.getPrice(), tick.getSize(), tick.getTimestamp()); 59 | }); 60 | } 61 | flushingTicks = false; 62 | } 63 | 64 | return bars.getRecentBars(barsNum); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/internal/GatewayAssetSpec.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.internal; 17 | 18 | 19 | import lombok.AllArgsConstructor; 20 | import lombok.Builder; 21 | import lombok.Getter; 22 | 23 | @Builder 24 | @AllArgsConstructor 25 | public final class GatewayAssetSpec { 26 | 27 | public final String assetCode; 28 | public final int assetId; 29 | public final int scale; // asset scale - normally 2 for currencies, 8 for BTC, etc 30 | 31 | // TODO lifecycle - new, active, ceased 32 | public final boolean active; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/internal/GatewayBarLast.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.internal; 17 | 18 | import lombok.AllArgsConstructor; 19 | import lombok.Getter; 20 | import lombok.ToString; 21 | 22 | import java.math.BigDecimal; 23 | import java.time.Instant; 24 | 25 | @AllArgsConstructor 26 | @Getter 27 | @ToString 28 | public class GatewayBarLast { 29 | 30 | private BigDecimal open; 31 | private BigDecimal high; 32 | private BigDecimal low; 33 | private BigDecimal close; 34 | private long volume; 35 | private Instant timestamp; 36 | 37 | public GatewayBarLast(BigDecimal price, long volume, Instant timestamp) { 38 | this.open = price; 39 | this.high = price; 40 | this.low = price; 41 | this.close = price; 42 | this.volume = volume; 43 | this.timestamp = timestamp; 44 | } 45 | 46 | public void addTick(BigDecimal price, long volume, Instant timestamp) { 47 | this.high = this.high.max(price); 48 | this.low = this.high.min(price); 49 | this.close = price; 50 | this.volume += volume; 51 | this.timestamp = timestamp; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/internal/GatewayBarStatic.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.internal; 17 | 18 | import lombok.AllArgsConstructor; 19 | import lombok.Builder; 20 | import lombok.Getter; 21 | import lombok.ToString; 22 | 23 | import java.math.BigDecimal; 24 | import java.time.Instant; 25 | 26 | @ToString 27 | @AllArgsConstructor 28 | @Builder 29 | @Getter 30 | public class GatewayBarStatic { 31 | 32 | private final BigDecimal open; 33 | private final BigDecimal high; 34 | private final BigDecimal low; 35 | private final BigDecimal close; 36 | private final long volume; 37 | // private final int index; 38 | private final long timestamp; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/internal/GatewayDeal.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.internal; 17 | 18 | import exchange.core2.rest.events.MatchingRole; 19 | import lombok.AllArgsConstructor; 20 | import lombok.Builder; 21 | import lombok.Getter; 22 | import lombok.ToString; 23 | 24 | import java.math.BigDecimal; 25 | 26 | @AllArgsConstructor 27 | @Builder 28 | @Getter 29 | @ToString 30 | public class GatewayDeal { 31 | 32 | private final long size; 33 | private final BigDecimal price; 34 | private final MatchingRole matchingRole; 35 | private final long timestamp; 36 | 37 | // hidden from regular user 38 | private final long counterOrderId; 39 | private final long counterPartyUid; 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/internal/GatewayOrder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.internal; 17 | 18 | import exchange.core2.core.common.OrderAction; 19 | import exchange.core2.core.common.OrderType; 20 | import exchange.core2.rest.model.api.GatewayOrderState; 21 | import lombok.*; 22 | 23 | import java.math.BigDecimal; 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | 27 | @AllArgsConstructor 28 | @Builder 29 | @Getter 30 | @ToString 31 | public class GatewayOrder { 32 | 33 | private final long orderId; 34 | 35 | private final int userCookie; 36 | 37 | private final long size; 38 | private final OrderAction action; 39 | private final OrderType orderType; 40 | 41 | private final String symbol; 42 | 43 | // mutable fields 44 | 45 | private final List deals = new ArrayList<>(); 46 | 47 | @Setter 48 | private BigDecimal price; 49 | 50 | @Setter 51 | private long filled; 52 | 53 | @Setter 54 | private GatewayOrderState state; 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/internal/GatewaySymbolSpec.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.internal; 17 | 18 | 19 | import exchange.core2.core.common.SymbolType; 20 | import lombok.AllArgsConstructor; 21 | import lombok.Builder; 22 | import lombok.experimental.Wither; 23 | 24 | import java.math.BigDecimal; 25 | 26 | @Builder 27 | @Wither 28 | @AllArgsConstructor 29 | public final class GatewaySymbolSpec { 30 | 31 | public final int symbolId; 32 | public final String symbolCode; 33 | 34 | public final SymbolType symbolType; 35 | 36 | public final GatewayAssetSpec baseAsset; // base asset 37 | public final GatewayAssetSpec quoteCurrency; // quote/counter currency (OR futures contract currency) 38 | public final BigDecimal lotSize; 39 | public final BigDecimal stepSize; 40 | 41 | public final BigDecimal takerFee; // TODO check invariant: taker fee is not less than maker fee 42 | public final BigDecimal makerFee; 43 | 44 | public final BigDecimal marginBuy; 45 | public final BigDecimal marginSell; 46 | 47 | public final BigDecimal priceHighLimit; 48 | public final BigDecimal priceLowLimit; 49 | 50 | public final GatewaySymbolLifecycle status; 51 | 52 | public enum GatewaySymbolLifecycle { 53 | NEW, 54 | ACTIVE, 55 | INACTIVE; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/internal/GatewayUserProfile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.internal; 17 | 18 | import exchange.core2.core.IEventsHandler.RejectEvent; 19 | import exchange.core2.core.common.Order; 20 | import exchange.core2.rest.commands.RestApiPlaceOrder; 21 | import exchange.core2.rest.events.MatchingRole; 22 | import exchange.core2.rest.model.api.GatewayOrderState; 23 | import lombok.extern.slf4j.Slf4j; 24 | 25 | import java.math.BigDecimal; 26 | import java.util.HashMap; 27 | import java.util.List; 28 | import java.util.Map; 29 | import java.util.function.Consumer; 30 | import java.util.function.Function; 31 | import java.util.stream.Collectors; 32 | import java.util.stream.Stream; 33 | import org.agrona.collections.MutableReference; 34 | 35 | 36 | /** 37 | * Thread safe 38 | */ 39 | @Slf4j 40 | public class GatewayUserProfile { 41 | 42 | // orders in status NEW/ACTIVE status 43 | private final Map openOrders = new HashMap<>(); 44 | 45 | // orders in other statuses 46 | private final Map ordersHistory = new HashMap<>(); 47 | 48 | public synchronized Map findUserCookies(final Stream activeOrders) { 49 | return activeOrders 50 | .map(Order::getOrderId) 51 | .collect(Collectors.toMap( 52 | orderId -> orderId, 53 | orderId -> openOrders.get(orderId).getUserCookie())); 54 | } 55 | 56 | public synchronized void addNewOrder(long orderId, String symbol, RestApiPlaceOrder restApiPlaceOrder) { 57 | 58 | GatewayOrder order = GatewayOrder.builder() 59 | .orderId(orderId) 60 | .userCookie(restApiPlaceOrder.getUserCookie()) 61 | .price(restApiPlaceOrder.getPrice()) 62 | .size(restApiPlaceOrder.getSize()) 63 | .orderType(restApiPlaceOrder.getOrderType()) 64 | .action(restApiPlaceOrder.getAction()) 65 | .filled(0) 66 | .symbol(symbol) 67 | .state(GatewayOrderState.NEW) 68 | .build(); 69 | 70 | openOrders.put(orderId, order); 71 | } 72 | 73 | public synchronized void activateOrder(long orderId) { 74 | GatewayOrder gatewayOrder = openOrders.get(orderId); 75 | gatewayOrder.setState(GatewayOrderState.ACTIVE); 76 | } 77 | 78 | public synchronized void tradeOrder( 79 | long orderId, 80 | long size, 81 | BigDecimal price, 82 | MatchingRole matchingRole, 83 | long timestamp, 84 | long counterOrderId, 85 | long counterPartyUid, 86 | Consumer notifier) { 87 | 88 | final GatewayOrder gatewayOrder = openOrders.get(orderId); 89 | 90 | if (gatewayOrder.getState() == GatewayOrderState.ACTIVE) { 91 | gatewayOrder.setState(GatewayOrderState.PARTIALLY_FILLED); 92 | } 93 | 94 | long filled = gatewayOrder.getFilled() + size; 95 | gatewayOrder.setFilled(filled); 96 | if (gatewayOrder.getSize() == filled) { 97 | openOrders.remove(orderId); 98 | ordersHistory.put(orderId, gatewayOrder); 99 | gatewayOrder.setState(GatewayOrderState.COMPLETED); 100 | log.debug("MOVED order {} into history section", orderId); 101 | } 102 | 103 | gatewayOrder.getDeals().add(GatewayDeal.builder() 104 | .size(size) 105 | .price(price) 106 | .matchingRole(matchingRole) 107 | .timestamp(timestamp) 108 | .counterOrderId(counterOrderId) 109 | .counterPartyUid(counterPartyUid) 110 | .build()); 111 | 112 | notifier.accept(gatewayOrder); 113 | } 114 | 115 | public synchronized void rejectOrder(MutableReference evt, Consumer notifier) { 116 | GatewayOrder gatewayOrder = openOrders.remove(evt.ref.orderId); 117 | gatewayOrder.setState(GatewayOrderState.REJECTED); 118 | ordersHistory.put(evt.ref.orderId, gatewayOrder); 119 | log.debug("MOVED order {} into history section", evt.ref.orderId); 120 | notifier.accept(gatewayOrder); 121 | } 122 | // btc-usdt 123 | // Ask: 3: 9000 124 | 125 | // Bid: 5: 9100 IOC 126 | // 9000 : 3 , 2 cancel 127 | //2: reject 1 128 | //3: trade 2 129 | 130 | public synchronized void cancelOrder(long orderId, Consumer notifier) { 131 | GatewayOrder gatewayOrder = openOrders.remove(orderId); 132 | ordersHistory.put(orderId, gatewayOrder); 133 | gatewayOrder.setState(GatewayOrderState.CANCELLED); 134 | log.debug("MOVED order {} into history section", orderId); 135 | notifier.accept(gatewayOrder); 136 | } 137 | 138 | 139 | public synchronized void updateOrderPrice(long orderId, BigDecimal newPrice, Consumer notifier) { 140 | GatewayOrder gatewayOrder = openOrders.get(orderId); 141 | ordersHistory.put(orderId, gatewayOrder); 142 | gatewayOrder.setPrice(newPrice); 143 | notifier.accept(gatewayOrder); 144 | } 145 | 146 | public synchronized List mapHistoryOrders(Function mapper) { 147 | return ordersHistory.values().stream().map(mapper).collect(Collectors.toList()); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/exchange/core2/rest/model/internal/TickRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.model.internal; 17 | 18 | import exchange.core2.core.common.OrderAction; 19 | import lombok.AllArgsConstructor; 20 | import lombok.Getter; 21 | import lombok.ToString; 22 | 23 | import java.math.BigDecimal; 24 | 25 | @AllArgsConstructor 26 | @ToString 27 | @Getter 28 | public class TickRecord { 29 | 30 | private BigDecimal price; 31 | private long size; 32 | private long timestamp; 33 | private OrderAction action; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # =============================== 2 | # = DATA SOURCE 3 | # =============================== 4 | # Set here configurations for the database connection 5 | spring.datasource.url=jdbc:postgresql://localhost:5432/postgres 6 | spring.datasource.username=postgres 7 | spring.datasource.password=123 8 | spring.datasource.driver-class-name=org.postgresql.Driver 9 | spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true 10 | spring.jpa.open-in-view=false 11 | # Keep the connection alive if idle for a long time (needed in production) 12 | #spring.datasource.testWhileIdle=true 13 | #spring.datasource.validationQuery=SELECT 1 14 | server.port=8080 15 | #spring.main.allow-circular-references=true 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V0000__initial_schema.sql: -------------------------------------------------------------------------------- 1 | DROP SCHEMA IF EXISTS gw CASCADE; 2 | CREATE SCHEMA gw; 3 | 4 | -- keeps last known sequence that database is consistent to 5 | CREATE TABLE gw.global_state ( 6 | state_seq BIGINT NOT NULL 7 | ); 8 | 9 | -- users 10 | CREATE TABLE gw.users ( 11 | uid BIGSERIAL, 12 | user_name VARCHAR(128) NOT NULL, 13 | user_email VARCHAR(128) NOT NULL, 14 | user_password_hash VARCHAR(64) NOT NULL, 15 | user_state SMALLINT NOT NULL, 16 | user_state_seq BIGINT NOT NULL, 17 | CONSTRAINT users_pk PRIMARY KEY (uid) 18 | ); 19 | 20 | -- assets and currencies 21 | CREATE TABLE gw.assets ( 22 | asset_id SERIAL, 23 | asset_code VARCHAR(32) NOT NULL, 24 | asset_name VARCHAR(64) NOT NULL, 25 | asset_scale SMALLINT NOT NULL, 26 | asset_state SMALLINT NOT NULL, 27 | asset_state_seq BIGINT NOT NULL, 28 | CONSTRAINT assets_pk PRIMARY KEY (asset_id) 29 | ); 30 | 31 | -- symbols 32 | CREATE TABLE gw.symbols ( 33 | symbol_id SERIAL, 34 | symbol_code VARCHAR(32) NOT NULL, 35 | symbol_name VARCHAR(64) NOT NULL, 36 | symbol_type SMALLINT NOT NULL, 37 | symbol_base_asset_id INTEGER NOT NULL REFERENCES gw.assets (asset_id), 38 | symbol_quote_asset_id INTEGER NOT NULL REFERENCES gw.assets (asset_id), 39 | symbol_lot_size NUMERIC(18,8) NOT NULL, 40 | symbol_step_size NUMERIC(18,8) NOT NULL, 41 | symbol_taker_fee NUMERIC(18,8) NOT NULL, 42 | symbol_margin_buy NUMERIC(18,8) NOT NULL, 43 | symbol_price_high_limit NUMERIC(18,8) NOT NULL, 44 | symbol_state SMALLINT NOT NULL, 45 | symbol_state_seq BIGINT NOT NULL, 46 | CONSTRAINT symbols_pk PRIMARY KEY (symbol_id) 47 | ); 48 | 49 | -- all orders (pending, opened, closed) 50 | CREATE TABLE gw.orders ( 51 | order_id BIGSERIAL, 52 | order_uid BIGINT NOT NULL REFERENCES gw.users (uid), 53 | order_price_raw BIGINT NOT NULL, 54 | order_size BIGINT NOT NULL, 55 | order_filled BIGINT NOT NULL, 56 | order_user_cookie INTEGER NOT NULL, 57 | order_action SMALLINT NOT NULL, 58 | order_order_type SMALLINT NOT NULL, 59 | order_create_time TIMESTAMP WITH TIME ZONE NOT NULL, 60 | order_state SMALLINT NOT NULL, 61 | order_state_seq BIGINT NOT NULL, 62 | CONSTRAINT orders_pk PRIMARY KEY (order_id) 63 | ); 64 | 65 | -- all deals (trades) 66 | CREATE TABLE gw.deals ( 67 | deal_id BIGSERIAL, 68 | deal_taker_uid BIGINT NOT NULL REFERENCES gw.users (uid), 69 | deal_taker_order_id BIGINT NOT NULL REFERENCES gw.orders (order_id), 70 | deal_taker_action SMALLINT NOT NULL, 71 | deal_maker_uid BIGINT NOT NULL REFERENCES gw.users (uid), 72 | deal_maker_order_id BIGINT NOT NULL REFERENCES gw.orders (order_id), 73 | deal_price_raw BIGINT NOT NULL, 74 | deal_size BIGINT NOT NULL, 75 | deal_seq BIGINT NOT NULL, 76 | CONSTRAINT deals_pk PRIMARY KEY (deal_id) 77 | ); 78 | -------------------------------------------------------------------------------- /src/main/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Atmosphere Chat 14 | 15 | 102 | 103 | 104 |

105 |
106 |
107 | Connecting... 108 | 109 |
110 | 111 |
112 |
113 |
114 |
115 | 116 | 117 | -------------------------------------------------------------------------------- /src/main/resources/javascript/application.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | "use strict"; 3 | 4 | var header = $('#header'); 5 | var content = $('#content'); 6 | var input = $('#input'); 7 | var status = $('#status'); 8 | var myName = false; 9 | var author = null; 10 | var logged = false; 11 | var socket = atmosphere; 12 | var subSocket; 13 | var transport = 'websocket'; 14 | 15 | // We are now ready to cut the request 16 | var request = { url: document.location.protocol + "//" + document.location.host + '/chat', 17 | contentType: "application/json", 18 | logLevel: 'debug', 19 | transport: transport, 20 | trackMessageLength: true, 21 | enableProtocol: true, 22 | fallbackTransport: 'long-polling'}; 23 | 24 | 25 | request.onOpen = function (response) { 26 | content.html($('

', { text: 'Atmosphere connected using ' + response.transport })); 27 | input.removeAttr('disabled').focus(); 28 | status.text('Choose name:'); 29 | transport = response.transport; 30 | }; 31 | 32 | // For demonstration of how you can customize the fallbackTransport using the onTransportFailure function 33 | request.onTransportFailure = function (errorMsg, request) { 34 | atmosphere.util.info(errorMsg); 35 | if (window.EventSource) { 36 | request.fallbackTransport = "sse"; 37 | } 38 | header.html($('

', { text: 'Atmosphere Chat. Default transport is WebSocket, fallback is ' + request.fallbackTransport })); 39 | }; 40 | 41 | request.onMessage = function (response) { 42 | 43 | var message = response.responseBody; 44 | try { 45 | var json = jQuery.parseJSON(message); 46 | } catch (e) { 47 | console.log('This doesn\'t look like a valid JSON: ', message.data); 48 | return; 49 | } 50 | 51 | if (!logged && myName) { 52 | logged = true; 53 | status.text(myName + ': ').css('color', 'blue'); 54 | input.removeAttr('disabled').focus(); 55 | } else { 56 | input.removeAttr('disabled'); 57 | if(json.msgType == 'chat'){ 58 | 59 | var me = json.author == author; 60 | var date = typeof(json.time) == 'string' ? parseInt(json.time) : json.time; 61 | addMessage(json.author, json.message, me ? 'blue' : 'black', new Date(date)); 62 | 63 | }else if(json.msgType == 'orderbook'){ 64 | updateOrderBook(json.askPrices, json.askVolumes, json.bidPrices, json.bidVolumes); 65 | } 66 | } 67 | }; 68 | 69 | request.onClose = function (response) { 70 | logged = false; 71 | }; 72 | 73 | request.onError = function (response) { 74 | content.html($('

', { text: 'Sorry, but there\'s some problem with your socket or the server is down' })); 75 | }; 76 | 77 | subSocket = socket.subscribe(request); 78 | 79 | input.keydown(function (e) { 80 | if (e.keyCode === 13) { 81 | var msg = $(this).val(); 82 | 83 | // First message is always the author's name 84 | if (author == null) { 85 | author = msg; 86 | } 87 | 88 | subSocket.push(atmosphere.util.stringifyJSON({ msgType: 'chat', author: author, message: msg })); 89 | $(this).val(''); 90 | 91 | input.attr('disabled', 'disabled'); 92 | if (myName === false) { 93 | myName = msg; 94 | } 95 | } 96 | }); 97 | 98 | function addMessage(author, message, color, datetime) { 99 | content.append('

' + author + ' @ ' + +(datetime.getHours() < 10 ? '0' + datetime.getHours() : datetime.getHours()) + ':' 100 | + (datetime.getMinutes() < 10 ? '0' + datetime.getMinutes() : datetime.getMinutes()) 101 | + ': ' + message + '

'); 102 | } 103 | 104 | function updateOrderBook(askPrices, askVolumes, bidPrices, bidVolumes) { 105 | var table = "
"; 106 | var size = askPrices.length; 107 | console.log('size='+size); 108 | var i; 109 | for(i = size - 1; i >= 0; i--){ 110 | table += ("
"+askPrices[i]+"
"+askVolumes[i]+"
"); 111 | } 112 | size = bidPrices.length; 113 | for(i = 0; i < size; i++){ 114 | table += ("
"+bidPrices[i]+"
"+bidVolumes[i]+"
"); 115 | } 116 | table += "
" 117 | orderbook.innerHTML = table; //"
"+price+""+volume+"
" 118 | } 119 | 120 | }); -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %d{yyyy-MM-dd HH:mm:ss} [%highlight(%-6level)] [%thread] %logger{} - %msg%n 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/main/resources/static/app.js: -------------------------------------------------------------------------------- 1 | var stompClient = null; 2 | 3 | var connectFlag = false; 4 | 5 | function setConnected(connected) { 6 | connectFlag = connected; 7 | $("#connect").prop("disabled", connected); 8 | $("#disconnect").prop("disabled", !connected); 9 | if (connected) { 10 | $("#conversation").show(); 11 | } 12 | else { 13 | $("#conversation").hide(); 14 | } 15 | $("#greetings").html(""); 16 | } 17 | 18 | function connect() { 19 | var socket = new SockJS('/ticks-websocket'); 20 | stompClient = Stomp.over(socket); 21 | stompClient.connect({}, function (frame) { 22 | setConnected(true); 23 | console.log('Connected: ' + frame); 24 | stompClient.subscribe('/topic/notifications', function (greeting) { 25 | showGreeting(JSON.parse(greeting.body).content); 26 | }); 27 | // stompClient.subscribe('/topic/ticks', function (tick) { 28 | // showTick(JSON.parse(tick.body)); 29 | // }); 30 | 31 | }); 32 | } 33 | 34 | function subscribe() { 35 | if (connectFlag) { 36 | var symbolToSubscribe = $("#symbol").val() 37 | console.log('Subscribing: ' + symbolToSubscribe); 38 | stompClient.subscribe('/topic/ticks/' + symbolToSubscribe, function (tick) { 39 | showTick(JSON.parse(tick.body)); 40 | }); 41 | } 42 | } 43 | 44 | function unsubscribe() { 45 | if (connectFlag) { 46 | var symbolToSubscribe = $("#symbol").val() 47 | console.log('UnSubscribing: ' + symbolToSubscribe); 48 | stompClient.unsubscribe('/topic/ticks/' + symbolToSubscribe); 49 | } 50 | } 51 | 52 | function disconnect() { 53 | if (stompClient !== null) { 54 | stompClient.disconnect(); 55 | } 56 | setConnected(false); 57 | console.log("Disconnected"); 58 | } 59 | 60 | function sendName() { 61 | stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()})); 62 | } 63 | 64 | function showGreeting(message) { 65 | $("#messages").append("" + message + ""); 66 | } 67 | 68 | function showTick(tick) { 69 | $("#messages").append("" + tick.timestamp + " " + tick.price + " " + tick.size +""); 70 | } 71 | 72 | 73 | $(function () { 74 | $("form").on('submit', function (e) { 75 | e.preventDefault(); 76 | }); 77 | $( "#connect" ).click(function() { connect(); }); 78 | $( "#disconnect" ).click(function() { disconnect(); }); 79 | $( "#send" ).click(function() { sendName(); }); 80 | $( "#subscribe" ).click(function() { subscribe(); }); 81 | $( "#unsubscribe" ).click(function() { unsubscribe(); }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello WebSocket 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 |
17 |
18 |
19 |
20 |
21 | 22 | 23 | 25 |
26 |
27 |
28 |
29 |
30 |
31 | 32 | 33 |
34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | 43 | 44 | 45 | 46 |
47 |
48 |
49 |
50 |
51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
Messages
61 |
62 |
63 |
64 | 65 | -------------------------------------------------------------------------------- /src/main/resources/static/main.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exchange-core/exchange-gateway-rest/edeb9f68512aba6b739d5ec2c6b6047448d48102/src/main/resources/static/main.css -------------------------------------------------------------------------------- /src/test/java/exchange/core2/rest/TestConfiguraton.java: -------------------------------------------------------------------------------- 1 | package exchange.core2.rest; 2 | 3 | import org.springframework.context.annotation.ComponentScan; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | @ComponentScan("exchange.core2.rest") 8 | public class TestConfiguraton { 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/exchange/core2/rest/support/StompTestClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Maksim Zheravin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package exchange.core2.rest.support; 17 | 18 | import exchange.core2.rest.model.api.StompApiTick; 19 | import exchange.core2.rest.model.api.StompOrderUpdate; 20 | import lombok.AccessLevel; 21 | import lombok.AllArgsConstructor; 22 | import lombok.RequiredArgsConstructor; 23 | import lombok.extern.slf4j.Slf4j; 24 | import org.hamcrest.core.Is; 25 | import org.springframework.messaging.converter.MappingJackson2MessageConverter; 26 | import org.springframework.messaging.simp.stomp.*; 27 | import org.springframework.web.socket.client.standard.StandardWebSocketClient; 28 | import org.springframework.web.socket.messaging.WebSocketStompClient; 29 | import org.springframework.web.socket.sockjs.client.SockJsClient; 30 | import org.springframework.web.socket.sockjs.client.WebSocketTransport; 31 | 32 | import java.lang.reflect.Type; 33 | import java.util.Collections; 34 | import java.util.List; 35 | import java.util.Map; 36 | import java.util.concurrent.BlockingDeque; 37 | import java.util.concurrent.ExecutionException; 38 | import java.util.concurrent.LinkedBlockingDeque; 39 | import java.util.concurrent.TimeUnit; 40 | import java.util.stream.Collectors; 41 | import java.util.stream.Stream; 42 | 43 | import static exchange.core2.rest.support.TestService.STOMP_TOPIC_ORDERS_PREFIX; 44 | import static exchange.core2.rest.support.TestService.STOMP_TOPIC_TICKS_PREFIX; 45 | 46 | 47 | @Slf4j 48 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 49 | public class StompTestClient { 50 | 51 | private BlockingDeque stompTicksQueue; 52 | private BlockingDeque stompOrderUpdateQueue; 53 | 54 | public static StompTestClient create(String symbolName, List uids, int randomServerPort) throws ExecutionException, InterruptedException { 55 | 56 | final BlockingDeque stompTicksQueue = new LinkedBlockingDeque<>(); 57 | final BlockingDeque stompOrderUpdatesQueue = new LinkedBlockingDeque<>(); 58 | 59 | final StandardWebSocketClient webSocketClient = new StandardWebSocketClient(); 60 | final SockJsClient sockJsClient = new SockJsClient(Collections.singletonList(new WebSocketTransport(webSocketClient))); 61 | final WebSocketStompClient stompClient = new WebSocketStompClient(sockJsClient); 62 | stompClient.setMessageConverter(new MappingJackson2MessageConverter()); 63 | final String url = "ws://localhost:" + randomServerPort + "/ticks-websocket"; 64 | 65 | final StompSessionHandler sessionHandler = new StompSessionHandlerAdapter() { 66 | @Override 67 | public void afterConnected(StompSession session, StompHeaders connectedHeaders) { 68 | session.subscribe(STOMP_TOPIC_TICKS_PREFIX + symbolName, this); 69 | uids.forEach(uid -> session.subscribe(STOMP_TOPIC_ORDERS_PREFIX + uid, new OrderUpdatesHandler(uid, stompOrderUpdatesQueue))); 70 | } 71 | 72 | @Override 73 | public void handleException(StompSession session, StompCommand command, StompHeaders headers, byte[] payload, Throwable exception) { 74 | log.warn("Stomp Error:", exception); 75 | } 76 | 77 | @Override 78 | public void handleTransportError(StompSession session, Throwable exception) { 79 | // super.handleTransportError(session, exception); 80 | log.warn("Stomp Transport Error: {}", exception.getMessage()); 81 | } 82 | 83 | @Override 84 | public Type getPayloadType(StompHeaders headers) { 85 | return StompApiTick.class; 86 | } 87 | 88 | @Override 89 | @SuppressWarnings("unchecked") 90 | public void handleFrame(StompHeaders stompHeaders, Object o) { 91 | stompTicksQueue.add((StompApiTick) o); 92 | } 93 | }; 94 | 95 | 96 | log.info("CONNECTING..."); 97 | final StompSession stompSession = stompClient.connect(url, sessionHandler).get(); 98 | log.info("CONNECTED: sessionId={}", stompSession.getSessionId()); 99 | 100 | return new StompTestClient(stompTicksQueue, stompOrderUpdatesQueue); 101 | } 102 | 103 | public StompApiTick pollTick() throws InterruptedException { 104 | return stompTicksQueue.poll(3, TimeUnit.SECONDS); 105 | } 106 | 107 | public boolean hasTicks() { 108 | return !stompTicksQueue.isEmpty(); 109 | } 110 | 111 | public Map> pollOrderUpdates(int num) { 112 | return Stream.generate( 113 | () -> { 114 | try { 115 | return stompOrderUpdateQueue.poll(3, TimeUnit.SECONDS); 116 | } catch (InterruptedException ex) { 117 | throw new RuntimeException(ex); 118 | } 119 | }) 120 | .limit(num) 121 | .collect(Collectors.groupingBy(StompOrderUpdate::getUid, Collectors.toList())); 122 | } 123 | 124 | @RequiredArgsConstructor 125 | private static class OrderUpdatesHandler extends StompSessionHandlerAdapter { 126 | private final long uid; 127 | private final BlockingDeque stompOrderUpdateQueue; 128 | 129 | @Override 130 | public Type getPayloadType(StompHeaders headers) { 131 | return StompOrderUpdate.class; 132 | } 133 | 134 | @Override 135 | @SuppressWarnings("unchecked") 136 | public void handleFrame(StompHeaders stompHeaders, Object o) { 137 | log.info("Handle StompOrderUpdate: {}", o); 138 | StompOrderUpdate orderUpdate = (StompOrderUpdate) o; 139 | org.hamcrest.MatcherAssert.assertThat(orderUpdate.getUid(), Is.is(uid)); 140 | stompOrderUpdateQueue.add(orderUpdate); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/test/java/exchange/core2/rest/support/TestSupport.java: -------------------------------------------------------------------------------- 1 | package exchange.core2.rest.support; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.autoconfigure.http.HttpMessageConverters; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.http.converter.HttpMessageConverter; 8 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 9 | import org.springframework.mock.http.MockHttpOutputMessage; 10 | import org.springframework.test.web.servlet.MockMvc; 11 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 12 | import org.springframework.web.context.WebApplicationContext; 13 | 14 | import javax.annotation.PostConstruct; 15 | import java.io.IOException; 16 | import java.nio.charset.Charset; 17 | 18 | @Slf4j 19 | public class TestSupport { 20 | 21 | protected MockMvc mockMvc; 22 | 23 | protected MediaType applicationJson = new MediaType( 24 | MediaType.APPLICATION_JSON.getType(), 25 | MediaType.APPLICATION_JSON.getSubtype(), 26 | Charset.forName("utf8") 27 | ); 28 | 29 | @PostConstruct 30 | public void setup() throws Exception { 31 | 32 | this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); 33 | 34 | // this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).addFilters((servletRequest, servletResponse, filterChain) -> { 35 | // 36 | // log.info("servletRequest={}", servletRequest); 37 | // log.info("servletResponse={}", servletResponse); 38 | // log.info("filterChain={}", filterChain); 39 | // 40 | // }).build(); 41 | } 42 | 43 | protected HttpMessageConverter mappingJackson2HttpMessageConverter; 44 | 45 | @Autowired 46 | protected WebApplicationContext webApplicationContext; 47 | 48 | @Autowired 49 | public void setConverters(HttpMessageConverters converters) { 50 | mappingJackson2HttpMessageConverter = converters.getConverters().stream() 51 | .filter(converter -> converter instanceof MappingJackson2HttpMessageConverter) 52 | .findFirst() 53 | .orElseThrow((() -> new IllegalStateException("the JSON message converter can not be null"))); 54 | } 55 | 56 | protected String json(Object object) throws IOException { 57 | MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage(); 58 | //noinspection unchecked 59 | mappingJackson2HttpMessageConverter.write(object, MediaType.APPLICATION_JSON, mockHttpOutputMessage); 60 | return mockHttpOutputMessage.getBodyAsString(); 61 | } 62 | 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/test/resources/it-local.properties: -------------------------------------------------------------------------------- 1 | 2 | server.port=8080 3 | --------------------------------------------------------------------------------