├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── docker-compose.yaml ├── lombok.config ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── gitbitex │ │ ├── AppConfiguration.java │ │ ├── AppProperties.java │ │ ├── Application.java │ │ ├── Bootstrap.java │ │ ├── demo │ │ └── CoinbaseTrader.java │ │ ├── enums │ │ ├── OrderSide.java │ │ ├── OrderStatus.java │ │ ├── OrderType.java │ │ └── TimeInForce.java │ │ ├── exception │ │ ├── ErrorCode.java │ │ └── ServiceException.java │ │ ├── feed │ │ ├── AuthHandshakeInterceptor.java │ │ ├── FeedMessageListener.java │ │ ├── FeedTextWebSocketHandler.java │ │ ├── SessionManager.java │ │ ├── WebsocketConfig.java │ │ └── message │ │ │ ├── AccountFeedMessage.java │ │ │ ├── CandleFeedMessage.java │ │ │ ├── L2SnapshotFeedMessage.java │ │ │ ├── L2UpdateFeedMessage.java │ │ │ ├── OrderDoneFeedMessage.java │ │ │ ├── OrderFeedMessage.java │ │ │ ├── OrderMatchFeedMessage.java │ │ │ ├── OrderOpenFeedMessage.java │ │ │ ├── OrderReceivedFeedMessage.java │ │ │ ├── PongFeedMessage.java │ │ │ ├── Request.java │ │ │ ├── SubscribeRequest.java │ │ │ ├── TickerFeedMessage.java │ │ │ └── UnsubscribeRequest.java │ │ ├── marketdata │ │ ├── AccountPersistenceThread.java │ │ ├── CandleMakerThread.java │ │ ├── OrderBookSnapshotThread.java │ │ ├── OrderPersistenceThread.java │ │ ├── TickerThread.java │ │ ├── TradePersistenceThread.java │ │ ├── entity │ │ │ ├── AccountEntity.java │ │ │ ├── AppEntity.java │ │ │ ├── Bill.java │ │ │ ├── Candle.java │ │ │ ├── Fill.java │ │ │ ├── OrderEntity.java │ │ │ ├── ProductEntity.java │ │ │ ├── Ticker.java │ │ │ ├── TradeEntity.java │ │ │ └── User.java │ │ ├── manager │ │ │ ├── AccountManager.java │ │ │ ├── OrderManager.java │ │ │ ├── ProductManager.java │ │ │ ├── TickerManager.java │ │ │ ├── TradeManager.java │ │ │ └── UserManager.java │ │ ├── orderbook │ │ │ ├── L2OrderBook.java │ │ │ ├── L2OrderBookChange.java │ │ │ ├── L3OrderBook.java │ │ │ ├── OrderBook.java │ │ │ └── OrderBookSnapshotManager.java │ │ ├── repository │ │ │ ├── AccountRepository.java │ │ │ ├── AppRepository.java │ │ │ ├── BillRepository.java │ │ │ ├── CandleRepository.java │ │ │ ├── FillRepository.java │ │ │ ├── OrderRepository.java │ │ │ ├── ProductRepository.java │ │ │ ├── TradeRepository.java │ │ │ └── UserRepository.java │ │ └── util │ │ │ └── DateUtil.java │ │ ├── matchingengine │ │ ├── Account.java │ │ ├── AccountBook.java │ │ ├── Depth.java │ │ ├── MatchingEngine.java │ │ ├── MatchingEngineLoader.java │ │ ├── MatchingEngineThread.java │ │ ├── MessageSender.java │ │ ├── Order.java │ │ ├── OrderBook.java │ │ ├── PriceGroupedOrderCollection.java │ │ ├── Product.java │ │ ├── ProductBook.java │ │ ├── Trade.java │ │ ├── command │ │ │ ├── CancelOrderCommand.java │ │ │ ├── Command.java │ │ │ ├── CommandDeserializer.java │ │ │ ├── CommandSerializer.java │ │ │ ├── CommandType.java │ │ │ ├── DepositCommand.java │ │ │ ├── MatchingEngineCommandProducer.java │ │ │ ├── PlaceOrderCommand.java │ │ │ └── PutProductCommand.java │ │ ├── message │ │ │ ├── AccountMessage.java │ │ │ ├── CommandEndMessage.java │ │ │ ├── CommandStartMessage.java │ │ │ ├── MatchingEngineMessageDeserializer.java │ │ │ ├── Message.java │ │ │ ├── MessageSerializer.java │ │ │ ├── MessageType.java │ │ │ ├── OrderBookMessage.java │ │ │ ├── OrderDoneMessage.java │ │ │ ├── OrderMatchMessage.java │ │ │ ├── OrderMessage.java │ │ │ ├── OrderOpenMessage.java │ │ │ ├── OrderReceivedMessage.java │ │ │ ├── ProductMessage.java │ │ │ ├── TickerMessage.java │ │ │ └── TradeMessage.java │ │ └── snapshot │ │ │ ├── EngineSnapshotManager.java │ │ │ ├── EngineState.java │ │ │ └── MatchingEngineSnapshotThread.java │ │ ├── middleware │ │ ├── kafka │ │ │ ├── KafkaConfig.java │ │ │ ├── KafkaConsumerThread.java │ │ │ └── KafkaProperties.java │ │ ├── mongodb │ │ │ ├── MongoDbConfig.java │ │ │ └── MongoProperties.java │ │ └── redis │ │ │ ├── RedisConfig.java │ │ │ └── RedisProperties.java │ │ ├── openapi │ │ ├── AuthInterceptor.java │ │ ├── ExceptionAdvise.java │ │ ├── WebConfig.java │ │ ├── controller │ │ │ ├── AccountController.java │ │ │ ├── AdminController.java │ │ │ ├── AppController.java │ │ │ ├── CodeController.java │ │ │ ├── ConfigController.java │ │ │ ├── HomeController.java │ │ │ ├── OrderController.java │ │ │ ├── ProductController.java │ │ │ └── UserController.java │ │ └── model │ │ │ ├── AccountDto.java │ │ │ ├── AppDto.java │ │ │ ├── ChangePasswordRequest.java │ │ │ ├── CreateAppRequest.java │ │ │ ├── ErrorMessage.java │ │ │ ├── NetworkDto.java │ │ │ ├── OrderDto.java │ │ │ ├── PagedList.java │ │ │ ├── PlaceOrderRequest.java │ │ │ ├── PlaceOrderResponse.java │ │ │ ├── ProductDto.java │ │ │ ├── Response.java │ │ │ ├── SendCodeRequest.java │ │ │ ├── SignInRequest.java │ │ │ ├── SignUpRequest.java │ │ │ ├── TokenDto.java │ │ │ ├── TotpUriDto.java │ │ │ ├── TradeDto.java │ │ │ ├── UpdateProfileRequest.java │ │ │ ├── UserDto.java │ │ │ └── WalletAddressDto.java │ │ ├── stripexecutor │ │ ├── StripedCallable.java │ │ ├── StripedExecutorService.java │ │ ├── StripedObject.java │ │ └── StripedRunnable.java │ │ └── wallet │ │ ├── Transaction.java │ │ └── TransactionSubmitter.java └── resources │ ├── application.properties │ ├── logback.xml │ └── static │ ├── assets │ ├── font │ │ ├── AtlasTypewriter-Bold-Web.woff2 │ │ ├── AtlasTypewriter-Medium-Web.woff2 │ │ ├── AtlasTypewriter-Regular-Web.woff2 │ │ ├── Graphik-Medium-Web.woff2 │ │ ├── Graphik-Regular-Web.woff2 │ │ ├── Graphik-RegularItalic-Web.woff2 │ │ ├── Graphik-Semibold-Web.woff2 │ │ ├── feather-webfont.woff │ │ ├── opensans-bold-webfont.woff2 │ │ └── opensans-regular-webfont.woff2 │ ├── image │ │ ├── HudexGlobal-200-04.png │ │ ├── HudexGlobal-200-07.png │ │ ├── HudexGlobal-400-04.png │ │ ├── HudexGlobal-400-07.png │ │ ├── favicon.ico │ │ ├── logo-light.svg │ │ └── logo.svg │ ├── script │ │ ├── app-01a1496904.js │ │ ├── app-ca958cac89.js │ │ ├── app.js │ │ ├── base-d808a76e37.js │ │ └── base.js │ └── style │ │ ├── app-159031df05.css │ │ ├── app.css │ │ ├── base-43955f37a9.css │ │ └── base.css │ └── index.html └── test └── resources └── application.properties /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-detectable=false 2 | *.css linguist-detectable=false 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | README.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | 35 | *.log 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | GitBitEX is an open source cryptocurrency exchange. 4 | 5 | ![微信图片_20220417184255](https://user-images.githubusercontent.com/4486680/163711067-8543457a-5b13-4131-bbd7-254860a580dc.png) 6 | 7 | 8 | ## Features 9 | - All in memory matching engine 10 | - Support distributed deployment (standby mode:only one matching engine is running at the same time) 11 | - Support 100000 orders per second (Intel(R) Core(TM) i7-10700K CPU @ 3.80GHz 3.79 GHz 32.0 GB RAM 1T SSD) 12 | - Support the replay of matching engine commands and ensure that each result is completely consistent 13 | 14 | ## Architecture 15 | ![gitbitex-architecture](https://github.com/gitbitex/gitbitex-new/assets/4486680/586238c9-a53d-4cb0-817b-c3ead4b96253) 16 | 17 | 18 | ## Quick Start 19 | ### Prerequisites 20 | - Install docker 21 | - Install jdk 22 | - Install maven 23 | - Update your **/etc/hosts** file. (required for mongodb-replica-set:https://github.com/UpSync-Dev/docker-compose-mongo-replica-set) 24 | ```text 25 | 127.0.0.1 mongo1 26 | 127.0.0.1 mongo2 27 | 127.0.0.1 mongo3 28 | ``` 29 | 30 | ### Run 31 | 32 | ```shell 33 | git clone https://github.com/gitbitex/gitbitex-new.git 34 | cd gitbitex-new 35 | docker compose up -d 36 | mvn clean package -Dmaven.test.skip=true 37 | cd target 38 | java -jar gitbitex-0.0.1-SNAPSHOT.jar 39 | #visit http://127.0.0.1/ 40 | ``` 41 | 42 | ## FAQ 43 | ### How do i add new product (currency pair)? 44 | ```shell 45 | curl -X PUT -H "Content-Type:application/json" http://127.0.0.1/api/admin/products -d '{"baseCurrency":"BTC","quoteCurrency":"USDT"}' 46 | ``` 47 | ### Does the project include blockchain wallets? 48 | No, you need to implement it yourself, and then connect to gitbiex. 49 | For example, after users recharge/withdraw, send **_DepositCommand_**/**_WithdrawalCommand_** to the matching engine 50 | 51 | ### How can I monitor the performance of the matching engine? 52 | Some prometheus metrics for measuring performance have been built into the project. 53 | You can visit http://127.0.0.1:7002/actuator/prometheus see.You can use Prometheus and grafana to monitor. 54 | The metrics are as follows: 55 | - **gbe_matching_engine_command_processed_total** : The number of commands processed by the matching engine. The greater the value change, the faster the processing. 56 | - **gbe_matching_engine_modified_object_created_total** : This value represents the number of objects that have modified,Wait to save to database. 57 | - **gbe_matching_engine_modified_object_saved_total** : The number of modified objects that have been saved. If the difference between this value and _gbe_matching_engine_modified_object_created_total_ is too large, it means that saving to disk is too slow. 58 | - **gbe_matching_engine_snapshot_taker_modified_objects_queue_size** : Objects that have not been written to the snapshot. This value reflects the performance of the snapshot thread. 59 | 60 | 61 | ### Where is the API document? 62 | http://127.0.0.1/swagger-ui/index.html#/ 63 | 64 | ### Web code 65 | https://github.com/gitbitex/gitbitex-web 66 | 67 | ## Contact Me 68 | - telegram:https://t.me/greensheng 69 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | redis: 5 | image: redis:7.0-alpine 6 | container_name: redis 7 | ports: 8 | - "6379:6379" 9 | mongo1: 10 | image: mongo:5 11 | container_name: mongo1 12 | command: [ "--replSet", "my-replica-set", "--bind_ip_all", "--port", "30001" ] 13 | ports: 14 | - 30001:30001 15 | healthcheck: 16 | test: test $$(echo "rs.initiate({_id:'my-replica-set',members:[{_id:0,host:\"mongo1:30001\"},{_id:1,host:\"mongo2:30002\"},{_id:2,host:\"mongo3:30003\"}]}).ok || rs.status().ok" | mongo --port 30001 --quiet) -eq 1 17 | interval: 10s 18 | mongo2: 19 | image: mongo:5 20 | container_name: mongo2 21 | command: [ "--replSet", "my-replica-set", "--bind_ip_all", "--port", "30002" ] 22 | ports: 23 | - 30002:30002 24 | mongo3: 25 | image: mongo:5 26 | container_name: mongo3 27 | command: [ "--replSet", "my-replica-set", "--bind_ip_all", "--port", "30003" ] 28 | ports: 29 | - 30003:30003 30 | mongo-express: 31 | image: mongo-express 32 | container_name: mongo-express 33 | restart: always 34 | ports: 35 | - 8082:8081 36 | environment: 37 | ME_CONFIG_MONGODB_URL: mongodb://mongo1:30001,mongo2:30002,mongo3:30003/?replicaSet=my-replica-set 38 | depends_on: 39 | - mongo1 40 | - mongo2 41 | - mongo3 42 | kafka: 43 | image: 'bitnami/kafka:3.4.0' 44 | container_name: kafka 45 | ports: 46 | - '19092:19092' 47 | environment: 48 | - KAFKA_ENABLE_KRAFT=yes 49 | - KAFKA_CFG_PROCESS_ROLES=broker,controller 50 | - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER 51 | - KAFKA_CFG_LISTENERS=PLAINTEXT://:19092,CONTROLLER://:2181 52 | - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT 53 | - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:19092 54 | - KAFKA_BROKER_ID=1 55 | - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@127.0.0.1:2181 56 | - ALLOW_PLAINTEXT_LISTENER=yes 57 | volumes: 58 | mongo1_data: 59 | mongo2_data: 60 | mongo3_data: 61 | mongo1_config: 62 | mongo2_config: 63 | mongo3_config: 64 | -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | lombok.log.fieldName=logger 2 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | jar 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.6.4 10 | 11 | 12 | com.gitbitex 13 | gitbitex 14 | 0.0.1-SNAPSHOT 15 | gitbitex 16 | GitBitEx is an open source cryptocurrency exchange 17 | 18 | 19 | 17 20 | 17 21 | 17 22 | UTF-8 23 | UTF-8 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-web 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-websocket 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-configuration-processor 38 | true 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-validation 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-actuator 47 | 48 | 49 | io.micrometer 50 | micrometer-registry-prometheus 51 | 52 | 53 | org.springdoc 54 | springdoc-openapi-ui 55 | 1.7.0 56 | 57 | 58 | org.projectlombok 59 | lombok 60 | 1.18.26 61 | 62 | 63 | javax.validation 64 | validation-api 65 | 2.0.1.Final 66 | 67 | 68 | org.apache.kafka 69 | kafka-clients 70 | 3.7.1 71 | 72 | 73 | com.alibaba 74 | fastjson 75 | 2.0.32 76 | 77 | 78 | org.redisson 79 | redisson 80 | 3.22.0 81 | 82 | 83 | org.mongodb 84 | mongodb-driver-sync 85 | 5.0.0 86 | 87 | 88 | 89 | org.mongodb 90 | mongodb-driver-core 91 | 5.0.0 92 | 93 | 94 | 95 | org.mongodb 96 | bson 97 | 5.0.0 98 | 99 | 100 | 101 | 102 | com.squareup.okhttp3 103 | okhttp 104 | 4.7.2 105 | 106 | 107 | com.google.guava 108 | guava 109 | 32.0.0-jre 110 | 111 | 112 | org.java-websocket 113 | Java-WebSocket 114 | 1.5.3 115 | 116 | 117 | org.springframework.boot 118 | spring-boot-starter-test 119 | test 120 | 121 | 122 | 123 | 124 | 125 | 126 | org.springframework.boot 127 | spring-boot-maven-plugin 128 | 129 | 130 | 131 | com.google.cloud.tools 132 | jib-maven-plugin 133 | 2.5.0 134 | 135 | 136 | com.gitbitex.Application 137 | 138 | -XX:ErrorFile=/logs/hs_err_pid_%p.log 139 | 140 | 141 | 142 | openjdk:14 143 | 144 | 145 | registry.hub.docker.com/greensheng/gitbitex:demo 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/AppConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex; 2 | 3 | 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | @RequiredArgsConstructor 11 | @EnableConfigurationProperties(AppProperties.class) 12 | @Slf4j 13 | public class AppConfiguration { 14 | 15 | 16 | } 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/AppProperties.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.validation.annotation.Validated; 7 | 8 | @ConfigurationProperties(prefix = "gbe") 9 | @Getter 10 | @Setter 11 | @Validated 12 | public class AppProperties { 13 | private String matchingEngineCommandTopic; 14 | private String matchingEngineMessageTopic; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/Application.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/enums/OrderSide.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.enums; 2 | 3 | public enum OrderSide { 4 | /** 5 | * buy 6 | */ 7 | BUY, 8 | /** 9 | * sell 10 | */ 11 | SELL 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/enums/OrderStatus.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.enums; 2 | 3 | public enum OrderStatus { 4 | REJECTED, 5 | RECEIVED, 6 | OPEN, 7 | CANCELLED, 8 | FILLED, 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/enums/OrderType.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.enums; 2 | 3 | public enum OrderType { 4 | /** 5 | * limit price order 6 | */ 7 | LIMIT, 8 | /** 9 | * market price order 10 | */ 11 | MARKET, 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/enums/TimeInForce.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.enums; 2 | 3 | public enum TimeInForce { 4 | GTC, 5 | GTT, 6 | IOC, 7 | FOK, 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/exception/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.exception; 2 | 3 | public enum ErrorCode { 4 | INSUFFICIENT_FUNDS, 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/exception/ServiceException.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.exception; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class ServiceException extends RuntimeException { 7 | private final ErrorCode code; 8 | 9 | public ServiceException(ErrorCode code) { 10 | super(code.name()); 11 | this.code = code; 12 | } 13 | 14 | public ServiceException(ErrorCode code, String message) { 15 | super(message); 16 | this.code = code; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/feed/AuthHandshakeInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.feed; 2 | 3 | import com.gitbitex.marketdata.entity.User; 4 | import com.gitbitex.marketdata.manager.UserManager; 5 | import lombok.RequiredArgsConstructor; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.springframework.http.server.ServerHttpRequest; 8 | import org.springframework.http.server.ServerHttpResponse; 9 | import org.springframework.http.server.ServletServerHttpRequest; 10 | import org.springframework.stereotype.Component; 11 | import org.springframework.web.socket.WebSocketHandler; 12 | import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor; 13 | 14 | import javax.servlet.http.Cookie; 15 | import javax.servlet.http.HttpServletRequest; 16 | import java.util.Map; 17 | 18 | @Component 19 | @RequiredArgsConstructor 20 | public class AuthHandshakeInterceptor extends HttpSessionHandshakeInterceptor { 21 | private final UserManager userManager; 22 | 23 | @Override 24 | public boolean beforeHandshake(@NotNull ServerHttpRequest request, @NotNull ServerHttpResponse response, 25 | @NotNull WebSocketHandler wsHandler, 26 | @NotNull Map attributes) throws Exception { 27 | HttpServletRequest httpServletRequest = ((ServletServerHttpRequest) request).getServletRequest(); 28 | String accessToken = getAccessToken(httpServletRequest); 29 | if (accessToken != null) { 30 | User user = userManager.getUserByAccessToken(accessToken); 31 | if (user != null) { 32 | attributes.put("CURRENT_USER_ID", user.getId()); 33 | } 34 | } 35 | return true; 36 | } 37 | 38 | private String getAccessToken(HttpServletRequest request) { 39 | String tokenKey = "accessToken"; 40 | String token = request.getParameter(tokenKey); 41 | if (token == null && request.getCookies() != null) { 42 | for (Cookie cookie : request.getCookies()) { 43 | if (cookie.getName().equals(tokenKey)) { 44 | token = cookie.getValue(); 45 | } 46 | } 47 | } 48 | return token; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/feed/FeedTextWebSocketHandler.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.feed; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.gitbitex.feed.message.Request; 5 | import com.gitbitex.feed.message.SubscribeRequest; 6 | import com.gitbitex.feed.message.UnsubscribeRequest; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.SneakyThrows; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.socket.CloseStatus; 11 | import org.springframework.web.socket.TextMessage; 12 | import org.springframework.web.socket.WebSocketSession; 13 | import org.springframework.web.socket.handler.TextWebSocketHandler; 14 | 15 | @Component 16 | @RequiredArgsConstructor 17 | public class FeedTextWebSocketHandler extends TextWebSocketHandler { 18 | private final SessionManager sessionManager; 19 | 20 | @Override 21 | public void afterConnectionEstablished(WebSocketSession session) throws Exception { 22 | } 23 | 24 | @Override 25 | public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { 26 | sessionManager.removeSession(session); 27 | } 28 | 29 | @Override 30 | @SneakyThrows 31 | public void handleTextMessage(WebSocketSession session, TextMessage message) { 32 | Request request = JSON.parseObject(message.getPayload(), Request.class); 33 | 34 | switch (request.getType()) { 35 | case "subscribe": { 36 | SubscribeRequest subscribeRequest = JSON.parseObject(message.getPayload(), SubscribeRequest.class); 37 | sessionManager.subOrUnSub(session, subscribeRequest.getProductIds(), subscribeRequest.getCurrencyIds(), 38 | subscribeRequest.getChannels(), true); 39 | break; 40 | } 41 | case "unsubscribe": { 42 | UnsubscribeRequest unsubscribeRequest = JSON.parseObject(message.getPayload(), 43 | UnsubscribeRequest.class); 44 | sessionManager.subOrUnSub(session, unsubscribeRequest.getProductIds(), 45 | unsubscribeRequest.getCurrencyIds(), 46 | unsubscribeRequest.getChannels(), false); 47 | break; 48 | } 49 | case "ping": 50 | sessionManager.sendPong(session); 51 | break; 52 | default: 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/feed/WebsocketConfig.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.feed; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocket; 6 | import org.springframework.web.socket.config.annotation.WebSocketConfigurer; 7 | import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; 8 | 9 | @Configuration 10 | @EnableWebSocket 11 | @RequiredArgsConstructor 12 | public class WebsocketConfig implements WebSocketConfigurer { 13 | private final FeedTextWebSocketHandler myHandler; 14 | private final AuthHandshakeInterceptor authHandshakeInterceptor; 15 | 16 | @Override 17 | public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { 18 | registry.addHandler(myHandler, "/ws") 19 | .addInterceptors(authHandshakeInterceptor) 20 | .setAllowedOrigins("*"); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/feed/message/AccountFeedMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.feed.message; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class AccountFeedMessage { 9 | private String type = "funds"; 10 | private String productId; 11 | private String userId; 12 | private String currencyCode; 13 | private String available; 14 | private String hold; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/feed/message/CandleFeedMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.feed.message; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class CandleFeedMessage { 9 | private String type = "candle"; 10 | private String productId; 11 | private long sequence; 12 | private int granularity; 13 | private long time; 14 | private String open; 15 | private String close; 16 | private String high; 17 | private String low; 18 | private String volume; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/feed/message/L2SnapshotFeedMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.feed.message; 2 | 3 | import com.gitbitex.marketdata.orderbook.L2OrderBook; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.springframework.beans.BeanUtils; 7 | 8 | @Getter 9 | @Setter 10 | public class L2SnapshotFeedMessage extends L2OrderBook { 11 | private String type = "snapshot"; 12 | 13 | public L2SnapshotFeedMessage(L2OrderBook snapshot) { 14 | BeanUtils.copyProperties(snapshot, this); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/feed/message/L2UpdateFeedMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.feed.message; 2 | 3 | import com.gitbitex.marketdata.orderbook.L2OrderBookChange; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import java.util.Date; 8 | import java.util.List; 9 | 10 | /** 11 | * { 12 | * "type": "l2update", 13 | * "product_id": "BTC-USD", 14 | * "time": "2019-08-14T20:42:27.265Z", 15 | * "changes": [ 16 | * [ 17 | * "buy", 18 | * "10101.80000000", 19 | * "0.162567" 20 | * ] 21 | * ] 22 | * } 23 | */ 24 | @Getter 25 | @Setter 26 | public class L2UpdateFeedMessage { 27 | private String type = "l2update"; 28 | private String productId; 29 | private String time; 30 | private List changes; 31 | 32 | public L2UpdateFeedMessage() { 33 | } 34 | 35 | public L2UpdateFeedMessage(String productId, List l2OrderBookChanges) { 36 | this.productId = productId; 37 | this.time = new Date().toInstant().toString(); 38 | this.changes = l2OrderBookChanges; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/feed/message/OrderDoneFeedMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.feed.message; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | /** 7 | * { 8 | * "type": "done", 9 | * "time": "2014-11-07T08:19:27.028459Z", 10 | * "product_id": "BTC-USD", 11 | * "sequence": 10, 12 | * "price": "200.2", 13 | * "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", 14 | * "reason": "filled", // or "canceled" 15 | * "side": "sell", 16 | * "remaining_size": "0" 17 | * } 18 | */ 19 | @Getter 20 | @Setter 21 | public class OrderDoneFeedMessage { 22 | private String type = "done"; 23 | private String productId; 24 | private long sequence; 25 | private String orderId; 26 | private String remainingSize; 27 | private String price; 28 | private String side; 29 | private String reason; 30 | private String time; 31 | } 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/feed/message/OrderFeedMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.feed.message; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class OrderFeedMessage { 9 | private String type = "order"; 10 | private String productId; 11 | private String userId; 12 | private String sequence; 13 | private String id; 14 | private String price; 15 | private String size; 16 | private String funds; 17 | private String side; 18 | private String orderType; 19 | private String createdAt; 20 | private String fillFees; 21 | private String filledSize; 22 | private String executedValue; 23 | private String status; 24 | private boolean settled; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/feed/message/OrderMatchFeedMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.feed.message; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class OrderMatchFeedMessage { 9 | private String type = "match"; 10 | private String productId; 11 | private long tradeId; 12 | private long sequence; 13 | private String takerOrderId; 14 | private String makerOrderId; 15 | private String time; 16 | private String size; 17 | private String price; 18 | private String side; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/feed/message/OrderOpenFeedMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.feed.message; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class OrderOpenFeedMessage { 9 | private String type = "open"; 10 | private String productId; 11 | private long sequence; 12 | private String time; 13 | private String orderId; 14 | private String remainingSize; 15 | private String price; 16 | private String side; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/feed/message/OrderReceivedFeedMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.feed.message; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class OrderReceivedFeedMessage { 9 | private String type = "received"; 10 | private String time; 11 | private String productId; 12 | private long sequence; 13 | private String orderId; 14 | private String size; 15 | private String price; 16 | private String funds; 17 | private String side; 18 | private String orderType; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/feed/message/PongFeedMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.feed.message; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class PongFeedMessage { 9 | private String type; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/feed/message/Request.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.feed.message; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class Request { 9 | private String type; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/feed/message/SubscribeRequest.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.feed.message; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.util.List; 7 | 8 | @Getter 9 | @Setter 10 | public class SubscribeRequest extends Request { 11 | private List productIds; 12 | private List currencyIds; 13 | private List channels; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/feed/message/TickerFeedMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.feed.message; 2 | 3 | import com.gitbitex.marketdata.entity.Ticker; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | public class TickerFeedMessage { 10 | private String type = "ticker"; 11 | private String productId; 12 | private long tradeId; 13 | private long sequence; 14 | private String time; 15 | private String price; 16 | private String side; 17 | private String lastSize; 18 | private String open24h; 19 | private String close24h; 20 | private String high24h; 21 | private String low24h; 22 | private String volume24h; 23 | private String volume30d; 24 | 25 | public TickerFeedMessage() { 26 | } 27 | 28 | public TickerFeedMessage(Ticker ticker) { 29 | this.setProductId(ticker.getProductId()); 30 | this.setTradeId(ticker.getTradeId()); 31 | this.setSequence(ticker.getSequence()); 32 | this.setTime(ticker.getTime().toInstant().toString()); 33 | this.setPrice(ticker.getPrice().stripTrailingZeros().toPlainString()); 34 | this.setSide(ticker.getSide().name().toLowerCase()); 35 | this.setLastSize(ticker.getLastSize().stripTrailingZeros().toPlainString()); 36 | this.setClose24h(ticker.getClose24h().stripTrailingZeros().toPlainString()); 37 | this.setOpen24h(ticker.getOpen24h().stripTrailingZeros().toPlainString()); 38 | this.setHigh24h(ticker.getHigh24h().stripTrailingZeros().toPlainString()); 39 | this.setLow24h(ticker.getLow24h().stripTrailingZeros().toPlainString()); 40 | this.setVolume24h(ticker.getVolume24h().stripTrailingZeros().toPlainString()); 41 | this.setVolume30d(ticker.getVolume30d().stripTrailingZeros().toPlainString()); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/feed/message/UnsubscribeRequest.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.feed.message; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.util.List; 7 | 8 | @Getter 9 | @Setter 10 | public class UnsubscribeRequest extends Request { 11 | private List productIds; 12 | private List currencyIds; 13 | private List channels; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/AccountPersistenceThread.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.gitbitex.AppProperties; 5 | import com.gitbitex.marketdata.entity.AccountEntity; 6 | import com.gitbitex.marketdata.manager.AccountManager; 7 | import com.gitbitex.matchingengine.Account; 8 | import com.gitbitex.matchingengine.message.AccountMessage; 9 | import com.gitbitex.matchingengine.message.Message; 10 | import com.gitbitex.middleware.kafka.KafkaConsumerThread; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.apache.kafka.clients.consumer.ConsumerRebalanceListener; 13 | import org.apache.kafka.clients.consumer.KafkaConsumer; 14 | import org.apache.kafka.common.TopicPartition; 15 | import org.redisson.api.RTopic; 16 | import org.redisson.api.RedissonClient; 17 | import org.redisson.client.codec.StringCodec; 18 | 19 | import java.time.Duration; 20 | import java.util.Collection; 21 | import java.util.Collections; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | 25 | @Slf4j 26 | public class AccountPersistenceThread extends KafkaConsumerThread implements ConsumerRebalanceListener { 27 | private final AccountManager accountManager; 28 | private final AppProperties appProperties; 29 | private final RTopic accountTopic; 30 | 31 | public AccountPersistenceThread(KafkaConsumer consumer, AccountManager accountManager, 32 | RedissonClient redissonClient, 33 | AppProperties appProperties) { 34 | super(consumer, logger); 35 | this.accountManager = accountManager; 36 | this.appProperties = appProperties; 37 | this.accountTopic = redissonClient.getTopic("account", StringCodec.INSTANCE); 38 | } 39 | 40 | @Override 41 | public void onPartitionsRevoked(Collection collection) { 42 | 43 | } 44 | 45 | @Override 46 | public void onPartitionsAssigned(Collection collection) { 47 | 48 | } 49 | 50 | @Override 51 | protected void doSubscribe() { 52 | consumer.subscribe(Collections.singletonList(appProperties.getMatchingEngineMessageTopic()), this); 53 | } 54 | 55 | @Override 56 | protected void doPoll() { 57 | var records = consumer.poll(Duration.ofSeconds(5)); 58 | Map accounts = new HashMap<>(); 59 | records.forEach(x -> { 60 | Message message = x.value(); 61 | if (message instanceof AccountMessage accountMessage) { 62 | AccountEntity accountEntity = accountEntity(accountMessage); 63 | accounts.put(accountEntity.getId(), accountEntity); 64 | accountTopic.publishAsync(JSON.toJSONString(accountMessage)); 65 | } 66 | }); 67 | accountManager.saveAll(accounts.values()); 68 | 69 | consumer.commitAsync(); 70 | } 71 | 72 | private AccountEntity accountEntity(AccountMessage message) { 73 | Account account = message.getAccount(); 74 | AccountEntity accountEntity = new AccountEntity(); 75 | accountEntity.setId(account.getUserId() + "-" + account.getCurrency()); 76 | accountEntity.setUserId(account.getUserId()); 77 | accountEntity.setCurrency(account.getCurrency()); 78 | accountEntity.setAvailable(account.getAvailable()); 79 | accountEntity.setHold(account.getHold()); 80 | return accountEntity; 81 | } 82 | } 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/CandleMakerThread.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata; 2 | 3 | import com.gitbitex.AppProperties; 4 | import com.gitbitex.marketdata.entity.Candle; 5 | import com.gitbitex.marketdata.repository.CandleRepository; 6 | import com.gitbitex.marketdata.util.DateUtil; 7 | import com.gitbitex.matchingengine.Trade; 8 | import com.gitbitex.matchingengine.message.Message; 9 | import com.gitbitex.matchingengine.message.TradeMessage; 10 | import com.gitbitex.middleware.kafka.KafkaConsumerThread; 11 | import lombok.SneakyThrows; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.apache.kafka.clients.consumer.ConsumerRebalanceListener; 14 | import org.apache.kafka.clients.consumer.KafkaConsumer; 15 | import org.apache.kafka.common.TopicPartition; 16 | 17 | import java.time.Duration; 18 | import java.time.ZoneId; 19 | import java.time.ZonedDateTime; 20 | import java.time.temporal.ChronoField; 21 | import java.util.Collection; 22 | import java.util.Collections; 23 | import java.util.LinkedHashMap; 24 | 25 | /** 26 | * My job is to produce candles 27 | */ 28 | @Slf4j 29 | public class CandleMakerThread extends KafkaConsumerThread implements ConsumerRebalanceListener { 30 | private static final int[] GRANULARITY_ARR = new int[]{1, 5, 15, 30, 60, 360, 1440}; 31 | private final CandleRepository candleRepository; 32 | private final AppProperties appProperties; 33 | 34 | public CandleMakerThread(KafkaConsumer consumer, CandleRepository candleRepository, 35 | AppProperties appProperties) { 36 | super(consumer, logger); 37 | this.candleRepository = candleRepository; 38 | this.appProperties = appProperties; 39 | } 40 | 41 | @Override 42 | public void onPartitionsRevoked(Collection partitions) { 43 | for (TopicPartition partition : partitions) { 44 | logger.info("partition revoked: {}", partition.toString()); 45 | } 46 | } 47 | 48 | @Override 49 | public void onPartitionsAssigned(Collection partitions) { 50 | for (TopicPartition partition : partitions) { 51 | logger.info("partition assigned: {}", partition.toString()); 52 | } 53 | } 54 | 55 | @Override 56 | protected void doSubscribe() { 57 | consumer.subscribe(Collections.singletonList(appProperties.getMatchingEngineMessageTopic()), this); 58 | } 59 | 60 | @Override 61 | @SneakyThrows 62 | protected void doPoll() { 63 | var records = consumer.poll(Duration.ofSeconds(5)); 64 | if (records.isEmpty()) { 65 | return; 66 | } 67 | 68 | LinkedHashMap candles = new LinkedHashMap<>(); 69 | records.forEach(x -> { 70 | Message message = x.value(); 71 | if (message instanceof TradeMessage) { 72 | Trade trade = ((TradeMessage) message).getTrade(); 73 | for (int granularity : GRANULARITY_ARR) { 74 | long time = DateUtil.round(ZonedDateTime.ofInstant(trade.getTime().toInstant(), ZoneId.systemDefault()), 75 | ChronoField.MINUTE_OF_DAY, granularity).toEpochSecond(); 76 | String candleId = trade.getProductId() + "-" + time + "-" + granularity; 77 | Candle candle = candles.get(candleId); 78 | if (candle == null) { 79 | candle = candleRepository.findById(candleId); 80 | } 81 | 82 | if (candle == null) { 83 | candle = new Candle(); 84 | candle.setId(candleId); 85 | candle.setProductId(trade.getProductId()); 86 | candle.setGranularity(granularity); 87 | candle.setTime(time); 88 | candle.setProductId(trade.getProductId()); 89 | candle.setOpen(trade.getPrice()); 90 | candle.setClose(trade.getPrice()); 91 | candle.setLow(trade.getPrice()); 92 | candle.setHigh(trade.getPrice()); 93 | candle.setVolume(trade.getSize()); 94 | candle.setTradeId(trade.getSequence()); 95 | } else { 96 | if (candle.getTradeId() >= trade.getSequence()) { 97 | //logger.warn("ignore trade: {}",trade.getTradeId()); 98 | continue; 99 | } else if (candle.getTradeId() + 1 != trade.getSequence()) { 100 | throw new RuntimeException( 101 | "out of order sequence: " + " " + (candle.getTradeId()) + " " + trade.getSequence()); 102 | } 103 | candle.setClose(trade.getPrice()); 104 | candle.setLow(candle.getLow().min(trade.getPrice())); 105 | candle.setHigh(candle.getLow().max(trade.getPrice())); 106 | candle.setVolume(candle.getVolume().add(trade.getSize())); 107 | candle.setTradeId(trade.getSequence()); 108 | } 109 | 110 | candles.put(candle.getId(), candle); 111 | } 112 | } 113 | }); 114 | 115 | if (!candles.isEmpty()) { 116 | long t1 = System.currentTimeMillis(); 117 | candleRepository.saveAll(candles.values()); 118 | logger.info("saved {} candle(s) ({}ms)", candles.size(), System.currentTimeMillis() - t1); 119 | } 120 | 121 | consumer.commitSync(); 122 | } 123 | 124 | 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/OrderBookSnapshotThread.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata; 2 | 3 | import com.gitbitex.AppProperties; 4 | import com.gitbitex.enums.OrderStatus; 5 | import com.gitbitex.marketdata.orderbook.L2OrderBook; 6 | import com.gitbitex.marketdata.orderbook.OrderBook; 7 | import com.gitbitex.marketdata.orderbook.OrderBookSnapshotManager; 8 | import com.gitbitex.matchingengine.Order; 9 | import com.gitbitex.matchingengine.Product; 10 | import com.gitbitex.matchingengine.message.Message; 11 | import com.gitbitex.matchingengine.message.OrderMessage; 12 | import com.gitbitex.matchingengine.snapshot.EngineSnapshotManager; 13 | import com.gitbitex.matchingengine.snapshot.EngineState; 14 | import com.gitbitex.middleware.kafka.KafkaConsumerThread; 15 | import lombok.extern.slf4j.Slf4j; 16 | import org.apache.kafka.clients.consumer.ConsumerRebalanceListener; 17 | import org.apache.kafka.clients.consumer.KafkaConsumer; 18 | import org.apache.kafka.common.TopicPartition; 19 | 20 | import java.time.Duration; 21 | import java.util.Collection; 22 | import java.util.Collections; 23 | import java.util.concurrent.ConcurrentHashMap; 24 | 25 | @Slf4j 26 | public class OrderBookSnapshotThread extends KafkaConsumerThread implements ConsumerRebalanceListener { 27 | private final ConcurrentHashMap orderBooks = new ConcurrentHashMap<>(); 28 | private final ConcurrentHashMap l2OrderBooks = new ConcurrentHashMap<>(); 29 | private final OrderBookSnapshotManager orderBookSnapshotManager; 30 | private final EngineSnapshotManager stateStore; 31 | private final AppProperties appProperties; 32 | 33 | public OrderBookSnapshotThread(KafkaConsumer consumer, 34 | OrderBookSnapshotManager orderBookSnapshotManager, 35 | EngineSnapshotManager engineSnapshotManager, 36 | AppProperties appProperties) { 37 | super(consumer, logger); 38 | this.orderBookSnapshotManager = orderBookSnapshotManager; 39 | this.stateStore = engineSnapshotManager; 40 | this.appProperties = appProperties; 41 | } 42 | 43 | @Override 44 | public void onPartitionsRevoked(Collection collection) { 45 | 46 | } 47 | 48 | @Override 49 | public void onPartitionsAssigned(Collection partitions) { 50 | // restore order book from engine state 51 | stateStore.runInSession(session -> { 52 | EngineState engineState = stateStore.getEngineState(session); 53 | if (engineState != null && engineState.getMessageOffset() != null) { 54 | this.consumer.seek(partitions.iterator().next(), engineState.getMessageOffset() + 1); 55 | } 56 | 57 | // restore order books 58 | for (Product product : this.stateStore.getProducts(session)) { 59 | orderBooks.remove(product.getId()); 60 | for (Order order : stateStore.getOrders(session, product.getId())) { 61 | OrderBook orderBook = getOrderBook(product.getId()); 62 | orderBook.addOrder(order); 63 | } 64 | } 65 | }); 66 | } 67 | 68 | @Override 69 | protected void doSubscribe() { 70 | consumer.subscribe(Collections.singletonList(appProperties.getMatchingEngineMessageTopic()), this); 71 | } 72 | 73 | @Override 74 | protected void doPoll() { 75 | var records = consumer.poll(Duration.ofSeconds(5)); 76 | records.forEach(x -> { 77 | Message message = x.value(); 78 | if (message instanceof OrderMessage orderMessage) { 79 | Order order = orderMessage.getOrder(); 80 | OrderBook orderBook = getOrderBook(order.getProductId()); 81 | if (order.getStatus() == OrderStatus.OPEN) { 82 | orderBook.addOrder(order); 83 | } else { 84 | orderBook.removeOrder(order); 85 | } 86 | orderBook.setSequence(orderMessage.getOrderBookSequence()); 87 | } 88 | }); 89 | 90 | orderBooks.entrySet().parallelStream().forEach(e -> { 91 | String productId=e.getKey(); 92 | OrderBook orderBook= e.getValue(); 93 | L2OrderBook l2OrderBook = l2OrderBooks.get(productId); 94 | if (l2OrderBook == null || 95 | orderBook.getSequence() - l2OrderBook.getSequence() > 1000 || 96 | System.currentTimeMillis() - l2OrderBook.getTime() > 1000) { 97 | takeL2OrderBookSnapshot(orderBook); 98 | } 99 | }); 100 | } 101 | 102 | private OrderBook getOrderBook(String productId) { 103 | OrderBook orderBook = orderBooks.get(productId); 104 | if (orderBook == null) { 105 | orderBook = new OrderBook(productId); 106 | orderBooks.put(productId, orderBook); 107 | } 108 | return orderBook; 109 | } 110 | 111 | private void takeL2OrderBookSnapshot(OrderBook orderBook) { 112 | logger.info("taking level2 order book snapshot: sequence={}", orderBook.getSequence()); 113 | L2OrderBook l2OrderBook = new L2OrderBook(orderBook, 25); 114 | l2OrderBooks.put(orderBook.getProductId(), l2OrderBook); 115 | orderBookSnapshotManager.saveL2BatchOrderBook(l2OrderBook); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/OrderPersistenceThread.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.gitbitex.AppProperties; 5 | import com.gitbitex.marketdata.entity.OrderEntity; 6 | import com.gitbitex.marketdata.manager.OrderManager; 7 | import com.gitbitex.matchingengine.Order; 8 | import com.gitbitex.matchingengine.message.Message; 9 | import com.gitbitex.matchingengine.message.OrderMessage; 10 | import com.gitbitex.middleware.kafka.KafkaConsumerThread; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.apache.kafka.clients.consumer.ConsumerRebalanceListener; 13 | import org.apache.kafka.clients.consumer.KafkaConsumer; 14 | import org.apache.kafka.common.TopicPartition; 15 | import org.redisson.api.RTopic; 16 | import org.redisson.api.RedissonClient; 17 | import org.redisson.client.codec.StringCodec; 18 | 19 | import java.time.Duration; 20 | import java.util.*; 21 | 22 | @Slf4j 23 | public class OrderPersistenceThread extends KafkaConsumerThread implements ConsumerRebalanceListener { 24 | private final AppProperties appProperties; 25 | private final OrderManager orderManager; 26 | private final RTopic orderTopic; 27 | 28 | public OrderPersistenceThread(KafkaConsumer kafkaConsumer, OrderManager orderManager, 29 | RedissonClient redissonClient, 30 | AppProperties appProperties) { 31 | super(kafkaConsumer, logger); 32 | this.appProperties = appProperties; 33 | this.orderManager = orderManager; 34 | this.orderTopic = redissonClient.getTopic("order", StringCodec.INSTANCE); 35 | } 36 | 37 | @Override 38 | public void onPartitionsRevoked(Collection collection) { 39 | 40 | } 41 | 42 | @Override 43 | public void onPartitionsAssigned(Collection collection) { 44 | 45 | } 46 | 47 | @Override 48 | protected void doSubscribe() { 49 | consumer.subscribe(Collections.singletonList(appProperties.getMatchingEngineMessageTopic()), this); 50 | } 51 | 52 | @Override 53 | protected void doPoll() { 54 | var records = consumer.poll(Duration.ofSeconds(5)); 55 | Map orders = new HashMap<>(); 56 | records.forEach(x -> { 57 | Message message = x.value(); 58 | if (message instanceof OrderMessage orderMessage) { 59 | OrderEntity orderEntity = orderEntity(orderMessage); 60 | orders.put(orderEntity.getId(), orderEntity); 61 | orderTopic.publishAsync(JSON.toJSONString(orderMessage)); 62 | } 63 | }); 64 | orderManager.saveAll(orders.values()); 65 | 66 | consumer.commitAsync(); 67 | } 68 | 69 | private OrderEntity orderEntity(OrderMessage message) { 70 | Order order = message.getOrder(); 71 | OrderEntity orderEntity = new OrderEntity(); 72 | orderEntity.setId(order.getId()); 73 | orderEntity.setSequence(order.getSequence()); 74 | orderEntity.setProductId(order.getProductId()); 75 | orderEntity.setUserId(order.getUserId()); 76 | orderEntity.setStatus(order.getStatus()); 77 | orderEntity.setPrice(order.getPrice()); 78 | orderEntity.setSize(order.getSize()); 79 | orderEntity.setFunds(order.getFunds()); 80 | orderEntity.setClientOid(order.getClientOid()); 81 | orderEntity.setSide(order.getSide()); 82 | orderEntity.setType(order.getType()); 83 | orderEntity.setTime(order.getTime()); 84 | orderEntity.setCreatedAt(new Date()); 85 | orderEntity.setFilledSize(order.getSize().subtract(order.getRemainingSize())); 86 | orderEntity.setExecutedValue(order.getFunds().subtract(order.getRemainingFunds())); 87 | return orderEntity; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/TickerThread.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata; 2 | 3 | import com.gitbitex.AppProperties; 4 | import com.gitbitex.marketdata.entity.Ticker; 5 | import com.gitbitex.marketdata.manager.TickerManager; 6 | import com.gitbitex.marketdata.util.DateUtil; 7 | import com.gitbitex.matchingengine.Trade; 8 | import com.gitbitex.matchingengine.message.Message; 9 | import com.gitbitex.matchingengine.message.TradeMessage; 10 | import com.gitbitex.middleware.kafka.KafkaConsumerThread; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.apache.kafka.clients.consumer.ConsumerRebalanceListener; 13 | import org.apache.kafka.clients.consumer.KafkaConsumer; 14 | import org.apache.kafka.common.TopicPartition; 15 | 16 | import java.time.Duration; 17 | import java.time.ZoneId; 18 | import java.time.ZonedDateTime; 19 | import java.time.temporal.ChronoField; 20 | import java.util.Collection; 21 | import java.util.Collections; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | 25 | @Slf4j 26 | public class TickerThread extends KafkaConsumerThread implements ConsumerRebalanceListener { 27 | private final AppProperties appProperties; 28 | private final TickerManager tickerManager; 29 | private final Map tickerByProductId = new HashMap<>(); 30 | 31 | public TickerThread(KafkaConsumer consumer, TickerManager tickerManager, 32 | AppProperties appProperties) { 33 | super(consumer, logger); 34 | this.tickerManager = tickerManager; 35 | this.appProperties = appProperties; 36 | } 37 | 38 | @Override 39 | public void onPartitionsRevoked(Collection partitions) { 40 | for (TopicPartition partition : partitions) { 41 | logger.info("partition revoked: {}", partition.toString()); 42 | } 43 | } 44 | 45 | @Override 46 | public void onPartitionsAssigned(Collection partitions) { 47 | for (TopicPartition partition : partitions) { 48 | logger.info("partition assigned: {}", partition.toString()); 49 | } 50 | } 51 | 52 | @Override 53 | protected void doSubscribe() { 54 | consumer.subscribe(Collections.singletonList(appProperties.getMatchingEngineMessageTopic()), this); 55 | } 56 | 57 | @Override 58 | protected void doPoll() { 59 | var records = consumer.poll(Duration.ofSeconds(5)); 60 | if (records.isEmpty()) { 61 | return; 62 | } 63 | 64 | records.forEach(x -> { 65 | Message message = x.value(); 66 | if (message instanceof TradeMessage) { 67 | refreshTicker(((TradeMessage) message).getTrade()); 68 | } 69 | }); 70 | 71 | consumer.commitSync(); 72 | } 73 | 74 | public void refreshTicker(Trade trade) { 75 | Ticker ticker = tickerByProductId.get(trade.getProductId()); 76 | if (ticker == null) { 77 | ticker = tickerManager.getTicker(trade.getProductId()); 78 | } 79 | if (ticker != null) { 80 | long diff = trade.getSequence() - ticker.getTradeId(); 81 | if (diff <= 0) { 82 | return; 83 | } else if (diff > 1) { 84 | throw new RuntimeException("tradeId is discontinuous"); 85 | } 86 | } 87 | 88 | if (ticker == null) { 89 | ticker = new Ticker(); 90 | ticker.setProductId(trade.getProductId()); 91 | } 92 | 93 | long time24h = DateUtil.round(ZonedDateTime.ofInstant(trade.getTime().toInstant(), ZoneId.systemDefault()), 94 | ChronoField.MINUTE_OF_DAY, 24 * 60).toEpochSecond(); 95 | long time30d = DateUtil.round(ZonedDateTime.ofInstant(trade.getTime().toInstant(), ZoneId.systemDefault()), 96 | ChronoField.MINUTE_OF_DAY, 24 * 60 * 30).toEpochSecond(); 97 | 98 | if (ticker.getTime24h() == null || ticker.getTime24h() != time24h) { 99 | ticker.setTime24h(time24h); 100 | ticker.setOpen24h(trade.getPrice()); 101 | ticker.setClose24h(trade.getPrice()); 102 | ticker.setHigh24h(trade.getPrice()); 103 | ticker.setLow24h(trade.getPrice()); 104 | ticker.setVolume24h(trade.getSize()); 105 | } else { 106 | ticker.setClose24h(trade.getPrice()); 107 | ticker.setHigh24h(ticker.getHigh24h().max(trade.getPrice())); 108 | ticker.setVolume24h(ticker.getVolume24h().add(trade.getSize())); 109 | } 110 | if (ticker.getTime30d() == null || ticker.getTime30d() != time30d) { 111 | ticker.setTime30d(time30d); 112 | ticker.setOpen30d(trade.getPrice()); 113 | ticker.setClose30d(trade.getPrice()); 114 | ticker.setHigh30d(trade.getPrice()); 115 | ticker.setLow30d(trade.getPrice()); 116 | ticker.setVolume30d(trade.getSize()); 117 | } else { 118 | ticker.setClose30d(trade.getPrice()); 119 | ticker.setHigh30d(ticker.getHigh30d().max(trade.getPrice())); 120 | ticker.setVolume30d(ticker.getVolume30d().add(trade.getSize())); 121 | } 122 | ticker.setLastSize(trade.getSize()); 123 | ticker.setTime(trade.getTime()); 124 | ticker.setPrice(trade.getPrice()); 125 | ticker.setSide(trade.getSide()); 126 | ticker.setTradeId(trade.getSequence()); 127 | tickerByProductId.put(trade.getProductId(), ticker); 128 | 129 | tickerManager.saveTicker(ticker); 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/TradePersistenceThread.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.gitbitex.AppProperties; 5 | import com.gitbitex.marketdata.entity.TradeEntity; 6 | import com.gitbitex.marketdata.manager.TradeManager; 7 | import com.gitbitex.matchingengine.Trade; 8 | import com.gitbitex.matchingengine.message.Message; 9 | import com.gitbitex.matchingengine.message.TradeMessage; 10 | import com.gitbitex.middleware.kafka.KafkaConsumerThread; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.apache.kafka.clients.consumer.ConsumerRebalanceListener; 13 | import org.apache.kafka.clients.consumer.KafkaConsumer; 14 | import org.apache.kafka.common.TopicPartition; 15 | import org.redisson.api.RTopic; 16 | import org.redisson.api.RedissonClient; 17 | import org.redisson.client.codec.StringCodec; 18 | 19 | import java.time.Duration; 20 | import java.util.Collection; 21 | import java.util.Collections; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | 25 | @Slf4j 26 | public class TradePersistenceThread extends KafkaConsumerThread implements ConsumerRebalanceListener { 27 | private final TradeManager tradeManager; 28 | private final AppProperties appProperties; 29 | private final RTopic tradeTopic; 30 | 31 | public TradePersistenceThread(KafkaConsumer consumer, TradeManager tradeManager, 32 | RedissonClient redissonClient, 33 | AppProperties appProperties) { 34 | super(consumer, logger); 35 | this.tradeManager = tradeManager; 36 | this.appProperties = appProperties; 37 | this.tradeTopic = redissonClient.getTopic("trade", StringCodec.INSTANCE); 38 | } 39 | 40 | @Override 41 | public void onPartitionsRevoked(Collection collection) { 42 | 43 | } 44 | 45 | @Override 46 | public void onPartitionsAssigned(Collection collection) { 47 | 48 | } 49 | 50 | @Override 51 | protected void doSubscribe() { 52 | consumer.subscribe(Collections.singletonList(appProperties.getMatchingEngineMessageTopic()), this); 53 | } 54 | 55 | @Override 56 | protected void doPoll() { 57 | var records = consumer.poll(Duration.ofSeconds(5)); 58 | Map trades = new HashMap<>(); 59 | records.forEach(x -> { 60 | Message message = x.value(); 61 | if (message instanceof TradeMessage tradeMessage) { 62 | TradeEntity tradeEntity = tradeEntity(tradeMessage); 63 | trades.put(tradeEntity.getId(), tradeEntity); 64 | tradeTopic.publishAsync(JSON.toJSONString(tradeMessage)); 65 | } 66 | }); 67 | tradeManager.saveAll(trades.values()); 68 | 69 | consumer.commitAsync(); 70 | } 71 | 72 | private TradeEntity tradeEntity(TradeMessage message) { 73 | Trade trade = message.getTrade(); 74 | TradeEntity tradeEntity = new TradeEntity(); 75 | tradeEntity.setId(trade.getProductId() + "-" + trade.getSequence()); 76 | tradeEntity.setSequence(trade.getSequence()); 77 | tradeEntity.setTime(trade.getTime()); 78 | tradeEntity.setSize(trade.getSize()); 79 | tradeEntity.setPrice(trade.getPrice()); 80 | tradeEntity.setProductId(trade.getProductId()); 81 | tradeEntity.setMakerOrderId(trade.getMakerOrderId()); 82 | tradeEntity.setTakerOrderId(trade.getTakerOrderId()); 83 | tradeEntity.setSide(trade.getSide()); 84 | return tradeEntity; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/entity/AccountEntity.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.math.BigDecimal; 7 | import java.util.Date; 8 | 9 | @Getter 10 | @Setter 11 | public class AccountEntity { 12 | private String id; 13 | private Date createdAt; 14 | private Date updatedAt; 15 | private String userId; 16 | private String currency; 17 | private BigDecimal hold; 18 | private BigDecimal available; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/entity/AppEntity.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.util.Date; 7 | 8 | @Getter 9 | @Setter 10 | public class AppEntity { 11 | private String id; 12 | private Date createdAt; 13 | private Date updatedAt; 14 | private String userId; 15 | private String name; 16 | private String accessKey; 17 | private String secretKey; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/entity/Bill.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.math.BigDecimal; 7 | import java.util.Date; 8 | 9 | @Getter 10 | @Setter 11 | public class Bill { 12 | private String id; 13 | private Date createdAt; 14 | private Date updatedAt; 15 | private String userId; 16 | private String currency; 17 | private BigDecimal holdIncrement; 18 | private BigDecimal availableIncrement; 19 | private String type; 20 | private boolean settled; 21 | private String notes; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/entity/Candle.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.math.BigDecimal; 7 | import java.util.Date; 8 | 9 | @Getter 10 | @Setter 11 | public class Candle { 12 | private String id; 13 | private Date createdAt; 14 | private Date updatedAt; 15 | private String productId; 16 | private int granularity; 17 | private long time; 18 | private BigDecimal open; 19 | private BigDecimal close; 20 | private BigDecimal high; 21 | private BigDecimal low; 22 | private BigDecimal volume; 23 | private long tradeId; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/entity/Fill.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.entity; 2 | 3 | import com.gitbitex.enums.OrderSide; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import java.math.BigDecimal; 8 | import java.util.Date; 9 | 10 | @Getter 11 | @Setter 12 | public class Fill { 13 | private String id; 14 | private Date createdAt; 15 | private Date updatedAt; 16 | private String orderId; 17 | private long tradeId; 18 | private String productId; 19 | private String userId; 20 | private BigDecimal size; 21 | private BigDecimal price; 22 | private BigDecimal funds; 23 | private BigDecimal fee; 24 | private String liquidity; 25 | private boolean settled; 26 | private OrderSide side; 27 | private boolean done; 28 | private String doneReason; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/entity/OrderEntity.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.entity; 2 | 3 | import com.gitbitex.enums.OrderSide; 4 | import com.gitbitex.enums.OrderStatus; 5 | import com.gitbitex.enums.OrderType; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | 9 | import java.math.BigDecimal; 10 | import java.util.Date; 11 | 12 | @Getter 13 | @Setter 14 | public class OrderEntity { 15 | private String id; 16 | private Date createdAt; 17 | private Date updatedAt; 18 | private long sequence; 19 | private String productId; 20 | private String userId; 21 | private String clientOid; 22 | private Date time; 23 | private BigDecimal size; 24 | private BigDecimal funds; 25 | private BigDecimal filledSize; 26 | private BigDecimal executedValue; 27 | private BigDecimal price; 28 | private BigDecimal fillFees; 29 | private OrderType type; 30 | private OrderSide side; 31 | private OrderStatus status; 32 | /** 33 | * Time in force policies provide guarantees about the lifetime of an order. There are four policies: good till 34 | * canceled GTC, good till time GTT, immediate or cancel IOC, and fill or kill FOK. 35 | *

36 | * GTC Good till canceled orders remain open on the book until canceled. This is the default behavior if no policy 37 | * is specified. 38 | *

39 | * GTT Good till time orders remain open on the book until canceled or the allotted cancel_after is depleted on 40 | * the matching engine. GTT orders are guaranteed to cancel before any other order is processed after the 41 | * cancel_after timestamp which is returned by the API. A day is considered 24 hours. 42 | *

43 | * IOC Immediate or cancel orders instantly cancel the remaining size of the limit order instead of opening it 44 | * on the book. 45 | *

46 | * FOK Fill or kill orders are rejected if the entire size cannot be matched. 47 | */ 48 | private String timeInForce; 49 | private boolean settled; 50 | /** 51 | * The post-only flag indicates that the order should only make liquidity. If any part of the order results in 52 | * taking liquidity, the order will be rejected and no part of it will execute. 53 | */ 54 | private boolean postOnly; 55 | 56 | } 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/entity/ProductEntity.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.math.BigDecimal; 7 | import java.util.Date; 8 | 9 | @Getter 10 | @Setter 11 | public class ProductEntity { 12 | private String id; 13 | private Date createdAt; 14 | private Date updatedAt; 15 | private String baseCurrency; 16 | private String quoteCurrency; 17 | private BigDecimal baseMinSize; 18 | private BigDecimal baseMaxSize; 19 | private BigDecimal quoteMinSize; 20 | private BigDecimal quoteMaxSize; 21 | private int baseScale; 22 | private int quoteScale; 23 | private float quoteIncrement; 24 | private float takerFeeRate; 25 | private float makerFeeRate; 26 | private int displayOrder; 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/entity/Ticker.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.entity; 2 | 3 | import com.gitbitex.enums.OrderSide; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import java.math.BigDecimal; 8 | import java.util.Date; 9 | 10 | @Getter 11 | @Setter 12 | public class Ticker { 13 | private String productId; 14 | private long tradeId; 15 | private long sequence; 16 | private Date time; 17 | private BigDecimal price; 18 | private OrderSide side; 19 | private BigDecimal lastSize; 20 | private Long time24h; 21 | private BigDecimal open24h; 22 | private BigDecimal close24h; 23 | private BigDecimal high24h; 24 | private BigDecimal low24h; 25 | private BigDecimal volume24h; 26 | private Long time30d; 27 | private BigDecimal open30d; 28 | private BigDecimal close30d; 29 | private BigDecimal high30d; 30 | private BigDecimal low30d; 31 | private BigDecimal volume30d; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/entity/TradeEntity.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.entity; 2 | 3 | import com.gitbitex.enums.OrderSide; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import java.math.BigDecimal; 8 | import java.util.Date; 9 | 10 | @Getter 11 | @Setter 12 | public class TradeEntity { 13 | private String id; 14 | private Date createdAt; 15 | private Date updatedAt; 16 | private long sequence; 17 | private String productId; 18 | private String takerOrderId; 19 | private String makerOrderId; 20 | private BigDecimal price; 21 | private BigDecimal size; 22 | private OrderSide side; 23 | private Date time; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/entity/User.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.math.BigDecimal; 7 | import java.util.Date; 8 | 9 | @Getter 10 | @Setter 11 | public class User { 12 | private String id; 13 | private Date createdAt; 14 | private Date updatedAt; 15 | private String email; 16 | private String passwordHash; 17 | private String passwordSalt; 18 | private String twoStepVerificationType; 19 | private BigDecimal gotpSecret; 20 | private String nickName; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/manager/AccountManager.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.manager; 2 | 3 | import com.gitbitex.marketdata.entity.AccountEntity; 4 | import com.gitbitex.marketdata.repository.AccountRepository; 5 | import com.gitbitex.marketdata.repository.BillRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.Collection; 11 | import java.util.List; 12 | 13 | @Slf4j 14 | @Component 15 | @RequiredArgsConstructor 16 | public class AccountManager { 17 | private final AccountRepository accountRepository; 18 | private final BillRepository billRepository; 19 | 20 | public List getAccounts(String userId) { 21 | return accountRepository.findAccountsByUserId(userId); 22 | } 23 | 24 | public void saveAll(Collection accounts) { 25 | if (accounts.isEmpty()) { 26 | return; 27 | } 28 | 29 | long t1 = System.currentTimeMillis(); 30 | accountRepository.saveAll(accounts); 31 | logger.info("saved {} account(s) ({}ms)", accounts.size(), System.currentTimeMillis() - t1); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/manager/OrderManager.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.manager; 2 | 3 | import com.gitbitex.marketdata.entity.OrderEntity; 4 | import com.gitbitex.marketdata.repository.FillRepository; 5 | import com.gitbitex.marketdata.repository.OrderRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.Collection; 11 | 12 | @Component 13 | @Slf4j 14 | @RequiredArgsConstructor 15 | public class OrderManager { 16 | private final OrderRepository orderRepository; 17 | private final FillRepository fillRepository; 18 | 19 | public void saveAll(Collection orders) { 20 | if (orders.isEmpty()) { 21 | return; 22 | } 23 | long t1 = System.currentTimeMillis(); 24 | orderRepository.saveAll(orders); 25 | logger.info("saved {} order(s) ({}ms)", orders.size(), System.currentTimeMillis() - t1); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/manager/ProductManager.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.manager; 2 | 3 | import com.gitbitex.marketdata.repository.ProductRepository; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | @RequiredArgsConstructor 9 | public class ProductManager { 10 | private final ProductRepository productRepository; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/manager/TickerManager.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.manager; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.gitbitex.marketdata.entity.Ticker; 5 | import org.redisson.api.RTopic; 6 | import org.redisson.api.RedissonClient; 7 | import org.redisson.client.codec.StringCodec; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | public class TickerManager { 12 | private final RedissonClient redissonClient; 13 | private final RTopic tickerTopic; 14 | 15 | public TickerManager(RedissonClient redissonClient) { 16 | this.redissonClient = redissonClient; 17 | this.tickerTopic = redissonClient.getTopic("ticker", StringCodec.INSTANCE); 18 | } 19 | 20 | public Ticker getTicker(String productId) { 21 | Object val = redissonClient.getBucket(keyForTicker(productId), StringCodec.INSTANCE).get(); 22 | if (val == null) { 23 | return null; 24 | } 25 | return JSON.parseObject(val.toString(), Ticker.class); 26 | } 27 | 28 | public void saveTicker(Ticker ticker) { 29 | String value = JSON.toJSONString(ticker); 30 | redissonClient.getBucket(keyForTicker(ticker.getProductId()), StringCodec.INSTANCE).set(value); 31 | tickerTopic.publishAsync(value); 32 | } 33 | 34 | private String keyForTicker(String productId) { 35 | return productId + ".ticker"; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/manager/TradeManager.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.manager; 2 | 3 | import com.gitbitex.marketdata.entity.TradeEntity; 4 | import com.gitbitex.marketdata.repository.TradeRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.Collection; 10 | 11 | @Component 12 | @RequiredArgsConstructor 13 | @Slf4j 14 | public class TradeManager { 15 | private final TradeRepository tradeRepository; 16 | 17 | public void saveAll(Collection trades) { 18 | if (trades.isEmpty()) { 19 | return; 20 | } 21 | 22 | long t1 = System.currentTimeMillis(); 23 | tradeRepository.saveAll(trades); 24 | logger.info("saved {} trade(s) ({}ms)", trades.size(), System.currentTimeMillis() - t1); 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/manager/UserManager.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.manager; 2 | 3 | import com.gitbitex.marketdata.entity.User; 4 | import com.gitbitex.marketdata.repository.UserRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import org.redisson.api.RedissonClient; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.util.DigestUtils; 9 | 10 | import java.nio.charset.StandardCharsets; 11 | import java.util.Date; 12 | import java.util.UUID; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | @Component 16 | @RequiredArgsConstructor 17 | public class UserManager { 18 | private final UserRepository userRepository; 19 | private final RedissonClient redissonClient; 20 | private final AccountManager accountManager; 21 | 22 | public User createUser(String email, String password) { 23 | // check if the email address is already registered 24 | User user = userRepository.findByEmail(email); 25 | if (user != null) { 26 | throw new RuntimeException("duplicate email address"); 27 | } 28 | 29 | // create new user 30 | user = new User(); 31 | user.setId(UUID.randomUUID().toString()); 32 | user.setEmail(email); 33 | user.setPasswordSalt(UUID.randomUUID().toString()); 34 | user.setPasswordHash(encryptPassword(password, user.getPasswordSalt())); 35 | userRepository.save(user); 36 | return user; 37 | } 38 | 39 | public String generateAccessToken(User user, String sessionId) { 40 | String accessToken = user.getId() + ":" + sessionId + ":" + generateAccessTokenSecret(user); 41 | redissonClient.getBucket(redisKeyForAccessToken(accessToken)) 42 | .set(new Date().toString(), 14, TimeUnit.DAYS); 43 | return accessToken; 44 | } 45 | 46 | public void deleteAccessToken(String accessToken) { 47 | redissonClient.getBucket(redisKeyForAccessToken(accessToken)).delete(); 48 | } 49 | 50 | public User getUserByAccessToken(String accessToken) { 51 | if (accessToken == null) { 52 | return null; 53 | } 54 | 55 | Object val = redissonClient.getBucket(redisKeyForAccessToken(accessToken)).get(); 56 | if (val == null) { 57 | return null; 58 | } 59 | 60 | String[] parts = accessToken.split(":"); 61 | if (parts.length != 3) { 62 | return null; 63 | } 64 | 65 | String userId = parts[0]; 66 | User user = userRepository.findByUserId(userId); 67 | if (user == null) { 68 | return null; 69 | } 70 | 71 | // check secret 72 | if (!parts[2].equals(generateAccessTokenSecret(user))) { 73 | return null; 74 | } 75 | return user; 76 | } 77 | 78 | public User getUser(String email, String password) { 79 | User user = userRepository.findByEmail(email); 80 | if (user == null) { 81 | return null; 82 | } 83 | 84 | if (user.getPasswordHash().equals(encryptPassword(password, user.getPasswordSalt()))) { 85 | return user; 86 | } 87 | return null; 88 | } 89 | 90 | private String encryptPassword(String password, String saltKey) { 91 | return DigestUtils.md5DigestAsHex((password + saltKey).getBytes(StandardCharsets.UTF_8)); 92 | } 93 | 94 | private String generateAccessTokenSecret(User user) { 95 | String key = user.getId() + user.getEmail() + user.getPasswordHash(); 96 | return DigestUtils.md5DigestAsHex(key.getBytes(StandardCharsets.UTF_8)); 97 | } 98 | 99 | private String redisKeyForAccessToken(String accessToken) { 100 | return "token." + accessToken; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/orderbook/L2OrderBook.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.orderbook; 2 | 3 | import com.gitbitex.enums.OrderSide; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.springframework.lang.Nullable; 7 | 8 | import java.math.BigDecimal; 9 | import java.util.ArrayList; 10 | import java.util.LinkedHashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.stream.Collectors; 14 | 15 | @Getter 16 | @Setter 17 | public class L2OrderBook { 18 | private String productId; 19 | private long sequence; 20 | private long time; 21 | private List asks = new ArrayList<>(); 22 | private List bids = new ArrayList<>(); 23 | 24 | public L2OrderBook() { 25 | } 26 | 27 | public L2OrderBook(OrderBook orderBook, int depth) { 28 | this.productId = orderBook.getProductId(); 29 | this.sequence = orderBook.getSequence(); 30 | this.time = System.currentTimeMillis(); 31 | this.asks = orderBook.getAsks().entrySet().stream() 32 | .limit(depth) 33 | .map(x -> new Line(x.getKey(), x.getValue().getRemainingSize(), x.getValue().size())) 34 | .collect(Collectors.toList()); 35 | this.bids = orderBook.getBids().entrySet().stream() 36 | .limit(depth) 37 | .map(x -> new Line(x.getKey(), x.getValue().getRemainingSize(), x.getValue().size())) 38 | .collect(Collectors.toList()); 39 | } 40 | 41 | @Nullable 42 | public List diff(L2OrderBook newL2OrderBook) { 43 | if (newL2OrderBook.getSequence() < this.sequence) { 44 | throw new RuntimeException("new l2 order book is too old"); 45 | } 46 | if (newL2OrderBook.getSequence() == this.sequence) { 47 | return null; 48 | } 49 | 50 | List changes = new ArrayList<>(); 51 | changes.addAll(diff(OrderSide.SELL, this.getAsks(), newL2OrderBook.getAsks())); 52 | changes.addAll(diff(OrderSide.BUY, this.getBids(), newL2OrderBook.getBids())); 53 | return changes; 54 | } 55 | 56 | private List diff(OrderSide side, List oldLines, List newLines) { 57 | Map oldLineByPrice = new LinkedHashMap<>(); 58 | Map newLineByPrice = new LinkedHashMap<>(); 59 | for (Line oldLine : oldLines) { 60 | oldLineByPrice.put(oldLine.getPrice(), oldLine); 61 | } 62 | for (Line newLine : newLines) { 63 | newLineByPrice.put(newLine.getPrice(), newLine); 64 | } 65 | 66 | List changes = new ArrayList<>(); 67 | oldLineByPrice.forEach(((oldPrice, oldLine) -> { 68 | Line newLine = newLineByPrice.get(oldPrice); 69 | if (newLine == null) { 70 | L2OrderBookChange change = new L2OrderBookChange(side.name().toLowerCase(), oldPrice, "0"); 71 | changes.add(change); 72 | } else if (!newLine.getSize().equals(oldLine.getSize())) { 73 | L2OrderBookChange change = new L2OrderBookChange(side.name().toLowerCase(), oldPrice, 74 | newLine.getSize()); 75 | changes.add(change); 76 | } 77 | })); 78 | newLineByPrice.forEach((newPrice, newLine) -> { 79 | Line oldLine = oldLineByPrice.get(newPrice); 80 | if (oldLine == null) { 81 | L2OrderBookChange change = new L2OrderBookChange(side.name().toLowerCase(), newPrice, 82 | newLine.getSize()); 83 | changes.add(change); 84 | } 85 | }); 86 | return changes; 87 | } 88 | 89 | public static class Line extends ArrayList { 90 | public Line() { 91 | } 92 | 93 | public Line(BigDecimal price, BigDecimal totalSize, int orderCount) { 94 | add(price); 95 | add(totalSize); 96 | add(orderCount); 97 | } 98 | 99 | public String getPrice() { 100 | return this.get(0).toString(); 101 | } 102 | 103 | public String getSize() { 104 | return this.get(1).toString(); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/orderbook/L2OrderBookChange.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.orderbook; 2 | 3 | import java.util.ArrayList; 4 | 5 | public class L2OrderBookChange extends ArrayList { 6 | public L2OrderBookChange() { 7 | } 8 | 9 | public L2OrderBookChange(String side, String price, String size) { 10 | this.add(side); 11 | this.add(price); 12 | this.add(size); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/orderbook/L3OrderBook.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.orderbook; 2 | 3 | import com.gitbitex.matchingengine.Order; 4 | import com.gitbitex.matchingengine.OrderBook; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | 12 | @Getter 13 | @Setter 14 | public class L3OrderBook { 15 | private String productId; 16 | private long sequence; 17 | private long tradeId; 18 | private long time; 19 | private List asks; 20 | private List bids; 21 | 22 | public L3OrderBook() { 23 | } 24 | 25 | public L3OrderBook(OrderBook orderBook) { 26 | this.productId = orderBook.getProductId(); 27 | this.tradeId = orderBook.getTradeSequence(); 28 | this.time = System.currentTimeMillis(); 29 | this.asks = orderBook.getAsks().values().stream() 30 | .flatMap(x -> x.values().stream()) 31 | .map(Line::new) 32 | .collect(Collectors.toList()); 33 | this.bids = orderBook.getBids().values().stream() 34 | .flatMap(x -> x.values().stream()) 35 | .map(Line::new) 36 | .collect(Collectors.toList()); 37 | } 38 | 39 | public static class Line extends ArrayList { 40 | public Line() { 41 | } 42 | 43 | public Line(Order order) { 44 | this.add(order.getId()); 45 | this.add(order.getPrice().stripTrailingZeros().toPlainString()); 46 | this.add(order.getRemainingSize().stripTrailingZeros().toPlainString()); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/orderbook/OrderBook.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.orderbook; 2 | 3 | import com.gitbitex.enums.OrderSide; 4 | import com.gitbitex.matchingengine.Depth; 5 | import com.gitbitex.matchingengine.Order; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | 9 | import java.util.Comparator; 10 | 11 | @Getter 12 | public class OrderBook { 13 | private final String productId; 14 | private final Depth asks = new Depth(Comparator.naturalOrder()); 15 | private final Depth bids = new Depth(Comparator.reverseOrder()); 16 | @Setter 17 | private long sequence; 18 | 19 | public OrderBook(String productId) { 20 | this.productId = productId; 21 | } 22 | 23 | public OrderBook(String productId, long sequence) { 24 | this.productId = productId; 25 | this.sequence = sequence; 26 | } 27 | 28 | public void addOrder(Order order) { 29 | var depth = order.getSide() == OrderSide.BUY ? bids : asks; 30 | depth.addOrder(order); 31 | } 32 | 33 | public void removeOrder(Order order) { 34 | var depth = order.getSide() == OrderSide.BUY ? bids : asks; 35 | depth.removeOrder(order); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/orderbook/OrderBookSnapshotManager.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.orderbook; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.redisson.api.RTopic; 6 | import org.redisson.api.RedissonClient; 7 | import org.redisson.client.codec.StringCodec; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | @Slf4j 12 | public class OrderBookSnapshotManager { 13 | private final RedissonClient redissonClient; 14 | private final RTopic l2BatchNotifyTopic; 15 | 16 | public OrderBookSnapshotManager(RedissonClient redissonClient) { 17 | this.redissonClient = redissonClient; 18 | this.l2BatchNotifyTopic = redissonClient.getTopic("l2_batch", StringCodec.INSTANCE); 19 | } 20 | 21 | public void saveL3OrderBook(L3OrderBook l3OrderBook) { 22 | redissonClient.getBucket(keyForL3(l3OrderBook.getProductId()), StringCodec.INSTANCE).set( 23 | JSON.toJSONString(l3OrderBook)); 24 | } 25 | 26 | public L3OrderBook getL3OrderBook(String productId) { 27 | Object o = redissonClient.getBucket(keyForL3(productId), StringCodec.INSTANCE).get(); 28 | if (o == null) { 29 | return null; 30 | } 31 | return JSON.parseObject(o.toString(), L3OrderBook.class); 32 | } 33 | 34 | public void saveL2OrderBook(L2OrderBook l2OrderBook) { 35 | redissonClient.getBucket(keyForL2(l2OrderBook.getProductId()), StringCodec.INSTANCE).setAsync( 36 | JSON.toJSONString(l2OrderBook)); 37 | } 38 | 39 | public L2OrderBook getL2OrderBook(String productId) { 40 | Object o = redissonClient.getBucket(keyForL2(productId), StringCodec.INSTANCE).get(); 41 | if (o == null) { 42 | return null; 43 | } 44 | return JSON.parseObject(o.toString(), L2OrderBook.class); 45 | } 46 | 47 | public void saveL2BatchOrderBook(L2OrderBook l2OrderBook) { 48 | String data = JSON.toJSONString(l2OrderBook); 49 | redissonClient.getBucket(keyForL2Batch(l2OrderBook.getProductId()), StringCodec.INSTANCE) 50 | .setAsync(data); 51 | l2BatchNotifyTopic.publishAsync(data); 52 | } 53 | 54 | public L2OrderBook getL2BatchOrderBook(String productId) { 55 | Object o = redissonClient.getBucket(keyForL2Batch(productId), StringCodec.INSTANCE).get(); 56 | if (o == null) { 57 | return null; 58 | } 59 | return JSON.parseObject(o.toString(), L2OrderBook.class); 60 | } 61 | 62 | public L2OrderBook getL1OrderBook(String productId) { 63 | Object o = redissonClient.getBucket(keyForL1(productId), StringCodec.INSTANCE).get(); 64 | if (o == null) { 65 | return null; 66 | } 67 | return JSON.parseObject(o.toString(), L2OrderBook.class); 68 | } 69 | 70 | private String keyForL1(String productId) { 71 | return productId + ".l1_order_book"; 72 | } 73 | 74 | private String keyForL2(String productId) { 75 | return productId + ".l2_order_book"; 76 | } 77 | 78 | private String keyForL2Batch(String productId) { 79 | return productId + ".l2_batch_order_book"; 80 | } 81 | 82 | private String keyForL3(String productId) { 83 | return productId + ".l3_order_book"; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/repository/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.repository; 2 | 3 | import com.gitbitex.marketdata.entity.AccountEntity; 4 | import com.mongodb.client.MongoCollection; 5 | import com.mongodb.client.MongoDatabase; 6 | import com.mongodb.client.model.*; 7 | import org.bson.conversions.Bson; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Collection; 12 | import java.util.List; 13 | 14 | @Component 15 | public class AccountRepository { 16 | private final MongoCollection collection; 17 | 18 | public AccountRepository(MongoDatabase database) { 19 | this.collection = database.getCollection(AccountEntity.class.getSimpleName().toLowerCase(), AccountEntity.class); 20 | this.collection.createIndex(Indexes.descending("userId", "currency"), new IndexOptions().unique(true)); 21 | } 22 | 23 | public List findAccountsByUserId(String userId) { 24 | return collection 25 | .find(Filters.eq("userId", userId)) 26 | .into(new ArrayList<>()); 27 | } 28 | 29 | public void saveAll(Collection accounts) { 30 | List> writeModels = new ArrayList<>(); 31 | for (AccountEntity item : accounts) { 32 | Bson filter = Filters.eq("userId", item.getUserId()); 33 | filter = Filters.and(filter, Filters.eq("currency", item.getCurrency())); 34 | WriteModel writeModel = new ReplaceOneModel<>(filter, item, new ReplaceOptions().upsert(true)); 35 | writeModels.add(writeModel); 36 | } 37 | collection.bulkWrite(writeModels, new BulkWriteOptions().ordered(false)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/repository/AppRepository.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.repository; 2 | 3 | import com.gitbitex.marketdata.entity.AppEntity; 4 | import org.springframework.stereotype.Component; 5 | 6 | import java.util.List; 7 | 8 | @Component 9 | public class AppRepository { 10 | 11 | public List findByUserId(String userId) { 12 | return null; 13 | } 14 | 15 | public AppEntity findByAppId(String appId) { 16 | return null; 17 | } 18 | 19 | public void save(AppEntity appEntity) { 20 | 21 | } 22 | 23 | public void deleteById(String id) { 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/repository/BillRepository.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.repository; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | @Component 6 | public class BillRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/repository/CandleRepository.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.repository; 2 | 3 | import com.gitbitex.marketdata.entity.Candle; 4 | import com.gitbitex.openapi.model.PagedList; 5 | import com.mongodb.client.MongoCollection; 6 | import com.mongodb.client.MongoDatabase; 7 | import com.mongodb.client.model.*; 8 | import org.bson.conversions.Bson; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Collection; 13 | import java.util.List; 14 | 15 | @Component 16 | public class CandleRepository { 17 | private final MongoCollection mongoCollection; 18 | 19 | public CandleRepository(MongoDatabase database) { 20 | this.mongoCollection = database.getCollection(Candle.class.getSimpleName().toLowerCase(), Candle.class); 21 | } 22 | 23 | public Candle findById(String id) { 24 | return this.mongoCollection 25 | .find(Filters.eq("_id", id)) 26 | .first(); 27 | } 28 | 29 | public PagedList findAll(String productId, Integer granularity, int pageIndex, int pageSize) { 30 | Bson filter = Filters.empty(); 31 | if (productId != null) { 32 | filter = Filters.and(Filters.eq("productId", productId), filter); 33 | } 34 | if (granularity != null) { 35 | filter = Filters.and(Filters.eq("granularity", granularity), filter); 36 | } 37 | 38 | long count = this.mongoCollection.countDocuments(filter); 39 | List candles = this.mongoCollection.find(filter) 40 | .sort(Sorts.descending("time")) 41 | .skip(pageIndex - 1) 42 | .limit(pageSize) 43 | .into(new ArrayList<>()); 44 | return new PagedList<>(candles, count); 45 | } 46 | 47 | public void saveAll(Collection candles) { 48 | List> writeModels = new ArrayList<>(); 49 | for (Candle item : candles) { 50 | Bson filter = Filters.eq("_id", item.getId()); 51 | WriteModel writeModel = new ReplaceOneModel<>(filter, item, new ReplaceOptions().upsert(true)); 52 | writeModels.add(writeModel); 53 | } 54 | this.mongoCollection.bulkWrite(writeModels); 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/repository/FillRepository.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.repository; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | @Component 6 | public class FillRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/repository/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.repository; 2 | 3 | import com.gitbitex.enums.OrderSide; 4 | import com.gitbitex.enums.OrderStatus; 5 | import com.gitbitex.marketdata.entity.OrderEntity; 6 | import com.gitbitex.openapi.model.PagedList; 7 | import com.mongodb.client.MongoCollection; 8 | import com.mongodb.client.MongoDatabase; 9 | import com.mongodb.client.model.*; 10 | import org.bson.conversions.Bson; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Collection; 15 | import java.util.List; 16 | 17 | @Component 18 | public class OrderRepository { 19 | private final MongoCollection collection; 20 | 21 | public OrderRepository(MongoDatabase database) { 22 | this.collection = database.getCollection(OrderEntity.class.getSimpleName().toLowerCase(), OrderEntity.class); 23 | this.collection.createIndex(Indexes.descending("userId", "productId", "sequence")); 24 | } 25 | 26 | public OrderEntity findByOrderId(String orderId) { 27 | return this.collection 28 | .find(Filters.eq("_id", orderId)) 29 | .first(); 30 | } 31 | 32 | public PagedList findAll(String userId, String productId, OrderStatus status, OrderSide side, int pageIndex, 33 | int pageSize) { 34 | Bson filter = Filters.empty(); 35 | if (userId != null) { 36 | filter = Filters.and(Filters.eq("userId", userId), filter); 37 | } 38 | if (productId != null) { 39 | filter = Filters.and(Filters.eq("productId", productId), filter); 40 | } 41 | if (status != null) { 42 | filter = Filters.and(Filters.eq("status", status.name()), filter); 43 | } 44 | if (side != null) { 45 | filter = Filters.and(Filters.eq("side", side.name()), filter); 46 | } 47 | 48 | long count = this.collection.countDocuments(filter); 49 | List orders = this.collection 50 | .find(filter) 51 | .sort(Sorts.descending("sequence")) 52 | .skip(pageIndex - 1) 53 | .limit(pageSize) 54 | .into(new ArrayList<>()); 55 | return new PagedList<>(orders, count); 56 | } 57 | 58 | public void saveAll(Collection orders) { 59 | List> writeModels = new ArrayList<>(); 60 | for (OrderEntity item : orders) { 61 | Bson filter = Filters.eq("_id", item.getId()); 62 | WriteModel writeModel = new ReplaceOneModel<>(filter, item, new ReplaceOptions().upsert(true)); 63 | writeModels.add(writeModel); 64 | } 65 | collection.bulkWrite(writeModels, new BulkWriteOptions().ordered(false)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/repository/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.repository; 2 | 3 | import com.gitbitex.marketdata.entity.ProductEntity; 4 | import com.mongodb.client.MongoCollection; 5 | import com.mongodb.client.MongoDatabase; 6 | import com.mongodb.client.model.*; 7 | import org.bson.conversions.Bson; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | @Component 14 | public class ProductRepository { 15 | private final MongoCollection mongoCollection; 16 | 17 | public ProductRepository(MongoDatabase database) { 18 | this.mongoCollection = database.getCollection(ProductEntity.class.getSimpleName().toLowerCase(), ProductEntity.class); 19 | } 20 | 21 | public ProductEntity findById(String id) { 22 | return this.mongoCollection.find(Filters.eq("_id", id)).first(); 23 | } 24 | 25 | public List findAll() { 26 | return this.mongoCollection.find().into(new ArrayList<>()); 27 | } 28 | 29 | public void save(ProductEntity product) { 30 | List> writeModels = new ArrayList<>(); 31 | Bson filter = Filters.eq("_id", product.getId()); 32 | WriteModel writeModel = new ReplaceOneModel<>(filter, product, new ReplaceOptions().upsert(true)); 33 | writeModels.add(writeModel); 34 | this.mongoCollection.bulkWrite(writeModels, new BulkWriteOptions().ordered(false)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/repository/TradeRepository.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.repository; 2 | 3 | import com.gitbitex.marketdata.entity.TradeEntity; 4 | import com.mongodb.client.MongoCollection; 5 | import com.mongodb.client.MongoDatabase; 6 | import com.mongodb.client.model.*; 7 | import org.bson.conversions.Bson; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Collection; 12 | import java.util.List; 13 | 14 | @Component 15 | public class TradeRepository { 16 | private final MongoCollection collection; 17 | 18 | public TradeRepository(MongoDatabase database) { 19 | this.collection = database.getCollection(TradeEntity.class.getSimpleName().toLowerCase(), TradeEntity.class); 20 | this.collection.createIndex(Indexes.descending("productId", "sequence")); 21 | } 22 | 23 | public List findByProductId(String productId, int limit) { 24 | return this.collection.find(Filters.eq("productId", productId)) 25 | .sort(Sorts.descending("sequence")) 26 | .limit(limit) 27 | .into(new ArrayList<>()); 28 | } 29 | 30 | public void saveAll(Collection trades) { 31 | List> writeModels = new ArrayList<>(); 32 | for (TradeEntity item : trades) { 33 | Bson filter = Filters.eq("_id", item.getId()); 34 | WriteModel writeModel = new ReplaceOneModel<>(filter, item, new ReplaceOptions().upsert(true)); 35 | writeModels.add(writeModel); 36 | } 37 | collection.bulkWrite(writeModels, new BulkWriteOptions().ordered(false)); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.repository; 2 | 3 | import com.gitbitex.marketdata.entity.User; 4 | import com.mongodb.client.MongoCollection; 5 | import com.mongodb.client.MongoDatabase; 6 | import com.mongodb.client.model.Filters; 7 | import com.mongodb.client.model.IndexOptions; 8 | import com.mongodb.client.model.Indexes; 9 | import org.springframework.stereotype.Component; 10 | 11 | @Component 12 | public class UserRepository { 13 | private final MongoCollection collection; 14 | 15 | public UserRepository(MongoDatabase database) { 16 | this.collection = database.getCollection(User.class.getSimpleName().toLowerCase(), User.class); 17 | this.collection.createIndex(Indexes.descending("email"), new IndexOptions().unique(true)); 18 | } 19 | 20 | public User findByEmail(String email) { 21 | return this.collection 22 | .find(Filters.eq("email", email)) 23 | .first(); 24 | } 25 | 26 | public User findByUserId(String userId) { 27 | return this.collection 28 | .find(Filters.eq("_id", userId)) 29 | .first(); 30 | } 31 | 32 | public void save(User user) { 33 | this.collection.insertOne(user); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/marketdata/util/DateUtil.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.marketdata.util; 2 | 3 | import java.time.ZonedDateTime; 4 | import java.time.temporal.TemporalField; 5 | 6 | public class DateUtil { 7 | /** 8 | * https://stackoverflow.com/questions/3553964/how-to-round-time-to-the-nearest-quarter-hour-in-java/37423588 9 | * #37423588 10 | * 11 | * @param input 12 | * @param roundTo 13 | * @param roundIncrement 14 | * @return 15 | */ 16 | public static ZonedDateTime round(ZonedDateTime input, TemporalField roundTo, int roundIncrement) { 17 | /* Extract the field being rounded. */ 18 | int field = input.get(roundTo); 19 | 20 | /* Distance from previous floor. */ 21 | int r = field % roundIncrement; 22 | 23 | /* Find floor and ceiling. Truncate values to base unit of field. */ 24 | ZonedDateTime ceiling = input.plus(roundIncrement - r, roundTo.getBaseUnit()).truncatedTo( 25 | roundTo.getBaseUnit()); 26 | 27 | return input.plus(-r, roundTo.getBaseUnit()).truncatedTo(roundTo.getBaseUnit()); 28 | 29 | /* 30 | * Do a half-up rounding. 31 | * 32 | * If (input - floor) < (ceiling - input) 33 | * (i.e. floor is closer to input than ceiling) 34 | * then return floor, otherwise return ceiling. 35 | */ 36 | 37 | //return Duration.between(floor, input).compareTo(Duration.between(input, ceiling)) > 0 ? floor : ceiling; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/Account.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.math.BigDecimal; 7 | 8 | @Getter 9 | @Setter 10 | public class Account implements Cloneable { 11 | private String id; 12 | private String userId; 13 | private String currency; 14 | private BigDecimal available; 15 | private BigDecimal hold; 16 | 17 | @Override 18 | public Account clone() { 19 | try { 20 | return (Account) super.clone(); 21 | } catch (CloneNotSupportedException e) { 22 | throw new AssertionError(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/AccountBook.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.gitbitex.enums.OrderSide; 5 | import com.gitbitex.matchingengine.message.AccountMessage; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.lang.Nullable; 9 | 10 | import java.math.BigDecimal; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | import java.util.concurrent.atomic.AtomicLong; 14 | 15 | @Slf4j 16 | @RequiredArgsConstructor 17 | public class AccountBook { 18 | private final Map> accounts = new HashMap<>(); 19 | private final MessageSender messageSender; 20 | private final AtomicLong messageSequence; 21 | 22 | public void add(Account account) { 23 | this.accounts.computeIfAbsent(account.getUserId(), x -> new HashMap<>()) 24 | .put(account.getCurrency(), account); 25 | } 26 | 27 | @Nullable 28 | public Account getAccount(String userId, String currency) { 29 | Map accountMap = accounts.get(userId); 30 | if (accountMap != null) { 31 | return accountMap.get(currency); 32 | } 33 | return null; 34 | } 35 | 36 | public void deposit(String userId, String currency, BigDecimal amount, String transactionId) { 37 | Account account = getAccount(userId, currency); 38 | if (account == null) { 39 | account = createAccount(userId, currency); 40 | } 41 | account.setAvailable(account.getAvailable().add(amount)); 42 | 43 | messageSender.send(accountMessage(account.clone())); 44 | } 45 | 46 | public boolean hold(String userId, String currency, BigDecimal amount) { 47 | if (amount.compareTo(BigDecimal.ZERO) <= 0) { 48 | logger.error("amount should greater than 0: {}", amount); 49 | return false; 50 | } 51 | Account account = getAccount(userId, currency); 52 | if (account == null || account.getAvailable().compareTo(amount) < 0) { 53 | return false; 54 | } 55 | account.setAvailable(account.getAvailable().subtract(amount)); 56 | account.setHold(account.getHold().add(amount)); 57 | 58 | messageSender.send(accountMessage(account.clone())); 59 | return true; 60 | } 61 | 62 | public void unhold(String userId, String currency, BigDecimal amount) { 63 | if (amount.compareTo(BigDecimal.ZERO) <= 0) { 64 | throw new NullPointerException("amount should greater than 0"); 65 | } 66 | Account account = getAccount(userId, currency); 67 | if (account == null || account.getHold().compareTo(amount) < 0) { 68 | throw new NullPointerException("insufficient funds"); 69 | } 70 | account.setAvailable(account.getAvailable().add(amount)); 71 | account.setHold(account.getHold().subtract(amount)); 72 | 73 | messageSender.send(accountMessage(account.clone())); 74 | } 75 | 76 | public void exchange(String takerUserId, String makerUserId, 77 | String baseCurrency, String quoteCurrency, 78 | OrderSide takerSide, BigDecimal size, BigDecimal funds) { 79 | Account takerBaseAccount = getAccount(takerUserId, baseCurrency); 80 | Account takerQuoteAccount = getAccount(takerUserId, quoteCurrency); 81 | Account makerBaseAccount = getAccount(makerUserId, baseCurrency); 82 | Account makerQuoteAccount = getAccount(makerUserId, quoteCurrency); 83 | 84 | if (takerBaseAccount == null) { 85 | takerBaseAccount = createAccount(takerUserId, baseCurrency); 86 | } 87 | if (takerQuoteAccount == null) { 88 | takerQuoteAccount = createAccount(takerUserId, quoteCurrency); 89 | } 90 | if (makerBaseAccount == null) { 91 | makerBaseAccount = createAccount(makerUserId, baseCurrency); 92 | } 93 | if (makerQuoteAccount == null) { 94 | makerQuoteAccount = createAccount(makerUserId, quoteCurrency); 95 | } 96 | 97 | if (takerSide == OrderSide.BUY) { 98 | takerBaseAccount.setAvailable(takerBaseAccount.getAvailable().add(size)); 99 | takerQuoteAccount.setHold(takerQuoteAccount.getHold().subtract(funds)); 100 | makerBaseAccount.setHold(makerBaseAccount.getHold().subtract(size)); 101 | makerQuoteAccount.setAvailable(makerQuoteAccount.getAvailable().add(funds)); 102 | } else { 103 | takerBaseAccount.setHold(takerBaseAccount.getHold().subtract(size)); 104 | takerQuoteAccount.setAvailable(takerQuoteAccount.getAvailable().add(funds)); 105 | makerBaseAccount.setAvailable(makerBaseAccount.getAvailable().add(size)); 106 | makerQuoteAccount.setHold(makerQuoteAccount.getHold().subtract(funds)); 107 | } 108 | 109 | validateAccount(takerBaseAccount); 110 | validateAccount(takerQuoteAccount); 111 | validateAccount(makerBaseAccount); 112 | validateAccount(makerQuoteAccount); 113 | 114 | messageSender.send(accountMessage(takerBaseAccount.clone())); 115 | messageSender.send(accountMessage(takerQuoteAccount.clone())); 116 | messageSender.send(accountMessage(makerBaseAccount.clone())); 117 | messageSender.send(accountMessage(makerQuoteAccount.clone())); 118 | } 119 | 120 | private void validateAccount(Account account) { 121 | if (account.getAvailable().compareTo(BigDecimal.ZERO) < 0 || account.getHold().compareTo(BigDecimal.ZERO) < 0) { 122 | throw new RuntimeException("bad account: " + JSON.toJSONString(account)); 123 | } 124 | } 125 | 126 | public Account createAccount(String userId, String currency) { 127 | Account account = new Account(); 128 | account.setId(userId + "-" + currency); 129 | account.setUserId(userId); 130 | account.setCurrency(currency); 131 | account.setAvailable(BigDecimal.ZERO); 132 | account.setHold(BigDecimal.ZERO); 133 | this.accounts.computeIfAbsent(account.getUserId(), x -> new HashMap<>()).put(account.getCurrency(), account); 134 | return account; 135 | } 136 | 137 | private AccountMessage accountMessage(Account account) { 138 | AccountMessage message = new AccountMessage(); 139 | message.setSequence(messageSequence.incrementAndGet()); 140 | message.setAccount(account); 141 | return message; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/Depth.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.Comparator; 5 | import java.util.TreeMap; 6 | 7 | public class Depth extends TreeMap { 8 | 9 | public Depth(Comparator comparator) { 10 | super(comparator); 11 | } 12 | 13 | public void addOrder(Order order) { 14 | this.computeIfAbsent(order.getPrice(), k -> new PriceGroupedOrderCollection()).put(order.getId(), order); 15 | } 16 | 17 | public void removeOrder(Order order) { 18 | var orders = get(order.getPrice()); 19 | if (orders == null) { 20 | return; 21 | } 22 | orders.remove(order.getId()); 23 | if (orders.isEmpty()) { 24 | remove(order.getPrice()); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/MatchingEngineLoader.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine; 2 | 3 | import com.gitbitex.matchingengine.snapshot.EngineSnapshotManager; 4 | import lombok.Getter; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.stereotype.Component; 7 | 8 | import javax.annotation.Nullable; 9 | import java.util.concurrent.Executors; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | @Slf4j 13 | @Component 14 | public class MatchingEngineLoader { 15 | private final EngineSnapshotManager engineSnapshotManager; 16 | private final MessageSender messageSender; 17 | @Getter 18 | @Nullable 19 | private volatile MatchingEngine preperedMatchingEngine; 20 | 21 | public MatchingEngineLoader(EngineSnapshotManager engineSnapshotManager, MessageSender messageSender) { 22 | this.engineSnapshotManager = engineSnapshotManager; 23 | this.messageSender = messageSender; 24 | startRefreshPreparingMatchingEnginePeriodically(); 25 | } 26 | 27 | private void startRefreshPreparingMatchingEnginePeriodically() { 28 | Executors.newScheduledThreadPool(1).scheduleWithFixedDelay(() -> { 29 | try { 30 | logger.info("reloading latest snapshot"); 31 | preperedMatchingEngine = new MatchingEngine(engineSnapshotManager, messageSender); 32 | logger.info("done"); 33 | } catch (Exception e) { 34 | logger.error("matching engine create error: {}", e.getMessage(), e); 35 | } 36 | }, 0, 1, TimeUnit.MINUTES); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/MatchingEngineThread.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine; 2 | 3 | import com.gitbitex.AppProperties; 4 | import com.gitbitex.matchingengine.command.Command; 5 | import com.gitbitex.middleware.kafka.KafkaConsumerThread; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.apache.kafka.clients.consumer.ConsumerRebalanceListener; 8 | import org.apache.kafka.clients.consumer.KafkaConsumer; 9 | import org.apache.kafka.common.TopicPartition; 10 | 11 | import java.time.Duration; 12 | import java.util.Collection; 13 | import java.util.Collections; 14 | 15 | @Slf4j 16 | public class MatchingEngineThread extends KafkaConsumerThread 17 | implements ConsumerRebalanceListener { 18 | private final AppProperties appProperties; 19 | private final MatchingEngineLoader matchingEngineLoader; 20 | private MatchingEngine matchingEngine; 21 | 22 | public MatchingEngineThread(KafkaConsumer consumer, MatchingEngineLoader matchingEngineLoader, 23 | AppProperties appProperties) { 24 | super(consumer, logger); 25 | this.appProperties = appProperties; 26 | this.matchingEngineLoader = matchingEngineLoader; 27 | 28 | } 29 | 30 | @Override 31 | public void onPartitionsRevoked(Collection partitions) { 32 | for (TopicPartition partition : partitions) { 33 | logger.warn("partition revoked: {}", partition.toString()); 34 | } 35 | } 36 | 37 | @Override 38 | public void onPartitionsAssigned(Collection partitions) { 39 | for (TopicPartition partition : partitions) { 40 | logger.info("partition assigned: {}", partition.toString()); 41 | matchingEngine = matchingEngineLoader.getPreperedMatchingEngine(); 42 | if (matchingEngine == null) { 43 | throw new RuntimeException("no prepared matching engine"); 44 | } 45 | if (matchingEngine.getStartupCommandOffset() != null) { 46 | logger.info("seek to offset: {}", matchingEngine.getStartupCommandOffset() + 1); 47 | consumer.seek(partition, matchingEngine.getStartupCommandOffset() + 1); 48 | } 49 | } 50 | } 51 | 52 | @Override 53 | protected void doSubscribe() { 54 | consumer.subscribe(Collections.singletonList(appProperties.getMatchingEngineCommandTopic()), this); 55 | } 56 | 57 | @Override 58 | protected void doPoll() { 59 | consumer.poll(Duration.ofSeconds(5)) 60 | .forEach(x -> matchingEngine.executeCommand(x.value(), x.offset())); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/MessageSender.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine; 2 | 3 | import com.gitbitex.AppProperties; 4 | import com.gitbitex.matchingengine.message.Message; 5 | import com.gitbitex.matchingengine.message.MessageSerializer; 6 | import com.gitbitex.middleware.kafka.KafkaProperties; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.apache.kafka.clients.producer.KafkaProducer; 9 | import org.apache.kafka.clients.producer.ProducerConfig; 10 | import org.apache.kafka.clients.producer.ProducerRecord; 11 | import org.apache.kafka.common.serialization.StringSerializer; 12 | import org.springframework.stereotype.Component; 13 | 14 | import java.util.Properties; 15 | 16 | @Slf4j 17 | @Component 18 | public class MessageSender { 19 | private final AppProperties appProperties; 20 | private final KafkaProperties kafkaProperties; 21 | private final KafkaProducer kafkaProducer; 22 | 23 | public MessageSender(AppProperties appProperties, KafkaProperties kafkaProperties) { 24 | this.appProperties = appProperties; 25 | this.kafkaProperties = kafkaProperties; 26 | this.kafkaProducer = kafkaProducer(); 27 | } 28 | 29 | public void send(Message message) { 30 | ProducerRecord record = new ProducerRecord<>(appProperties.getMatchingEngineMessageTopic(), message); 31 | kafkaProducer.send(record); 32 | } 33 | 34 | private KafkaProducer kafkaProducer() { 35 | Properties properties = new Properties(); 36 | properties.put("bootstrap.servers", kafkaProperties.getBootstrapServers()); 37 | properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 38 | properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, MessageSerializer.class.getName()); 39 | properties.put("compression.type", "zstd"); 40 | properties.put("retries", 2147483647); 41 | properties.put("linger.ms", 100); 42 | properties.put("batch.size", 16384 * 2); 43 | properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true"); //Important, prevent message duplication 44 | properties.put("max.in.flight.requests.per.connection", 5); // Must be less than or equal to 5 45 | properties.put(ProducerConfig.ACKS_CONFIG, "all"); 46 | return new KafkaProducer<>(properties); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/Order.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine; 2 | 3 | import com.gitbitex.enums.OrderSide; 4 | import com.gitbitex.enums.OrderStatus; 5 | import com.gitbitex.enums.OrderType; 6 | import com.gitbitex.matchingengine.command.PlaceOrderCommand; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | 10 | import java.math.BigDecimal; 11 | import java.util.Date; 12 | 13 | @Getter 14 | @Setter 15 | public class Order implements Cloneable { 16 | private String id; 17 | private long sequence; 18 | private String userId; 19 | private OrderType type; 20 | private OrderSide side; 21 | private BigDecimal remainingSize; 22 | private BigDecimal price; 23 | private BigDecimal remainingFunds; 24 | private BigDecimal size; 25 | private BigDecimal funds; 26 | private boolean postOnly; 27 | private Date time; 28 | private String productId; 29 | private OrderStatus status; 30 | private String clientOid; 31 | 32 | public Order() { 33 | } 34 | 35 | public Order(PlaceOrderCommand command) { 36 | this.productId = command.getProductId(); 37 | this.userId = command.getUserId(); 38 | this.id = command.getOrderId(); 39 | this.type = command.getOrderType(); 40 | this.side = command.getOrderSide(); 41 | this.price = command.getPrice(); 42 | this.size = command.getSize(); 43 | if (command.getOrderType() == OrderType.LIMIT) { 44 | this.funds = command.getSize().multiply(command.getPrice()); 45 | } else { 46 | this.funds = command.getFunds(); 47 | } 48 | this.remainingSize = this.size; 49 | this.remainingFunds = this.funds; 50 | this.time = command.getTime(); 51 | } 52 | 53 | @Override 54 | public Order clone() { 55 | try { 56 | return (Order) super.clone(); 57 | } catch (CloneNotSupportedException e) { 58 | throw new AssertionError(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/PriceGroupedOrderCollection.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine; 2 | 3 | import lombok.Getter; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.LinkedHashMap; 7 | 8 | @Getter 9 | public class PriceGroupedOrderCollection extends LinkedHashMap { 10 | //public BigDecimal remainingSize = BigDecimal.ZERO; 11 | 12 | public void addOrder(Order order) { 13 | put(order.getId(), order); 14 | //remainingSize = remainingSize.add(order.getRemainingSize()); 15 | } 16 | 17 | public void decrRemainingSize(BigDecimal size) { 18 | //remainingSize=remainingSize.subtract(size); 19 | } 20 | 21 | public BigDecimal getRemainingSize() { 22 | return values().stream() 23 | .map(Order::getRemainingSize) 24 | .reduce(BigDecimal::add) 25 | .get(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/Product.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine; 2 | 3 | import com.gitbitex.matchingengine.command.PutProductCommand; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | public class Product implements Cloneable { 10 | private String id; 11 | private String baseCurrency; 12 | private String quoteCurrency; 13 | 14 | public Product() { 15 | } 16 | 17 | public Product(PutProductCommand command) { 18 | this.id = command.getProductId(); 19 | this.baseCurrency = command.getBaseCurrency(); 20 | this.quoteCurrency = command.getQuoteCurrency(); 21 | } 22 | 23 | @Override 24 | public Product clone() { 25 | try { 26 | return (Product) super.clone(); 27 | } catch (CloneNotSupportedException e) { 28 | throw new AssertionError(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/ProductBook.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine; 2 | 3 | import com.gitbitex.matchingengine.message.ProductMessage; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | import java.util.Collection; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.concurrent.atomic.AtomicLong; 11 | 12 | @Slf4j 13 | @RequiredArgsConstructor 14 | public class ProductBook { 15 | private final Map products = new HashMap<>(); 16 | private final MessageSender messageSender; 17 | private final AtomicLong messageSequence; 18 | 19 | public Collection getAllProducts() { 20 | return products.values(); 21 | } 22 | 23 | public Product getProduct(String productId) { 24 | return products.get(productId); 25 | } 26 | 27 | public void putProduct(Product product) { 28 | this.products.put(product.getId(), product); 29 | messageSender.send(productMessage(product.clone())); 30 | } 31 | 32 | public void addProduct(Product product) { 33 | this.products.put(product.getId(), product); 34 | } 35 | 36 | private ProductMessage productMessage(Product product) { 37 | ProductMessage message = new ProductMessage(); 38 | message.setSequence(messageSequence.incrementAndGet()); 39 | message.setProduct(product); 40 | return message; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/Trade.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine; 2 | 3 | import com.gitbitex.enums.OrderSide; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import java.math.BigDecimal; 8 | import java.util.Date; 9 | 10 | @Getter 11 | @Setter 12 | public class Trade { 13 | private String productId; 14 | private long sequence; 15 | private BigDecimal size; 16 | private BigDecimal funds; 17 | private BigDecimal price; 18 | private Date time; 19 | private OrderSide side; 20 | private String takerOrderId; 21 | private String makerOrderId; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/command/CancelOrderCommand.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.command; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class CancelOrderCommand extends Command { 9 | private String productId; 10 | private String orderId; 11 | 12 | public CancelOrderCommand() { 13 | this.setType(CommandType.CANCEL_ORDER); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/command/Command.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.command; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class Command { 9 | private CommandType type; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/command/CommandDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.command; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.apache.kafka.common.serialization.Deserializer; 6 | 7 | import java.nio.charset.Charset; 8 | 9 | @Slf4j 10 | public class CommandDeserializer implements Deserializer { 11 | @Override 12 | public Command deserialize(String topic, byte[] bytes) { 13 | try { 14 | CommandType commandType = CommandType.valueOfByte(bytes[0]); 15 | return switch (commandType) { 16 | case PUT_PRODUCT -> 17 | JSON.parseObject(bytes, 1, bytes.length - 1, Charset.defaultCharset(), PutProductCommand.class); 18 | case DEPOSIT -> 19 | JSON.parseObject(bytes, 1, bytes.length - 1, Charset.defaultCharset(), DepositCommand.class); 20 | case PLACE_ORDER -> JSON.parseObject(bytes, 1, bytes.length - 1, Charset.defaultCharset(), 21 | PlaceOrderCommand.class); 22 | case CANCEL_ORDER -> JSON.parseObject(bytes, 1, bytes.length - 1, Charset.defaultCharset(), 23 | CancelOrderCommand.class); 24 | default -> { 25 | logger.warn("Unhandled order message type: {}", commandType); 26 | yield JSON.parseObject(bytes, 1, bytes.length - 1, Charset.defaultCharset(), 27 | Command.class); 28 | } 29 | }; 30 | } catch (Exception e) { 31 | throw new RuntimeException("deserialize error: " + new String(bytes), e); 32 | } 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/command/CommandSerializer.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.command; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import org.apache.kafka.common.serialization.Serializer; 5 | 6 | public class CommandSerializer implements Serializer { 7 | @Override 8 | public byte[] serialize(String s, Command command) { 9 | byte[] jsonBytes = JSON.toJSONBytes(command); 10 | byte[] messageBytes = new byte[jsonBytes.length + 1]; 11 | messageBytes[0] = command.getType().getByteValue(); 12 | System.arraycopy(jsonBytes, 0, messageBytes, 1, jsonBytes.length); 13 | return messageBytes; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/command/CommandType.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.command; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public enum CommandType { 7 | PLACE_ORDER((byte) 1), 8 | CANCEL_ORDER((byte) 2), 9 | DEPOSIT((byte) 3), 10 | WITHDRAWAL((byte) 4), 11 | PUT_PRODUCT((byte) 5); 12 | 13 | private final byte byteValue; 14 | 15 | CommandType(byte value) { 16 | this.byteValue = value; 17 | } 18 | 19 | public static CommandType valueOfByte(byte b) { 20 | for (CommandType type : CommandType.values()) { 21 | if (b == type.byteValue) { 22 | return type; 23 | } 24 | } 25 | throw new RuntimeException("Unknown byte: " + b); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/command/DepositCommand.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.command; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.math.BigDecimal; 7 | 8 | @Getter 9 | @Setter 10 | public class DepositCommand extends Command { 11 | private String userId; 12 | private String currency; 13 | private BigDecimal amount; 14 | private String transactionId; 15 | 16 | public DepositCommand() { 17 | this.setType(CommandType.DEPOSIT); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/command/MatchingEngineCommandProducer.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.command; 2 | 3 | import com.gitbitex.AppProperties; 4 | import com.gitbitex.middleware.kafka.KafkaProperties; 5 | import org.apache.kafka.clients.producer.Callback; 6 | import org.apache.kafka.clients.producer.KafkaProducer; 7 | import org.apache.kafka.clients.producer.ProducerConfig; 8 | import org.apache.kafka.clients.producer.ProducerRecord; 9 | import org.apache.kafka.common.serialization.StringSerializer; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.util.Properties; 13 | 14 | @Component 15 | public class MatchingEngineCommandProducer { 16 | private final AppProperties appProperties; 17 | private final KafkaProperties kafkaProperties; 18 | private final KafkaProducer kafkaProducer; 19 | 20 | public MatchingEngineCommandProducer(AppProperties appProperties, KafkaProperties kafkaProperties) { 21 | this.appProperties = appProperties; 22 | this.kafkaProperties = kafkaProperties; 23 | this.kafkaProducer = kafkaProducer(); 24 | } 25 | 26 | public void send(Command command, Callback callback) { 27 | ProducerRecord record = new ProducerRecord<>(appProperties.getMatchingEngineCommandTopic(), command); 28 | kafkaProducer.send(record, callback); 29 | } 30 | 31 | public KafkaProducer kafkaProducer() { 32 | Properties properties = new Properties(); 33 | properties.put("bootstrap.servers", kafkaProperties.getBootstrapServers()); 34 | properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 35 | properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, CommandSerializer.class.getName()); 36 | properties.put("compression.type", "zstd"); 37 | properties.put("retries", 2147483647); 38 | properties.put("linger.ms", 100); 39 | properties.put("batch.size", 16384 * 2); 40 | properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true"); //Important, prevent message duplication 41 | properties.put("max.in.flight.requests.per.connection", 5); // Must be less than or equal to 5 42 | properties.put(ProducerConfig.ACKS_CONFIG, "all"); 43 | return new KafkaProducer<>(properties); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/command/PlaceOrderCommand.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.command; 2 | 3 | import com.gitbitex.enums.OrderSide; 4 | import com.gitbitex.enums.OrderType; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | 8 | import java.math.BigDecimal; 9 | import java.util.Date; 10 | 11 | @Getter 12 | @Setter 13 | public class PlaceOrderCommand extends Command { 14 | private String productId; 15 | private String orderId; 16 | private String userId; 17 | private BigDecimal size; 18 | private BigDecimal price; 19 | private BigDecimal funds; 20 | private OrderType orderType; 21 | private OrderSide orderSide; 22 | private Date time; 23 | 24 | public PlaceOrderCommand() { 25 | this.setType(CommandType.PLACE_ORDER); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/command/PutProductCommand.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.command; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class PutProductCommand extends Command { 9 | private String productId; 10 | private String baseCurrency; 11 | private String quoteCurrency; 12 | 13 | public PutProductCommand() { 14 | this.setType(CommandType.PUT_PRODUCT); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/message/AccountMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.message; 2 | 3 | import com.gitbitex.matchingengine.Account; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | public class AccountMessage extends Message { 10 | private Account account; 11 | 12 | public AccountMessage() { 13 | this.setMessageType(MessageType.ACCOUNT); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/message/CommandEndMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.message; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class CommandEndMessage extends Message { 9 | private long commandOffset; 10 | 11 | public CommandEndMessage() { 12 | this.setMessageType(MessageType.COMMAND_END); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/message/CommandStartMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.message; 2 | 3 | import com.gitbitex.matchingengine.command.Command; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | public class CommandStartMessage extends Message { 10 | private Command command; 11 | private long commandOffset; 12 | 13 | public CommandStartMessage() { 14 | this.setMessageType(MessageType.COMMAND_START); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/message/MatchingEngineMessageDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.message; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.apache.kafka.common.serialization.Deserializer; 6 | 7 | import java.nio.charset.Charset; 8 | 9 | @Slf4j 10 | public class MatchingEngineMessageDeserializer implements Deserializer { 11 | @Override 12 | public Message deserialize(String topic, byte[] bytes) { 13 | try { 14 | MessageType messageType = MessageType.valueOfByte(bytes[0]); 15 | switch (messageType) { 16 | case COMMAND_START: 17 | return JSON.parseObject(bytes, 1, bytes.length - 1, Charset.defaultCharset(), 18 | CommandStartMessage.class); 19 | case COMMAND_END: 20 | return JSON.parseObject(bytes, 1, bytes.length - 1, Charset.defaultCharset(), 21 | CommandEndMessage.class); 22 | case ACCOUNT: 23 | return JSON.parseObject(bytes, 1, bytes.length - 1, Charset.defaultCharset(), 24 | AccountMessage.class); 25 | case PRODUCT: 26 | return JSON.parseObject(bytes, 1, bytes.length - 1, Charset.defaultCharset(), 27 | ProductMessage.class); 28 | case ORDER: 29 | return JSON.parseObject(bytes, 1, bytes.length - 1, Charset.defaultCharset(), 30 | OrderMessage.class); 31 | case TRADE: 32 | return JSON.parseObject(bytes, 1, bytes.length - 1, Charset.defaultCharset(), 33 | TradeMessage.class); 34 | default: 35 | logger.warn("Unhandled order message type: {}", messageType); 36 | return JSON.parseObject(bytes, 1, bytes.length - 1, Charset.defaultCharset(), 37 | Message.class); 38 | } 39 | } catch (Exception e) { 40 | throw new RuntimeException("deserialize error: " + new String(bytes), e); 41 | } 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/message/Message.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.message; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class Message { 9 | private long sequence; 10 | private MessageType messageType; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/message/MessageSerializer.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.message; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import org.apache.kafka.common.serialization.Serializer; 5 | 6 | public class MessageSerializer implements Serializer { 7 | @Override 8 | public byte[] serialize(String s, Message command) { 9 | byte[] jsonBytes = JSON.toJSONBytes(command); 10 | byte[] messageBytes = new byte[jsonBytes.length + 1]; 11 | messageBytes[0] = command.getMessageType().getByteValue(); 12 | System.arraycopy(jsonBytes, 0, messageBytes, 1, jsonBytes.length); 13 | return messageBytes; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/message/MessageType.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.message; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public enum MessageType { 7 | ACCOUNT((byte) 1), 8 | PRODUCT((byte) 2), 9 | ORDER((byte) 3), 10 | TRADE((byte) 4), 11 | COMMAND_START((byte) 5), 12 | COMMAND_END((byte) 6); 13 | 14 | private final byte byteValue; 15 | 16 | MessageType(byte value) { 17 | this.byteValue = value; 18 | } 19 | 20 | public static MessageType valueOfByte(byte b) { 21 | for (MessageType type : MessageType.values()) { 22 | if (b == type.byteValue) { 23 | return type; 24 | } 25 | } 26 | throw new RuntimeException("Unknown byte: " + b); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/message/OrderBookMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.message; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.util.Date; 7 | 8 | @Getter 9 | @Setter 10 | public class OrderBookMessage { 11 | private String productId; 12 | private long sequence; 13 | private Date time; 14 | private MessageType type; 15 | 16 | public enum MessageType { 17 | ORDER_RECEIVED, 18 | ORDER_OPEN, 19 | ORDER_MATCH, 20 | ORDER_DONE, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/message/OrderDoneMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.message; 2 | 3 | import com.gitbitex.enums.OrderSide; 4 | import com.gitbitex.enums.OrderType; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | 8 | import java.math.BigDecimal; 9 | 10 | @Getter 11 | @Setter 12 | public class OrderDoneMessage extends OrderBookMessage { 13 | private String orderId; 14 | private BigDecimal remainingSize; 15 | private BigDecimal remainingFunds; 16 | private BigDecimal price; 17 | private OrderSide side; 18 | private OrderType orderType; 19 | private String doneReason; 20 | private String userId; 21 | 22 | public OrderDoneMessage() { 23 | this.setType(MessageType.ORDER_DONE); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/message/OrderMatchMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.message; 2 | 3 | import com.gitbitex.enums.OrderSide; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import java.math.BigDecimal; 8 | 9 | @Getter 10 | @Setter 11 | public class OrderMatchMessage extends OrderBookMessage { 12 | private String productId; 13 | private long sequence; 14 | private long tradeId; 15 | private String takerOrderId; 16 | private String makerOrderId; 17 | private String takerUserId; 18 | private String makerUserId; 19 | private OrderSide side; 20 | private BigDecimal price; 21 | private BigDecimal size; 22 | private BigDecimal funds; 23 | 24 | public OrderMatchMessage() { 25 | this.setType(MessageType.ORDER_MATCH); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/message/OrderMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.message; 2 | 3 | import com.gitbitex.matchingengine.Order; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | public class OrderMessage extends Message { 10 | private long orderBookSequence; 11 | private Order order; 12 | 13 | public OrderMessage() { 14 | this.setMessageType(MessageType.ORDER); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/message/OrderOpenMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.message; 2 | 3 | import com.gitbitex.enums.OrderSide; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import java.math.BigDecimal; 8 | 9 | @Getter 10 | @Setter 11 | public class OrderOpenMessage extends OrderBookMessage { 12 | private String orderId; 13 | private BigDecimal remainingSize; 14 | private BigDecimal price; 15 | private OrderSide side; 16 | private String userId; 17 | 18 | public OrderOpenMessage() { 19 | this.setType(MessageType.ORDER_OPEN); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/message/OrderReceivedMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.message; 2 | 3 | import com.gitbitex.enums.OrderSide; 4 | import com.gitbitex.enums.OrderType; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | 8 | import java.math.BigDecimal; 9 | import java.util.Date; 10 | 11 | @Getter 12 | @Setter 13 | public class OrderReceivedMessage extends OrderBookMessage { 14 | private String orderId; 15 | private String userId; 16 | private BigDecimal size; 17 | private BigDecimal price; 18 | private BigDecimal funds; 19 | private OrderSide side; 20 | private OrderType orderType; 21 | private String clientOid; 22 | private Date time; 23 | 24 | public OrderReceivedMessage() { 25 | this.setType(MessageType.ORDER_RECEIVED); 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/message/ProductMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.message; 2 | 3 | import com.gitbitex.matchingengine.Product; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | public class ProductMessage extends Message { 10 | private Product product; 11 | 12 | public ProductMessage() { 13 | this.setMessageType(MessageType.PRODUCT); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/message/TickerMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.message; 2 | 3 | import com.gitbitex.enums.OrderSide; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import java.math.BigDecimal; 8 | import java.util.Date; 9 | 10 | @Getter 11 | @Setter 12 | public class TickerMessage { 13 | private String productId; 14 | private long tradeId; 15 | private long sequence; 16 | private Date time; 17 | private BigDecimal price; 18 | private OrderSide side; 19 | private BigDecimal lastSize; 20 | private Long time24h; 21 | private BigDecimal open24h; 22 | private BigDecimal close24h; 23 | private BigDecimal high24h; 24 | private BigDecimal low24h; 25 | private BigDecimal volume24h; 26 | private Long time30d; 27 | private BigDecimal open30d; 28 | private BigDecimal close30d; 29 | private BigDecimal high30d; 30 | private BigDecimal low30d; 31 | private BigDecimal volume30d; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/message/TradeMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.message; 2 | 3 | import com.gitbitex.matchingengine.Trade; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | public class TradeMessage extends Message { 10 | private Trade trade; 11 | 12 | public TradeMessage() { 13 | this.setMessageType(MessageType.TRADE); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/snapshot/EngineState.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.snapshot; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | @Getter 10 | @Setter 11 | public class EngineState { 12 | private String id = "default"; 13 | private Long commandOffset; 14 | private Long messageOffset; 15 | private Long messageSequence; 16 | private Map tradeSequences = new HashMap<>(); 17 | private Map orderSequences = new HashMap<>(); 18 | private Map orderBookSequences = new HashMap<>(); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/matchingengine/snapshot/MatchingEngineSnapshotThread.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.matchingengine.snapshot; 2 | 3 | import com.gitbitex.AppProperties; 4 | import com.gitbitex.matchingengine.Account; 5 | import com.gitbitex.matchingengine.Order; 6 | import com.gitbitex.matchingengine.Product; 7 | import com.gitbitex.matchingengine.Trade; 8 | import com.gitbitex.matchingengine.message.*; 9 | import com.gitbitex.middleware.kafka.KafkaConsumerThread; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.apache.kafka.clients.consumer.ConsumerRebalanceListener; 12 | import org.apache.kafka.clients.consumer.ConsumerRecord; 13 | import org.apache.kafka.clients.consumer.KafkaConsumer; 14 | import org.apache.kafka.common.TopicPartition; 15 | 16 | import java.time.Duration; 17 | import java.util.Collection; 18 | import java.util.Collections; 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | @Slf4j 23 | public class MatchingEngineSnapshotThread extends KafkaConsumerThread implements ConsumerRebalanceListener { 24 | private final EngineSnapshotManager engineSnapshotManager; 25 | private final AppProperties appProperties; 26 | private final Map accounts = new HashMap<>(); 27 | private final Map orders = new HashMap<>(); 28 | private final Map products = new HashMap<>(); 29 | private EngineState engineState; 30 | 31 | public MatchingEngineSnapshotThread(KafkaConsumer consumer, 32 | EngineSnapshotManager engineSnapshotManager, AppProperties appProperties) { 33 | super(consumer, logger); 34 | this.engineSnapshotManager = engineSnapshotManager; 35 | this.appProperties = appProperties; 36 | } 37 | 38 | @Override 39 | public void onPartitionsRevoked(Collection collection) { 40 | 41 | } 42 | 43 | @Override 44 | public void onPartitionsAssigned(Collection partitions) { 45 | engineSnapshotManager.runInSession(session -> { 46 | engineState = engineSnapshotManager.getEngineState(session); 47 | if (engineState == null) { 48 | engineState = new EngineState(); 49 | } 50 | }); 51 | cleanBuffers(); 52 | 53 | if (engineState.getMessageOffset() != null) { 54 | long offset = engineState.getMessageOffset() + 1; 55 | logger.info("seek to offset: {}", offset); 56 | consumer.seek(partitions.iterator().next(), offset); 57 | } 58 | } 59 | 60 | @Override 61 | protected void doSubscribe() { 62 | consumer.subscribe(Collections.singletonList(appProperties.getMatchingEngineMessageTopic()), this); 63 | } 64 | 65 | @Override 66 | protected void doPoll() { 67 | var records = consumer.poll(Duration.ofSeconds(5)); 68 | for (ConsumerRecord record : records) { 69 | Message message = record.value(); 70 | 71 | long expectedSequence = engineState.getMessageSequence() != null 72 | ? engineState.getMessageSequence() + 1 : 1; 73 | if (message.getSequence() < expectedSequence) { 74 | continue; 75 | } else if (message.getSequence() > expectedSequence) { 76 | throw new RuntimeException(String.format("out of sequence: sequence=%s, expectedSequence=%s", message.getSequence(), expectedSequence)); 77 | } 78 | 79 | engineState.setMessageOffset(record.offset()); 80 | engineState.setMessageSequence(message.getSequence()); 81 | 82 | if (message instanceof OrderMessage orderMessage) { 83 | Order order = orderMessage.getOrder(); 84 | orders.put(order.getId(), order); 85 | engineState.getOrderSequences().put(order.getProductId(), order.getSequence()); 86 | engineState.getOrderBookSequences().put(order.getProductId(), orderMessage.getOrderBookSequence()); 87 | 88 | } else if (message instanceof TradeMessage tradeMessage) { 89 | Trade trade = tradeMessage.getTrade(); 90 | engineState.getTradeSequences().put(trade.getProductId(), trade.getSequence()); 91 | 92 | } else if (message instanceof AccountMessage accountMessage) { 93 | Account account = accountMessage.getAccount(); 94 | accounts.put(account.getId(), account); 95 | 96 | } else if (message instanceof ProductMessage productMessage) { 97 | Product product = productMessage.getProduct(); 98 | products.put(product.getId(), product); 99 | 100 | } else if (message instanceof CommandStartMessage commandStartMessage) { 101 | engineState.setCommandOffset(null); 102 | 103 | } else if (message instanceof CommandEndMessage commandEndMessage) { 104 | engineState.setCommandOffset(commandEndMessage.getCommandOffset()); 105 | 106 | saveState(); 107 | } 108 | } 109 | } 110 | 111 | private void saveState() { 112 | engineSnapshotManager.save(engineState, accounts.values(), orders.values(), products.values()); 113 | cleanBuffers(); 114 | } 115 | 116 | private void cleanBuffers() { 117 | accounts.clear(); 118 | orders.clear(); 119 | products.clear(); 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/middleware/kafka/KafkaConfig.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.middleware.kafka; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | @Configuration 8 | @RequiredArgsConstructor 9 | @EnableConfigurationProperties(KafkaProperties.class) 10 | public class KafkaConfig { 11 | 12 | } 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/middleware/kafka/KafkaConsumerThread.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.middleware.kafka; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.apache.kafka.clients.consumer.KafkaConsumer; 5 | import org.apache.kafka.common.errors.WakeupException; 6 | import org.slf4j.Logger; 7 | 8 | import java.util.concurrent.atomic.AtomicBoolean; 9 | 10 | /** 11 | * https://kafka.apache.org/23/javadoc/index.html?org/apache/kafka/clients/consumer/KafkaConsumer.html 12 | *

13 | * Multi-threaded Processing 14 | * The Kafka consumer is NOT thread-safe. All network I/O happens in the thread of the application making the call. 15 | * It is the responsibility of the user to ensure that multi-threaded access is properly synchronized. 16 | * Un-synchronized access will result in ConcurrentModificationException. 17 | * The only exception to this rule is wakeup(), which can safely be used from an external thread to interrupt an 18 | * active operation. In this case, a WakeupException will be thrown from the thread blocking on the operation. This 19 | * can be used to shutdown the consumer from another thread. The following snippet shows the typical pattern: 20 | * 21 | * @param 22 | * @param 23 | */ 24 | @RequiredArgsConstructor 25 | public abstract class KafkaConsumerThread extends Thread { 26 | protected final KafkaConsumer consumer; 27 | private final AtomicBoolean closed = new AtomicBoolean(); 28 | private final Logger logger; 29 | 30 | @Override 31 | public void run() { 32 | logger.info("starting..."); 33 | try { 34 | // subscribe 35 | doSubscribe(); 36 | 37 | // poll & process 38 | while (!closed.get()) { 39 | doPoll(); 40 | } 41 | } catch (WakeupException e) { 42 | // ignore exception if closing 43 | if (!closed.get()) { 44 | throw e; 45 | } 46 | } catch (Exception e) { 47 | logger.error("consumer error: {}", e.getMessage(), e); 48 | } finally { 49 | consumer.close(); 50 | } 51 | logger.info("exiting..."); 52 | } 53 | 54 | public void shutdown() { 55 | closed.set(true); 56 | consumer.wakeup(); 57 | } 58 | 59 | @Override 60 | public void interrupt() { 61 | this.shutdown(); 62 | super.interrupt(); 63 | } 64 | 65 | protected abstract void doSubscribe(); 66 | 67 | protected abstract void doPoll(); 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/middleware/kafka/KafkaProperties.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.middleware.kafka; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.validation.annotation.Validated; 7 | 8 | @ConfigurationProperties(prefix = "kafka") 9 | @Getter 10 | @Setter 11 | @Validated 12 | public class KafkaProperties { 13 | private String bootstrapServers; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/middleware/mongodb/MongoDbConfig.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.middleware.mongodb; 2 | 3 | import com.mongodb.MongoClientSettings; 4 | import com.mongodb.client.MongoClient; 5 | import com.mongodb.client.MongoClients; 6 | import com.mongodb.client.MongoDatabase; 7 | import lombok.RequiredArgsConstructor; 8 | import org.bson.codecs.configuration.CodecRegistry; 9 | import org.bson.codecs.pojo.PojoCodecProvider; 10 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | 14 | import static org.bson.codecs.configuration.CodecRegistries.fromProviders; 15 | import static org.bson.codecs.configuration.CodecRegistries.fromRegistries; 16 | 17 | @Configuration 18 | @RequiredArgsConstructor 19 | @EnableConfigurationProperties(MongoProperties.class) 20 | public class MongoDbConfig { 21 | 22 | @Bean(destroyMethod = "close") 23 | public MongoClient mongoClient(MongoProperties mongoProperties) { 24 | return MongoClients.create(mongoProperties.getUri()); 25 | } 26 | 27 | @Bean 28 | public MongoDatabase database(MongoClient mongoClient) { 29 | CodecRegistry pojoCodecRegistry = fromRegistries(MongoClientSettings.getDefaultCodecRegistry(), 30 | fromProviders(PojoCodecProvider.builder().automatic(true).build())); 31 | 32 | return mongoClient.getDatabase("gitbitex").withCodecRegistry(pojoCodecRegistry); 33 | } 34 | } 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/middleware/mongodb/MongoProperties.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.middleware.mongodb; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.validation.annotation.Validated; 7 | 8 | @ConfigurationProperties(prefix = "mongodb") 9 | @Getter 10 | @Setter 11 | @Validated 12 | public class MongoProperties { 13 | private String uri; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/middleware/redis/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.middleware.redis; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.redisson.Redisson; 5 | import org.redisson.api.RedissonClient; 6 | import org.redisson.config.Config; 7 | import org.redisson.config.SingleServerConfig; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 9 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | @Configuration 14 | @RequiredArgsConstructor 15 | @EnableConfigurationProperties(RedisProperties.class) 16 | @ConditionalOnClass(RedissonClient.class) 17 | public class RedisConfig { 18 | 19 | @Bean(destroyMethod = "shutdown") 20 | public RedissonClient redissonClient(RedisProperties redisProperties) { 21 | Config config = new Config(); 22 | SingleServerConfig singleServerConfig = config.useSingleServer(); 23 | singleServerConfig.setAddress(redisProperties.getAddress()); 24 | singleServerConfig.setConnectionPoolSize(200); 25 | if (redisProperties.getPassword() != null && !"".equals(redisProperties.getPassword().trim())) { 26 | singleServerConfig.setPassword(redisProperties.getPassword()); 27 | } 28 | return Redisson.create(config); 29 | } 30 | } 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/middleware/redis/RedisProperties.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.middleware.redis; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.validation.annotation.Validated; 7 | 8 | @ConfigurationProperties(prefix = "redis") 9 | @Getter 10 | @Setter 11 | @Validated 12 | public class RedisProperties { 13 | private String address; 14 | private String password; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/AuthInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi; 2 | 3 | import com.gitbitex.marketdata.entity.User; 4 | import com.gitbitex.marketdata.manager.UserManager; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.servlet.HandlerInterceptor; 8 | 9 | import javax.servlet.http.Cookie; 10 | import javax.servlet.http.HttpServletRequest; 11 | import javax.servlet.http.HttpServletResponse; 12 | 13 | @Component 14 | @RequiredArgsConstructor 15 | public class AuthInterceptor implements HandlerInterceptor { 16 | private final UserManager userManager; 17 | 18 | @Override 19 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { 20 | String accessToken = getAccessToken(request); 21 | if (accessToken != null) { 22 | User user = userManager.getUserByAccessToken(accessToken); 23 | request.setAttribute("currentUser", user); 24 | request.setAttribute("accessToken", accessToken); 25 | } 26 | return true; 27 | } 28 | 29 | private String getAccessToken(HttpServletRequest request) { 30 | String tokenKey = "accessToken"; 31 | String token = request.getParameter(tokenKey); 32 | if (token == null && request.getCookies() != null) { 33 | for (Cookie cookie : request.getCookies()) { 34 | if (cookie.getName().equals(tokenKey)) { 35 | token = cookie.getValue(); 36 | } 37 | } 38 | } 39 | return token; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/ExceptionAdvise.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi; 2 | 3 | import com.gitbitex.exception.ServiceException; 4 | import com.gitbitex.openapi.model.ErrorMessage; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.web.bind.MethodArgumentNotValidException; 8 | import org.springframework.web.bind.annotation.ControllerAdvice; 9 | import org.springframework.web.bind.annotation.ExceptionHandler; 10 | import org.springframework.web.bind.annotation.ResponseBody; 11 | import org.springframework.web.bind.annotation.ResponseStatus; 12 | import org.springframework.web.server.ResponseStatusException; 13 | import org.springframework.web.servlet.support.RequestContext; 14 | 15 | import javax.servlet.http.HttpServletRequest; 16 | import javax.servlet.http.HttpServletResponse; 17 | 18 | /** 19 | * @author lingqingwan 20 | */ 21 | @ControllerAdvice 22 | @Slf4j 23 | public class ExceptionAdvise { 24 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 25 | @ExceptionHandler(Exception.class) 26 | @ResponseBody 27 | public ErrorMessage handleException(Exception e) { 28 | logger.error("http error", e); 29 | 30 | return new ErrorMessage(e.getMessage()); 31 | } 32 | 33 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 34 | @ExceptionHandler(ServiceException.class) 35 | @ResponseBody 36 | public ErrorMessage handleException(ServiceException e, RequestContext request) { 37 | logger.error("http error: {} {} {}", e.getCode(), e.getMessage(), request.getRequestUri()); 38 | 39 | return new ErrorMessage(e.getCode().name()); 40 | } 41 | 42 | @ResponseStatus(HttpStatus.BAD_REQUEST) 43 | @ExceptionHandler(MethodArgumentNotValidException.class) 44 | @ResponseBody 45 | public ErrorMessage handleException(MethodArgumentNotValidException e) { 46 | logger.error("http error", e); 47 | 48 | StringBuilder sb = new StringBuilder(); 49 | e.getFieldErrors().forEach(x -> { 50 | sb.append(x.getField()).append(":").append(x.getDefaultMessage()).append("\n"); 51 | }); 52 | 53 | return new ErrorMessage(sb.toString()); 54 | } 55 | 56 | @ExceptionHandler(ResponseStatusException.class) 57 | @ResponseBody 58 | public ErrorMessage handleException(ResponseStatusException e, HttpServletRequest request, 59 | HttpServletResponse response) { 60 | logger.error("http error: {} {}", e.getMessage(), request.getRequestURI()); 61 | 62 | response.setStatus(e.getStatus().value()); 63 | return new ErrorMessage(e.getMessage()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 7 | 8 | @Configuration 9 | @RequiredArgsConstructor 10 | public class WebConfig implements WebMvcConfigurer { 11 | private final AuthInterceptor authInterceptor; 12 | 13 | @Override 14 | public void addInterceptors(InterceptorRegistry registry) { 15 | registry.addInterceptor(authInterceptor); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/controller/AccountController.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.controller; 2 | 3 | import com.gitbitex.marketdata.entity.AccountEntity; 4 | import com.gitbitex.marketdata.entity.User; 5 | import com.gitbitex.marketdata.manager.AccountManager; 6 | import com.gitbitex.openapi.model.AccountDto; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.web.bind.annotation.*; 10 | import org.springframework.web.server.ResponseStatusException; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.stream.Collectors; 16 | 17 | @RestController 18 | @RequestMapping("/api") 19 | @RequiredArgsConstructor 20 | public class AccountController { 21 | private final AccountManager accountManager; 22 | 23 | @GetMapping("/accounts") 24 | public List getAccounts(@RequestParam(name = "currency") List currencies, 25 | @RequestAttribute(required = false) User currentUser) { 26 | if (currentUser == null) { 27 | throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); 28 | } 29 | 30 | List accounts = accountManager.getAccounts(currentUser.getId()); 31 | Map accountByCurrency = accounts.stream() 32 | .collect(Collectors.toMap(AccountEntity::getCurrency, x -> x)); 33 | 34 | List accountDtoList = new ArrayList<>(); 35 | for (String currency : currencies) { 36 | AccountEntity account = accountByCurrency.get(currency); 37 | if (account != null) { 38 | accountDtoList.add(accountDto(account)); 39 | } else { 40 | AccountDto accountDto = new AccountDto(); 41 | accountDto.setCurrency(currency); 42 | accountDto.setAvailable("0"); 43 | accountDto.setHold("0"); 44 | accountDtoList.add(accountDto); 45 | } 46 | } 47 | return accountDtoList; 48 | } 49 | 50 | private AccountDto accountDto(AccountEntity account) { 51 | AccountDto accountDto = new AccountDto(); 52 | accountDto.setId(account.getId()); 53 | accountDto.setCurrency(account.getCurrency()); 54 | accountDto.setAvailable(account.getAvailable() != null ? account.getAvailable().toPlainString() : "0"); 55 | accountDto.setHold(account.getHold() != null ? account.getHold().toPlainString() : "0"); 56 | return accountDto; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/controller/AdminController.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.controller; 2 | 3 | import com.gitbitex.marketdata.entity.ProductEntity; 4 | import com.gitbitex.marketdata.entity.User; 5 | import com.gitbitex.marketdata.manager.AccountManager; 6 | import com.gitbitex.marketdata.manager.UserManager; 7 | import com.gitbitex.marketdata.repository.ProductRepository; 8 | import com.gitbitex.matchingengine.command.CancelOrderCommand; 9 | import com.gitbitex.matchingengine.command.DepositCommand; 10 | import com.gitbitex.matchingengine.command.MatchingEngineCommandProducer; 11 | import com.gitbitex.matchingengine.command.PutProductCommand; 12 | import lombok.Getter; 13 | import lombok.RequiredArgsConstructor; 14 | import lombok.Setter; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | import javax.validation.Valid; 18 | import javax.validation.constraints.NotBlank; 19 | import java.math.BigDecimal; 20 | import java.util.UUID; 21 | 22 | /** 23 | * For demonstration, do not expose to external users !!!!!! 24 | * For demonstration, do not expose to external users !!!!!! 25 | * For demonstration, do not expose to external users !!!!!! 26 | */ 27 | @RestController 28 | @RequiredArgsConstructor 29 | public class AdminController { 30 | private final MatchingEngineCommandProducer producer; 31 | private final AccountManager accountManager; 32 | private final ProductRepository productRepository; 33 | private final UserManager userManager; 34 | 35 | @GetMapping("/api/admin/createUser") 36 | public User createUser(String email, String password) { 37 | User user = userManager.getUser(email, password); 38 | if (user != null) { 39 | return user; 40 | } 41 | return userManager.createUser(email, password); 42 | } 43 | 44 | @GetMapping("/api/admin/deposit") 45 | public String deposit(@RequestParam String userId, @RequestParam String currency, @RequestParam String amount) { 46 | DepositCommand command = new DepositCommand(); 47 | command.setUserId(userId); 48 | command.setCurrency(currency); 49 | command.setAmount(new BigDecimal(amount)); 50 | command.setTransactionId(UUID.randomUUID().toString()); 51 | producer.send(command, null); 52 | return "ok"; 53 | } 54 | 55 | @PutMapping("/api/admin/products") 56 | public ProductEntity saveProduct(@RequestBody @Valid PutProductRequest request) { 57 | String productId = request.getBaseCurrency() + "-" + request.getQuoteCurrency(); 58 | ProductEntity product = new ProductEntity(); 59 | product.setId(productId); 60 | product.setBaseCurrency(request.baseCurrency); 61 | product.setQuoteCurrency(request.quoteCurrency); 62 | product.setBaseScale(6); 63 | product.setQuoteScale(2); 64 | product.setBaseMinSize(BigDecimal.ZERO); 65 | product.setBaseMaxSize(new BigDecimal("100000000")); 66 | product.setQuoteMinSize(BigDecimal.ZERO); 67 | product.setQuoteMaxSize(new BigDecimal("10000000000")); 68 | productRepository.save(product); 69 | 70 | PutProductCommand putProductCommand = new PutProductCommand(); 71 | putProductCommand.setProductId(product.getId()); 72 | putProductCommand.setBaseCurrency(product.getBaseCurrency()); 73 | putProductCommand.setQuoteCurrency(product.getQuoteCurrency()); 74 | producer.send(putProductCommand, null); 75 | 76 | return product; 77 | } 78 | 79 | public void cancelOrder(String orderId, String productId) { 80 | CancelOrderCommand command = new CancelOrderCommand(); 81 | command.setProductId(productId); 82 | command.setOrderId(orderId); 83 | producer.send(command, null); 84 | } 85 | 86 | @Getter 87 | @Setter 88 | public static class PutProductRequest { 89 | @NotBlank 90 | private String baseCurrency; 91 | @NotBlank 92 | private String quoteCurrency; 93 | 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/controller/AppController.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.controller; 2 | 3 | import com.gitbitex.marketdata.entity.AppEntity; 4 | import com.gitbitex.marketdata.entity.User; 5 | import com.gitbitex.marketdata.repository.AppRepository; 6 | import com.gitbitex.openapi.model.AppDto; 7 | import com.gitbitex.openapi.model.CreateAppRequest; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.web.bind.annotation.*; 11 | import org.springframework.web.server.ResponseStatusException; 12 | 13 | import java.util.List; 14 | import java.util.UUID; 15 | import java.util.stream.Collectors; 16 | 17 | @RestController 18 | @RequestMapping("/api") 19 | @RequiredArgsConstructor 20 | public class AppController { 21 | private final AppRepository appRepository; 22 | 23 | @GetMapping("/apps") 24 | public List getApps(@RequestAttribute(required = false) User currentUser) { 25 | if (currentUser == null) { 26 | throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); 27 | } 28 | 29 | List appEntities = appRepository.findByUserId(currentUser.getId()); 30 | return appEntities.stream().map(this::appDto).collect(Collectors.toList()); 31 | } 32 | 33 | @PostMapping("/apps") 34 | public AppDto createApp(CreateAppRequest request, @RequestAttribute(required = false) User currentUser) { 35 | if (currentUser == null) { 36 | throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); 37 | } 38 | 39 | AppEntity appEntity = new AppEntity(); 40 | appEntity.setId(UUID.randomUUID().toString()); 41 | appEntity.setUserId(currentUser.getId()); 42 | appEntity.setAccessKey(UUID.randomUUID().toString()); 43 | appEntity.setSecretKey(UUID.randomUUID().toString()); 44 | appEntity.setName(request.getName()); 45 | appRepository.save(appEntity); 46 | 47 | return appDto(appEntity); 48 | } 49 | 50 | @DeleteMapping("/apps/{appId}") 51 | public void deleteApp(@PathVariable String appId, @RequestAttribute(required = false) User currentUser) { 52 | if (currentUser == null) { 53 | throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); 54 | } 55 | 56 | AppEntity appEntity = appRepository.findByAppId(appId); 57 | if (appEntity == null) { 58 | throw new ResponseStatusException(HttpStatus.NOT_FOUND); 59 | } 60 | if (!appEntity.getUserId().equals(currentUser.getId())) { 61 | throw new ResponseStatusException(HttpStatus.FORBIDDEN); 62 | } 63 | 64 | appRepository.deleteById(appEntity.getId()); 65 | } 66 | 67 | private AppDto appDto(AppEntity appEntity) { 68 | AppDto appDto = new AppDto(); 69 | appDto.setId(appEntity.getId()); 70 | appDto.setName(appEntity.getName()); 71 | appDto.setKey(appEntity.getAccessKey()); 72 | appDto.setSecret(appEntity.getSecretKey()); 73 | appDto.setCreatedAt(appEntity.getCreatedAt() != null ? appEntity.getCreatedAt().toInstant().toString() : null); 74 | return appDto; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/controller/CodeController.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.controller; 2 | 3 | import org.springframework.web.bind.annotation.PostMapping; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | @RestController 8 | @RequestMapping("/api") 9 | public class CodeController { 10 | 11 | @PostMapping("/codes") 12 | public void getCode() { 13 | throw new RuntimeException("123456"); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/controller/ConfigController.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.controller; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | import org.springframework.web.bind.annotation.RestController; 5 | 6 | import java.util.Map; 7 | 8 | @RestController 9 | public class ConfigController { 10 | 11 | @GetMapping("/configs") 12 | public Map getConfigs() { 13 | return null; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/controller/HomeController.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class HomeController { 8 | @GetMapping(value = {"trade/*", "account/*"}) 9 | public String test() { 10 | return "forward:/index.html"; 11 | } 12 | } 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/controller/ProductController.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.controller; 2 | 3 | import com.gitbitex.marketdata.entity.Candle; 4 | import com.gitbitex.marketdata.entity.ProductEntity; 5 | import com.gitbitex.marketdata.entity.TradeEntity; 6 | import com.gitbitex.marketdata.orderbook.OrderBookSnapshotManager; 7 | import com.gitbitex.marketdata.repository.CandleRepository; 8 | import com.gitbitex.marketdata.repository.ProductRepository; 9 | import com.gitbitex.marketdata.repository.TradeRepository; 10 | import com.gitbitex.openapi.model.PagedList; 11 | import com.gitbitex.openapi.model.ProductDto; 12 | import com.gitbitex.openapi.model.TradeDto; 13 | import lombok.RequiredArgsConstructor; 14 | import org.springframework.beans.BeanUtils; 15 | import org.springframework.web.bind.annotation.GetMapping; 16 | import org.springframework.web.bind.annotation.PathVariable; 17 | import org.springframework.web.bind.annotation.RequestParam; 18 | import org.springframework.web.bind.annotation.RestController; 19 | 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.stream.Collectors; 23 | 24 | @RestController() 25 | @RequiredArgsConstructor 26 | public class ProductController { 27 | private final OrderBookSnapshotManager orderBookSnapshotManager; 28 | private final ProductRepository productRepository; 29 | private final TradeRepository tradeRepository; 30 | private final CandleRepository candleRepository; 31 | 32 | @GetMapping("/api/products") 33 | public List getProducts() { 34 | List products = productRepository.findAll(); 35 | return products.stream().map(this::productDto).collect(Collectors.toList()); 36 | } 37 | 38 | @GetMapping("/api/products/{productId}/trades") 39 | public List getProductTrades(@PathVariable String productId) { 40 | List trades = tradeRepository.findByProductId(productId, 50); 41 | return trades.stream().map(this::tradeDto).collect(Collectors.toList()); 42 | } 43 | 44 | @GetMapping("/api/products/{productId}/candles") 45 | public List> getProductCandles(@PathVariable String productId, @RequestParam int granularity, 46 | @RequestParam(defaultValue = "1000") int limit) { 47 | PagedList candlePage = candleRepository.findAll(productId, granularity / 60, 1, limit); 48 | 49 | //[ 50 | // [ time, low, high, open, close, volume ], 51 | // [ 1415398768, 0.32, 4.2, 0.35, 4.2, 12.3 ], 52 | //] 53 | List> lines = new ArrayList<>(); 54 | candlePage.getItems().forEach(x -> { 55 | List line = new ArrayList<>(); 56 | line.add(x.getTime()); 57 | line.add(x.getLow().stripTrailingZeros()); 58 | line.add(x.getHigh().stripTrailingZeros()); 59 | line.add(x.getOpen().stripTrailingZeros()); 60 | line.add(x.getClose().stripTrailingZeros()); 61 | line.add(x.getVolume().stripTrailingZeros()); 62 | lines.add(line); 63 | }); 64 | return lines; 65 | } 66 | 67 | @GetMapping("/api/products/{productId}/book") 68 | public Object getProductBook(@PathVariable String productId, @RequestParam(defaultValue = "2") int level) { 69 | return switch (level) { 70 | case 1 -> orderBookSnapshotManager.getL1OrderBook(productId); 71 | case 2 -> orderBookSnapshotManager.getL2BatchOrderBook(productId); 72 | case 3 -> orderBookSnapshotManager.getL3OrderBook(productId); 73 | default -> null; 74 | }; 75 | } 76 | 77 | private ProductDto productDto(ProductEntity product) { 78 | ProductDto productDto = new ProductDto(); 79 | BeanUtils.copyProperties(product, productDto); 80 | productDto.setId(product.getId()); 81 | productDto.setQuoteIncrement(String.valueOf(product.getQuoteIncrement())); 82 | return productDto; 83 | } 84 | 85 | private TradeDto tradeDto(TradeEntity trade) { 86 | TradeDto tradeDto = new TradeDto(); 87 | tradeDto.setSequence(trade.getSequence()); 88 | tradeDto.setTime(trade.getTime().toInstant().toString()); 89 | tradeDto.setPrice(trade.getPrice().toPlainString()); 90 | tradeDto.setSize(trade.getSize().toPlainString()); 91 | tradeDto.setSide(trade.getSide().name().toLowerCase()); 92 | return tradeDto; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.controller; 2 | 3 | import com.gitbitex.marketdata.entity.User; 4 | import com.gitbitex.marketdata.manager.UserManager; 5 | import com.gitbitex.marketdata.repository.UserRepository; 6 | import com.gitbitex.matchingengine.command.DepositCommand; 7 | import com.gitbitex.matchingengine.command.MatchingEngineCommandProducer; 8 | import com.gitbitex.openapi.model.*; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.web.bind.annotation.*; 12 | import org.springframework.web.server.ResponseStatusException; 13 | 14 | import javax.servlet.http.Cookie; 15 | import javax.servlet.http.HttpServletRequest; 16 | import javax.servlet.http.HttpServletResponse; 17 | import javax.validation.Valid; 18 | import java.math.BigDecimal; 19 | import java.util.UUID; 20 | 21 | @RestController 22 | @RequestMapping("/api") 23 | @RequiredArgsConstructor 24 | public class UserController { 25 | private final UserManager userManager; 26 | private final UserRepository userRepository; 27 | private final MatchingEngineCommandProducer matchingEngineCommandProducer; 28 | 29 | @GetMapping("/users/self") 30 | public UserDto getCurrentUser(@RequestAttribute(required = false) User currentUser) { 31 | if (currentUser == null) { 32 | throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); 33 | } 34 | return userDto(currentUser); 35 | } 36 | 37 | @PutMapping("/users/self") 38 | public UserDto updateProfile(@RequestBody UpdateProfileRequest updateProfileRequest, 39 | @RequestAttribute(required = false) User currentUser) { 40 | if (currentUser == null) { 41 | throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); 42 | } 43 | 44 | if (updateProfileRequest.getNickName() != null) { 45 | currentUser.setNickName(updateProfileRequest.getNickName()); 46 | } 47 | if (updateProfileRequest.getTwoStepVerificationType() != null) { 48 | currentUser.setTwoStepVerificationType(updateProfileRequest.getTwoStepVerificationType()); 49 | } 50 | userRepository.save(currentUser); 51 | 52 | return userDto(currentUser); 53 | } 54 | 55 | @PostMapping("/users/accessToken") 56 | public TokenDto signIn(@RequestBody @Valid SignInRequest signInRequest, HttpServletRequest request, 57 | HttpServletResponse response) { 58 | User user = userManager.getUser(signInRequest.getEmail(), signInRequest.getPassword()); 59 | if (user == null) { 60 | throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "email or password error"); 61 | } 62 | 63 | String token = userManager.generateAccessToken(user, request.getSession().getId()); 64 | 65 | addAccessTokenCookie(response, token); 66 | 67 | TokenDto tokenDto = new TokenDto(); 68 | tokenDto.setToken(token); 69 | tokenDto.setTwoStepVerification("none"); 70 | return tokenDto; 71 | } 72 | 73 | @DeleteMapping("/users/accessToken") 74 | public void signOut(@RequestAttribute(required = false) User currentUser, 75 | @RequestAttribute(required = false) String accessToken) { 76 | if (currentUser == null) { 77 | throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); 78 | } 79 | 80 | userManager.deleteAccessToken(accessToken); 81 | } 82 | 83 | @PostMapping("/users") 84 | public UserDto signUp(@RequestBody @Valid SignUpRequest signUpRequest) { 85 | User user = userManager.createUser(signUpRequest.getEmail(), signUpRequest.getPassword()); 86 | 87 | //TODO: Recharge each user for demonstration 88 | deposit(user.getId(), "BTC", BigDecimal.valueOf(1000000000)); 89 | deposit(user.getId(), "ETH", BigDecimal.valueOf(1000000000)); 90 | deposit(user.getId(), "USDT", BigDecimal.valueOf(1000000000)); 91 | 92 | return userDto(user); 93 | } 94 | 95 | private void addAccessTokenCookie(HttpServletResponse response, String accessToken) { 96 | Cookie cookie = new Cookie("accessToken", accessToken); 97 | cookie.setPath("/"); 98 | cookie.setMaxAge(7 * 24 * 60 * 60); 99 | cookie.setSecure(false); 100 | cookie.setHttpOnly(false); 101 | response.addCookie(cookie); 102 | } 103 | 104 | private UserDto userDto(User user) { 105 | UserDto userDto = new UserDto(); 106 | userDto.setId(user.getId()); 107 | userDto.setEmail(user.getEmail()); 108 | userDto.setBand(false); 109 | userDto.setCreatedAt(user.getCreatedAt() != null ? user.getCreatedAt().toInstant().toString() : null); 110 | userDto.setName(user.getNickName()); 111 | userDto.setTwoStepVerificationType(user.getTwoStepVerificationType()); 112 | return userDto; 113 | } 114 | 115 | private void deposit(String userId, String currency, BigDecimal amount) { 116 | DepositCommand command = new DepositCommand(); 117 | command.setUserId(userId); 118 | command.setCurrency(currency); 119 | command.setAmount(amount); 120 | command.setTransactionId(UUID.randomUUID().toString()); 121 | matchingEngineCommandProducer.send(command, null); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/AccountDto.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class AccountDto { 9 | private String id; 10 | private String currency; 11 | private String currencyIcon; 12 | private String available; 13 | private String hold; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/AppDto.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class AppDto { 9 | private String id; 10 | private String name; 11 | private String key; 12 | private String secret; 13 | private String createdAt; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/ChangePasswordRequest.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | public class ChangePasswordRequest { 4 | private String email; 5 | private String password; 6 | private String code; 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/CreateAppRequest.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class CreateAppRequest { 9 | private String name; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/ErrorMessage.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class ErrorMessage { 9 | private String message; 10 | 11 | public ErrorMessage() { 12 | } 13 | 14 | public ErrorMessage(String message) { 15 | this.message = message; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/NetworkDto.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class NetworkDto { 9 | private String status; 10 | private String hash; 11 | private String amount; 12 | private String feeAmount; 13 | private String feeCurrency; 14 | private int confirmations; 15 | private String resourceUrl; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/OrderDto.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class OrderDto { 9 | private String id; 10 | private String price; 11 | private String size; 12 | private String funds; 13 | private String productId; 14 | private String side; 15 | private String type; 16 | private String createdAt; 17 | private String fillFees; 18 | private String filledSize; 19 | private String executedValue; 20 | private String status; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/PagedList.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.util.List; 7 | 8 | @Getter 9 | @Setter 10 | public class PagedList { 11 | private List items; 12 | private long count; 13 | 14 | public PagedList(List items, long count) { 15 | this.items = items; 16 | this.count = count; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/PlaceOrderRequest.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import javax.validation.constraints.NotBlank; 7 | 8 | @Getter 9 | @Setter 10 | public class PlaceOrderRequest { 11 | 12 | private String clientOid; 13 | 14 | @NotBlank 15 | private String productId; 16 | 17 | @NotBlank 18 | private String size; 19 | 20 | private String funds; 21 | 22 | private String price; 23 | 24 | @NotBlank 25 | private String side; 26 | 27 | @NotBlank 28 | private String type; 29 | /** 30 | * [optional] GTC, GTT, IOC, or FOK (default is GTC) 31 | */ 32 | private String timeInForce; 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/PlaceOrderResponse.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | public class PlaceOrderResponse { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/ProductDto.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class ProductDto { 9 | private String id; 10 | private String baseCurrency; 11 | private String quoteCurrency; 12 | private String baseMinSize; 13 | private String baseMaxSize; 14 | private String quoteIncrement; 15 | private int baseScale; 16 | private int quoteScale; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/Response.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class Response { 9 | private T data; 10 | 11 | public Response(T data) { 12 | this.data = data; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/SendCodeRequest.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | public class SendCodeRequest { 4 | private String type; 5 | private String email; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/SignInRequest.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class SignInRequest { 9 | private String email; 10 | private String password; 11 | private String code; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/SignUpRequest.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import javax.validation.constraints.Email; 7 | import javax.validation.constraints.NotBlank; 8 | 9 | @Getter 10 | @Setter 11 | public class SignUpRequest { 12 | @Email 13 | @NotBlank(message = "Email cannot be empty") 14 | private String email; 15 | 16 | @NotBlank 17 | private String password; 18 | 19 | private String code; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/TokenDto.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class TokenDto { 9 | private String token; 10 | private String twoStepVerification; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/TotpUriDto.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class TotpUriDto { 9 | private String uri; 10 | private String secretKey; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/TradeDto.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class TradeDto { 9 | private long sequence; 10 | private String time; 11 | private String price; 12 | private String size; 13 | private String side; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/UpdateProfileRequest.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class UpdateProfileRequest { 9 | private String nickName; 10 | private String twoStepVerificationType; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/UserDto.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class UserDto { 9 | private String id; 10 | private String email; 11 | private String name; 12 | private String profilePhoto; 13 | private boolean isBand; 14 | private String createdAt; 15 | private String twoStepVerificationType; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/openapi/model/WalletAddressDto.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.openapi.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class WalletAddressDto { 9 | private String address; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/stripexecutor/StripedCallable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2000-2012 Heinz Max Kabutz 3 | * 4 | * See the NOTICE file distributed with this work for additional 5 | * information regarding copyright ownership. Heinz Max Kabutz licenses 6 | * this file to you under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. You may 8 | * obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package com.gitbitex.stripexecutor; 20 | 21 | import java.util.concurrent.Callable; 22 | 23 | /** 24 | * All of the Callables in the same "Stripe" will be executed consecutively. 25 | * 26 | * @author Dr Heinz M. Kabutz 27 | * @see StripedExecutorService 28 | */ 29 | public interface StripedCallable extends Callable, StripedObject { 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/stripexecutor/StripedObject.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2000-2012 Heinz Max Kabutz 3 | * 4 | * See the NOTICE file distributed with this work for additional 5 | * information regarding copyright ownership. Heinz Max Kabutz licenses 6 | * this file to you under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. You may 8 | * obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package com.gitbitex.stripexecutor; 20 | 21 | /** 22 | * Used to indicate which "stripe" this Runnable or Callable belongs to. The 23 | * stripe is determined by the identity of the object, rather than its hash 24 | * code and equals. 25 | * 26 | * @author Dr Heinz M. Kabutz 27 | * @see StripedExecutorService 28 | */ 29 | public interface StripedObject { 30 | Object getStripe(); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/stripexecutor/StripedRunnable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2000-2012 Heinz Max Kabutz 3 | * 4 | * See the NOTICE file distributed with this work for additional 5 | * information regarding copyright ownership. Heinz Max Kabutz licenses 6 | * this file to you under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. You may 8 | * obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package com.gitbitex.stripexecutor; 20 | 21 | /** 22 | * All of the Runnables in the same "Stripe" will be executed consecutively. 23 | * 24 | * @author Dr Heinz M. Kabutz 25 | * @see StripedExecutorService 26 | */ 27 | public interface StripedRunnable extends Runnable, StripedObject { 28 | } -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/wallet/Transaction.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.wallet; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.math.BigDecimal; 7 | 8 | @Getter 9 | @Setter 10 | public class Transaction { 11 | private String id; 12 | private String userId; 13 | private long sequence; 14 | private String currency; 15 | private BigDecimal amount; 16 | private boolean submitted; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/gitbitex/wallet/TransactionSubmitter.java: -------------------------------------------------------------------------------- 1 | package com.gitbitex.wallet; 2 | 3 | public class TransactionSubmitter { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=gitbitex 2 | server.port=80 3 | # 4 | #management 5 | # 6 | management.server.port=7002 7 | management.endpoints.web.exposure.include=health, metrics, prometheus 8 | management.metrics.tags.application=${spring.application.name} 9 | # 10 | # mongodb 11 | # 12 | mongodb.uri=mongodb://mongo1:30001,mongo2:30002,mongo3:30003/?replicaSet=my-replica-set 13 | # 14 | # kafka 15 | # 16 | kafka.bootstrap-servers=localhost:19092 17 | # 18 | # redis 19 | # 20 | redis.address=redis://127.0.0.1:6379 21 | redis.password= 22 | # 23 | # GitBitEX 24 | # 25 | gbe.matching-engine-command-topic=matching-engine-command 26 | gbe.matching-engine-message-topic=matching-engine-message -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | ${CONSOLE_LOG_PATTERN} 20 | ${CONSOLE_LOG_CHARSET} 21 | 22 | 23 | 24 | 25 | logs/appEntity.log 26 | 27 | ${CONSOLE_LOG_PATTERN} 28 | ${FILE_LOG_CHARSET} 29 | 30 | 31 | 32 | logs/appEntity-%d{yyyy-MM-dd}.%i.log 33 | 500MB 34 | 30 35 | 5000MB 36 | false 37 | 38 | 39 | 40 | logs/metric.log 41 | 42 | %m%n 43 | ${FILE_LOG_CHARSET} 44 | 45 | 46 | 47 | logs/metric-%d{yyyy-MM-dd}.%i.log 48 | 500MB 49 | 30 50 | 5000MB 51 | false 52 | 53 | 54 | 55 | 56 | 57 | 10000 58 | 59 | 60 | 61 | 62 | 10000 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/main/resources/static/assets/font/AtlasTypewriter-Bold-Web.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitbitex/gitbitex-new/c61209ba84e0b4d5c22f996f3eabff753500541f/src/main/resources/static/assets/font/AtlasTypewriter-Bold-Web.woff2 -------------------------------------------------------------------------------- /src/main/resources/static/assets/font/AtlasTypewriter-Medium-Web.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitbitex/gitbitex-new/c61209ba84e0b4d5c22f996f3eabff753500541f/src/main/resources/static/assets/font/AtlasTypewriter-Medium-Web.woff2 -------------------------------------------------------------------------------- /src/main/resources/static/assets/font/AtlasTypewriter-Regular-Web.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitbitex/gitbitex-new/c61209ba84e0b4d5c22f996f3eabff753500541f/src/main/resources/static/assets/font/AtlasTypewriter-Regular-Web.woff2 -------------------------------------------------------------------------------- /src/main/resources/static/assets/font/Graphik-Medium-Web.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitbitex/gitbitex-new/c61209ba84e0b4d5c22f996f3eabff753500541f/src/main/resources/static/assets/font/Graphik-Medium-Web.woff2 -------------------------------------------------------------------------------- /src/main/resources/static/assets/font/Graphik-Regular-Web.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitbitex/gitbitex-new/c61209ba84e0b4d5c22f996f3eabff753500541f/src/main/resources/static/assets/font/Graphik-Regular-Web.woff2 -------------------------------------------------------------------------------- /src/main/resources/static/assets/font/Graphik-RegularItalic-Web.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitbitex/gitbitex-new/c61209ba84e0b4d5c22f996f3eabff753500541f/src/main/resources/static/assets/font/Graphik-RegularItalic-Web.woff2 -------------------------------------------------------------------------------- /src/main/resources/static/assets/font/Graphik-Semibold-Web.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitbitex/gitbitex-new/c61209ba84e0b4d5c22f996f3eabff753500541f/src/main/resources/static/assets/font/Graphik-Semibold-Web.woff2 -------------------------------------------------------------------------------- /src/main/resources/static/assets/font/feather-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitbitex/gitbitex-new/c61209ba84e0b4d5c22f996f3eabff753500541f/src/main/resources/static/assets/font/feather-webfont.woff -------------------------------------------------------------------------------- /src/main/resources/static/assets/font/opensans-bold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitbitex/gitbitex-new/c61209ba84e0b4d5c22f996f3eabff753500541f/src/main/resources/static/assets/font/opensans-bold-webfont.woff2 -------------------------------------------------------------------------------- /src/main/resources/static/assets/font/opensans-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitbitex/gitbitex-new/c61209ba84e0b4d5c22f996f3eabff753500541f/src/main/resources/static/assets/font/opensans-regular-webfont.woff2 -------------------------------------------------------------------------------- /src/main/resources/static/assets/image/HudexGlobal-200-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitbitex/gitbitex-new/c61209ba84e0b4d5c22f996f3eabff753500541f/src/main/resources/static/assets/image/HudexGlobal-200-04.png -------------------------------------------------------------------------------- /src/main/resources/static/assets/image/HudexGlobal-200-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitbitex/gitbitex-new/c61209ba84e0b4d5c22f996f3eabff753500541f/src/main/resources/static/assets/image/HudexGlobal-200-07.png -------------------------------------------------------------------------------- /src/main/resources/static/assets/image/HudexGlobal-400-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitbitex/gitbitex-new/c61209ba84e0b4d5c22f996f3eabff753500541f/src/main/resources/static/assets/image/HudexGlobal-400-04.png -------------------------------------------------------------------------------- /src/main/resources/static/assets/image/HudexGlobal-400-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitbitex/gitbitex-new/c61209ba84e0b4d5c22f996f3eabff753500541f/src/main/resources/static/assets/image/HudexGlobal-400-07.png -------------------------------------------------------------------------------- /src/main/resources/static/assets/image/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitbitex/gitbitex-new/c61209ba84e0b4d5c22f996f3eabff753500541f/src/main/resources/static/assets/image/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/static/assets/image/logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/resources/static/assets/image/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=4567 2 | # 3 | # Mysql 4 | # 5 | spring.datasource.url=jdbc:h2:mem:testdb 6 | spring.datasource.username=test 7 | spring.datasource.password=test 8 | spring.datasource.driver-class-name=org.h2.Driver 9 | # 10 | # Jpa 11 | # 12 | spring.jpa.database-platform=org.hibernate.dialect.H2Dialect 13 | spring.jpa.hibernate.use-new-id-generator-mappings=false 14 | spring.jpa.hibernate.ddl-auto=update 15 | spring.jpa.show-sql=false 16 | spring.jpa.open-in-view=false 17 | # 18 | # kafka 19 | # 20 | spring.kafka.max 21 | logging.level.org.apache.kafka=WARN 22 | --------------------------------------------------------------------------------