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 |
--------------------------------------------------------------------------------