Compare commits
4 Commits
62235b80bf
...
lab9_1
| Author | SHA1 | Date | |
|---|---|---|---|
|
826389b3c3
|
|||
|
045a5a9f64
|
|||
|
9ab033bd58
|
|||
|
39f827f3e7
|
@@ -12,6 +12,8 @@ repositories {
|
||||
dependencies {
|
||||
testImplementation platform('org.junit:junit-bom:5.10.0')
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
|
||||
implementation 'com.google.guava:guava:33.2.1-jre'
|
||||
}
|
||||
|
||||
test {
|
||||
|
||||
23
src/main/java/ru/lionarius/api/CurrencyExchange.java
Normal file
23
src/main/java/ru/lionarius/api/CurrencyExchange.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package ru.lionarius.api;
|
||||
|
||||
import ru.lionarius.api.client.Client;
|
||||
import ru.lionarius.api.currency.CurrencyPair;
|
||||
import ru.lionarius.api.order.OrderType;
|
||||
import ru.lionarius.api.order.OrderView;
|
||||
import ru.lionarius.api.order.message.OrderMessage;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface CurrencyExchange {
|
||||
CompletableFuture<Client> createClient(String name);
|
||||
|
||||
CompletableFuture<UUID> placeOrder(UUID clientId, CurrencyPair pair, OrderType type, double price, double quantity);
|
||||
|
||||
CompletableFuture<Void> cancelOrder(UUID clientId, UUID orderId);
|
||||
|
||||
CompletableFuture<List<OrderView>> getOrders(UUID clientId);
|
||||
|
||||
CompletableFuture<List<OrderMessage>> getOrderMessages(UUID clientId, UUID orderId);
|
||||
}
|
||||
23
src/main/java/ru/lionarius/api/client/Client.java
Normal file
23
src/main/java/ru/lionarius/api/client/Client.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package ru.lionarius.api.client;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record Client(UUID id, String name) {
|
||||
public Client(String name) {
|
||||
this(UUID.randomUUID(), name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Client client) {
|
||||
return id.equals(client.id);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return id.hashCode();
|
||||
}
|
||||
}
|
||||
15
src/main/java/ru/lionarius/api/client/ClientRepository.java
Normal file
15
src/main/java/ru/lionarius/api/client/ClientRepository.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package ru.lionarius.api.client;
|
||||
|
||||
|
||||
import ru.lionarius.api.order.OrderList;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface ClientRepository {
|
||||
Client createClient(String name);
|
||||
|
||||
Optional<Client> getClient(UUID clientId);
|
||||
|
||||
Optional<OrderList> getClientOrders(UUID clientId);
|
||||
}
|
||||
4
src/main/java/ru/lionarius/api/currency/Currency.java
Normal file
4
src/main/java/ru/lionarius/api/currency/Currency.java
Normal file
@@ -0,0 +1,4 @@
|
||||
package ru.lionarius.api.currency;
|
||||
|
||||
public record Currency(String name) {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package ru.lionarius.api.currency;
|
||||
|
||||
public record CurrencyPair(Currency base, Currency quote) {
|
||||
}
|
||||
127
src/main/java/ru/lionarius/api/order/Order.java
Normal file
127
src/main/java/ru/lionarius/api/order/Order.java
Normal file
@@ -0,0 +1,127 @@
|
||||
package ru.lionarius.api.order;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import ru.lionarius.api.currency.CurrencyPair;
|
||||
import ru.lionarius.api.order.message.OrderCreatedMessage;
|
||||
import ru.lionarius.api.order.message.OrderFilledMessage;
|
||||
import ru.lionarius.api.order.message.OrderMessage;
|
||||
import ru.lionarius.api.order.message.OrderMessageType;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
public class Order {
|
||||
private final UUID id = UUID.randomUUID();
|
||||
private final UUID clientId;
|
||||
private final OrderType type;
|
||||
private final CurrencyPair pair;
|
||||
private final OrderData originalData;
|
||||
|
||||
private OrderData lastData;
|
||||
private boolean closed = false;
|
||||
private final List<OrderMessage> messages = new ArrayList<>();
|
||||
|
||||
private final ReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
|
||||
public Order(UUID clientId, OrderType type, CurrencyPair pair, OrderData data) {
|
||||
this.clientId = clientId;
|
||||
this.type = type;
|
||||
this.pair = pair;
|
||||
this.originalData = data;
|
||||
|
||||
pushMessage(new OrderCreatedMessage(data, LocalDateTime.now()));
|
||||
}
|
||||
|
||||
public OrderView getView() {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
return new OrderView(id, clientId, type, pair, originalData, lastData, closed);
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public UUID getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public OrderType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public CurrencyPair getPair() {
|
||||
return pair;
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
return closed;
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public OrderData getLastData() {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
return lastData;
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void pushMessage(OrderMessage message) {
|
||||
if (closed)
|
||||
throw new IllegalStateException("Order is closed");
|
||||
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
if (message.type() == OrderMessageType.CREATED) {
|
||||
if (lastData != null)
|
||||
throw new IllegalStateException("Order already has data");
|
||||
|
||||
lastData = ((OrderCreatedMessage) message).data();
|
||||
} else if (message.type() == OrderMessageType.FILLED) {
|
||||
lastData = ((OrderFilledMessage) message).newData();
|
||||
} else if (message.type() == OrderMessageType.CLOSED) {
|
||||
closed = true;
|
||||
}
|
||||
|
||||
messages.add(message);
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public List<OrderMessage> getMessages() {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
return ImmutableList.copyOf(messages);
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return id.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Order order) {
|
||||
return id.equals(order.id);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
7
src/main/java/ru/lionarius/api/order/OrderData.java
Normal file
7
src/main/java/ru/lionarius/api/order/OrderData.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package ru.lionarius.api.order;
|
||||
|
||||
public record OrderData(double price, double quantity) {
|
||||
public double rate() {
|
||||
return price() / quantity();
|
||||
}
|
||||
}
|
||||
13
src/main/java/ru/lionarius/api/order/OrderList.java
Normal file
13
src/main/java/ru/lionarius/api/order/OrderList.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package ru.lionarius.api.order;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface OrderList {
|
||||
void add(Order order);
|
||||
|
||||
Optional<Order> get(UUID orderId);
|
||||
|
||||
List<Order> get();
|
||||
}
|
||||
6
src/main/java/ru/lionarius/api/order/OrderType.java
Normal file
6
src/main/java/ru/lionarius/api/order/OrderType.java
Normal file
@@ -0,0 +1,6 @@
|
||||
package ru.lionarius.api.order;
|
||||
|
||||
public enum OrderType {
|
||||
BUY,
|
||||
SELL
|
||||
}
|
||||
9
src/main/java/ru/lionarius/api/order/OrderView.java
Normal file
9
src/main/java/ru/lionarius/api/order/OrderView.java
Normal file
@@ -0,0 +1,9 @@
|
||||
package ru.lionarius.api.order;
|
||||
|
||||
import ru.lionarius.api.currency.CurrencyPair;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record OrderView(UUID id, UUID clientId, OrderType type, CurrencyPair pair, OrderData originalData,
|
||||
OrderData lastData, boolean closed) {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package ru.lionarius.api.order.message;
|
||||
|
||||
public record OrderClosedMessage(Reason reason) implements OrderMessage {
|
||||
|
||||
@Override
|
||||
public OrderMessageType type() {
|
||||
return OrderMessageType.CLOSED;
|
||||
}
|
||||
|
||||
public enum Reason {
|
||||
CANCELLED,
|
||||
FULFILLED
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package ru.lionarius.api.order.message;
|
||||
|
||||
import ru.lionarius.api.order.OrderData;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record OrderCreatedMessage(OrderData data, LocalDateTime timestamp) implements OrderMessage {
|
||||
|
||||
@Override
|
||||
public OrderMessageType type() {
|
||||
return OrderMessageType.CREATED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.lionarius.api.order.message;
|
||||
|
||||
import ru.lionarius.api.order.OrderData;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record OrderFilledMessage(UUID otherOrderId, OrderData newData) implements OrderMessage {
|
||||
@Override
|
||||
public OrderMessageType type() {
|
||||
return OrderMessageType.FILLED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package ru.lionarius.api.order.message;
|
||||
|
||||
public interface OrderMessage {
|
||||
OrderMessageType type();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.lionarius.api.order.message;
|
||||
|
||||
public enum OrderMessageType {
|
||||
CREATED,
|
||||
CLOSED,
|
||||
FILLED
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package ru.lionarius.impl;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import ru.lionarius.api.client.Client;
|
||||
import ru.lionarius.api.client.ClientRepository;
|
||||
import ru.lionarius.api.order.OrderList;
|
||||
import ru.lionarius.api.order.Order;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class InMemoryClientRepository implements ClientRepository {
|
||||
private final Map<UUID, Client> clients = new ConcurrentHashMap<>();
|
||||
private final Map<UUID, OrdersList> orders = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public Client createClient(String name) {
|
||||
var client = new Client(name);
|
||||
|
||||
clients.put(client.id(), client);
|
||||
orders.put(client.id(), new OrdersList());
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Client> getClient(UUID clientId) {
|
||||
return Optional.ofNullable(clients.get(clientId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<OrderList> getClientOrders(UUID clientId) {
|
||||
return Optional.ofNullable(orders.get(clientId));
|
||||
}
|
||||
|
||||
private static class OrdersList implements OrderList {
|
||||
private final Map<UUID, Order> orders = new ConcurrentHashMap<>();
|
||||
|
||||
public void add(Order order) {
|
||||
orders.put(order.getId(), order);
|
||||
}
|
||||
|
||||
public Optional<Order> get(UUID orderId) {
|
||||
return Optional.ofNullable(orders.get(orderId));
|
||||
}
|
||||
|
||||
public List<Order> get() {
|
||||
return ImmutableList.copyOf(orders.values());
|
||||
}
|
||||
}
|
||||
}
|
||||
206
src/main/java/ru/lionarius/impl/PlainCurrencyExchange.java
Normal file
206
src/main/java/ru/lionarius/impl/PlainCurrencyExchange.java
Normal file
@@ -0,0 +1,206 @@
|
||||
package ru.lionarius.impl;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import ru.lionarius.api.CurrencyExchange;
|
||||
import ru.lionarius.api.client.Client;
|
||||
import ru.lionarius.api.client.ClientRepository;
|
||||
import ru.lionarius.api.currency.Currency;
|
||||
import ru.lionarius.api.currency.CurrencyPair;
|
||||
import ru.lionarius.api.order.Order;
|
||||
import ru.lionarius.api.order.OrderData;
|
||||
import ru.lionarius.api.order.OrderType;
|
||||
import ru.lionarius.api.order.OrderView;
|
||||
import ru.lionarius.api.order.message.OrderClosedMessage;
|
||||
import ru.lionarius.api.order.message.OrderFilledMessage;
|
||||
import ru.lionarius.api.order.message.OrderMessage;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
public class PlainCurrencyExchange implements CurrencyExchange {
|
||||
private final ClientRepository clientRepository = new InMemoryClientRepository();
|
||||
private final Map<CurrencyPair, OrderBook> orderBooks;
|
||||
|
||||
private final Set<Currency> allowedCurrencies;
|
||||
private final Set<CurrencyPair> allowedPairs;
|
||||
|
||||
public PlainCurrencyExchange(Set<Currency> allowedCurrencies, Set<CurrencyPair> allowedPairs) {
|
||||
this.allowedCurrencies = ImmutableSet.copyOf(allowedCurrencies);
|
||||
this.allowedPairs = ImmutableSet.copyOf(allowedPairs);
|
||||
|
||||
this.orderBooks = allowedPairs.stream()
|
||||
.map(pair -> new AbstractMap.SimpleEntry<>(pair, new OrderBook()))
|
||||
.collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Client> createClient(String name) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (name == null)
|
||||
throw new IllegalArgumentException("Name cannot be null");
|
||||
|
||||
return clientRepository.createClient(name);
|
||||
}, Runnable::run);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<UUID> placeOrder(UUID clientId, CurrencyPair pair, OrderType type, double price, double quantity) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (clientId == null)
|
||||
throw new IllegalArgumentException("Client ID cannot be null");
|
||||
|
||||
if (pair == null)
|
||||
throw new IllegalArgumentException("Currency pair cannot be null");
|
||||
|
||||
if (type == null)
|
||||
throw new IllegalArgumentException("Order type cannot be null");
|
||||
|
||||
if (price <= 0.0)
|
||||
throw new IllegalArgumentException("Price must be positive");
|
||||
|
||||
if (quantity <= 0.0)
|
||||
throw new IllegalArgumentException("Quantity must be positive");
|
||||
|
||||
var orders = clientRepository.getClientOrders(clientId).orElseThrow();
|
||||
|
||||
var order = new Order(clientId, type, pair, new OrderData(price, quantity));
|
||||
orders.add(order);
|
||||
|
||||
var orderBook = orderBooks.get(pair);
|
||||
|
||||
orderBook.addOrder(order);
|
||||
orderBook.matchOrders();
|
||||
|
||||
return order.getId();
|
||||
}, Runnable::run);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> cancelOrder(UUID clientId, UUID orderId) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
if (clientId == null)
|
||||
throw new IllegalArgumentException("Client ID cannot be null");
|
||||
|
||||
if (orderId == null)
|
||||
throw new IllegalArgumentException("Order ID cannot be null");
|
||||
|
||||
var orders = clientRepository.getClientOrders(clientId).orElseThrow();
|
||||
|
||||
orders.get(orderId).ifPresent(order -> {
|
||||
order.pushMessage(new OrderClosedMessage(OrderClosedMessage.Reason.CANCELLED));
|
||||
});
|
||||
}, Runnable::run);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<OrderView>> getOrders(UUID clientId) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (clientId == null)
|
||||
throw new IllegalArgumentException("Client ID cannot be null");
|
||||
|
||||
var orders = clientRepository.getClientOrders(clientId).orElseThrow();
|
||||
|
||||
return orders.get().stream().map(Order::getView).toList();
|
||||
}, Runnable::run);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<OrderMessage>> getOrderMessages(UUID clientId, UUID orderId) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (clientId == null)
|
||||
throw new IllegalArgumentException("Client ID cannot be null");
|
||||
|
||||
if (orderId == null)
|
||||
throw new IllegalArgumentException("Order ID cannot be null");
|
||||
|
||||
var orders = clientRepository.getClientOrders(clientId).orElseThrow();
|
||||
|
||||
return orders.get(orderId).map(Order::getMessages).orElseThrow();
|
||||
}, Runnable::run);
|
||||
}
|
||||
|
||||
private static class OrderBook {
|
||||
private final TreeMap<Double, List<Order>> buyOrders = new TreeMap<>(Collections.reverseOrder());
|
||||
private final TreeMap<Double, List<Order>> sellOrders = new TreeMap<>();
|
||||
private final ReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
|
||||
public void addOrder(Order order) {
|
||||
var orders = order.getType() == OrderType.BUY ? buyOrders : sellOrders;
|
||||
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
orders.computeIfAbsent(order.getLastData().rate(), (k) -> new ArrayList<>()).add(order);
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void matchOrders() {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
while (!buyOrders.isEmpty() && !sellOrders.isEmpty()) {
|
||||
var bestBuyEntry = buyOrders.firstEntry();
|
||||
var bestSellEntry = sellOrders.firstEntry();
|
||||
|
||||
var bestBuy = bestBuyEntry.getValue().getFirst();
|
||||
var bestSell = bestSellEntry.getValue().getFirst();
|
||||
|
||||
if (bestBuy.isClosed()) {
|
||||
bestBuyEntry.getValue().remove(bestBuy);
|
||||
if (bestBuyEntry.getValue().isEmpty())
|
||||
buyOrders.remove(bestBuyEntry.getKey());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bestSell.isClosed()) {
|
||||
bestSellEntry.getValue().remove(bestSell);
|
||||
if (bestSellEntry.getValue().isEmpty())
|
||||
sellOrders.remove(bestSellEntry.getKey());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bestBuy.getLastData().rate() < bestSell.getLastData().rate())
|
||||
break;
|
||||
|
||||
var matchQuantity = Math.min(bestBuy.getLastData().quantity(), bestSell.getLastData().quantity());
|
||||
|
||||
var newBuyOrderData = new OrderData(
|
||||
bestBuy.getLastData().price() - matchQuantity * bestBuy.getLastData().rate(),
|
||||
bestBuy.getLastData().quantity() - matchQuantity
|
||||
);
|
||||
|
||||
var newSellOrderData = new OrderData(
|
||||
bestSell.getLastData().price() - matchQuantity * bestSell.getLastData().rate(),
|
||||
bestSell.getLastData().quantity() - matchQuantity
|
||||
);
|
||||
|
||||
bestBuy.pushMessage(new OrderFilledMessage(bestSell.getId(), newBuyOrderData));
|
||||
bestSell.pushMessage(new OrderFilledMessage(bestBuy.getId(), newSellOrderData));
|
||||
|
||||
if (bestBuy.getLastData().quantity() <= 0) {
|
||||
bestBuyEntry.getValue().remove(bestBuy);
|
||||
bestBuy.pushMessage(new OrderClosedMessage(OrderClosedMessage.Reason.FULFILLED));
|
||||
|
||||
if (bestBuyEntry.getValue().isEmpty())
|
||||
buyOrders.remove(bestBuyEntry.getKey());
|
||||
}
|
||||
|
||||
if (bestSell.getLastData().quantity() <= 0) {
|
||||
bestSellEntry.getValue().remove(bestSell);
|
||||
bestSell.pushMessage(new OrderClosedMessage(OrderClosedMessage.Reason.FULFILLED));
|
||||
|
||||
if (bestSellEntry.getValue().isEmpty())
|
||||
sellOrders.remove(bestSellEntry.getKey());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
334
src/test/java/ConcurrentCurrencyExchangeTest.java
Normal file
334
src/test/java/ConcurrentCurrencyExchangeTest.java
Normal file
@@ -0,0 +1,334 @@
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import ru.lionarius.api.CurrencyExchange;
|
||||
import ru.lionarius.api.client.Client;
|
||||
import ru.lionarius.api.currency.Currency;
|
||||
import ru.lionarius.api.currency.CurrencyPair;
|
||||
import ru.lionarius.api.order.OrderType;
|
||||
import ru.lionarius.impl.PlainCurrencyExchange;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ConcurrentCurrencyExchangeTest {
|
||||
private CurrencyExchange exchange;
|
||||
private Currency RUB;
|
||||
private Currency CNY;
|
||||
private CurrencyPair RUB_CNY;
|
||||
private ExecutorService executorService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
RUB = new Currency("RUB");
|
||||
CNY = new Currency("CNY");
|
||||
RUB_CNY = new CurrencyPair(RUB, CNY);
|
||||
|
||||
exchange = new PlainCurrencyExchange(
|
||||
Set.of(RUB, CNY),
|
||||
Set.of(RUB_CNY)
|
||||
);
|
||||
|
||||
executorService = Executors.newFixedThreadPool(10);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConcurrentClientCreation() throws InterruptedException {
|
||||
int numClients = 100;
|
||||
var latch = new CountDownLatch(numClients);
|
||||
var futures = new ArrayList<CompletableFuture<Client>>();
|
||||
|
||||
for (int i = 0; i < numClients; i++) {
|
||||
var future = CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return exchange.createClient("Client " + UUID.randomUUID()).join();
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
}, executorService);
|
||||
futures.add(future);
|
||||
}
|
||||
|
||||
latch.await(30, TimeUnit.SECONDS);
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||
|
||||
assertEquals(numClients, futures.stream().map(CompletableFuture::join).distinct().count(),
|
||||
"All created clients should be unique");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testConcurrentOrderPlacement() throws InterruptedException {
|
||||
var numSellers = 10;
|
||||
var numBuyers = 10;
|
||||
var ordersPerClient = 20;
|
||||
var latch = new CountDownLatch(numSellers + numBuyers);
|
||||
|
||||
var sellers = new ArrayList<Client>();
|
||||
for (int i = 0; i < numSellers; i++) {
|
||||
var seller = exchange.createClient("Seller " + i).join();
|
||||
sellers.add(seller);
|
||||
}
|
||||
|
||||
var buyers = new ArrayList<Client>();
|
||||
for (int i = 0; i < numBuyers; i++) {
|
||||
var buyer = exchange.createClient("Buyer " + i).join();
|
||||
buyers.add(buyer);
|
||||
}
|
||||
|
||||
var sellerFutures = sellers.stream()
|
||||
.map(seller -> CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
for (int i = 0; i < ordersPerClient; i++) {
|
||||
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 10.0, 10.0).join();
|
||||
Thread.sleep(10);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
}, executorService))
|
||||
.toList();
|
||||
|
||||
var buyerFutures = buyers.stream()
|
||||
.map(buyer -> CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
for (int i = 0; i < ordersPerClient; i++) {
|
||||
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 1.2, 10.0).join();
|
||||
Thread.sleep(10);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
}, executorService))
|
||||
.toList();
|
||||
|
||||
latch.await(30, TimeUnit.SECONDS);
|
||||
CompletableFuture.allOf(
|
||||
Stream.concat(sellerFutures.stream(), buyerFutures.stream())
|
||||
.toArray(CompletableFuture[]::new)
|
||||
).join();
|
||||
|
||||
for (var seller : sellers) {
|
||||
assertTrue((long) exchange.getOrders(seller.id()).join().size() <= ordersPerClient);
|
||||
}
|
||||
|
||||
for (var buyer : buyers) {
|
||||
assertTrue((long) exchange.getOrders(buyer.id()).join().size() <= ordersPerClient);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConcurrentOrderMatchingWithDifferentPrices() throws InterruptedException {
|
||||
var numOrders = 50;
|
||||
var latch = new CountDownLatch(2);
|
||||
|
||||
var seller = exchange.createClient("Seller").join();
|
||||
var buyer = exchange.createClient("Buyer").join();
|
||||
|
||||
var sellerFuture = CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
for (int i = 0; i < numOrders; i++) {
|
||||
var price = 1.0 + (i * 0.01);
|
||||
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, price, 10.0).join();
|
||||
Thread.sleep(5);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
}, executorService);
|
||||
|
||||
var buyerFuture = CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
for (var i = 0; i < numOrders; i++) {
|
||||
var price = 2.0 - (i * 0.01);
|
||||
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, price, 10.0).join();
|
||||
Thread.sleep(5);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
}, executorService);
|
||||
|
||||
latch.await(30, TimeUnit.SECONDS);
|
||||
CompletableFuture.allOf(sellerFuture, buyerFuture).join();
|
||||
|
||||
var sellerOrders = exchange.getOrders(seller.id()).join();
|
||||
var buyerOrders = exchange.getOrders(buyer.id()).join();
|
||||
|
||||
var remainingSellerOrders = sellerOrders.stream().filter(order -> !order.closed()).count();
|
||||
var remainingBuyerOrders = buyerOrders.stream().filter(order -> !order.closed()).count();
|
||||
|
||||
assertTrue(remainingSellerOrders + remainingBuyerOrders < numOrders * 2,
|
||||
"Some orders should have been matched");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBalanceConsistencyWithConcurrentTradesSamePrices() throws InterruptedException {
|
||||
var numTraders = 100;
|
||||
var numOrders = 1000;
|
||||
var latch = new CountDownLatch(numTraders * 2);
|
||||
|
||||
var sellers = new ArrayList<Client>();
|
||||
var buyers = new ArrayList<Client>();
|
||||
var sellAmount = 100.0;
|
||||
var buyAmount = 100.0;
|
||||
|
||||
for (var i = 0; i < numTraders; i++) {
|
||||
var seller = exchange.createClient("Seller " + i).join();
|
||||
var buyer = exchange.createClient("Buyer " + i).join();
|
||||
|
||||
sellers.add(seller);
|
||||
buyers.add(buyer);
|
||||
}
|
||||
|
||||
var futures = new ArrayList<CompletableFuture<Void>>();
|
||||
|
||||
for (var seller : sellers) {
|
||||
var future = CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
for (var i = 0; i < numOrders; i++) {
|
||||
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, buyAmount, sellAmount).join();
|
||||
}
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
}, executorService);
|
||||
futures.add(future);
|
||||
}
|
||||
|
||||
for (var buyer : buyers) {
|
||||
var future = CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
for (var i = 0; i < numOrders; i++) {
|
||||
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, sellAmount, buyAmount).join();
|
||||
}
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
}, executorService);
|
||||
futures.add(future);
|
||||
}
|
||||
|
||||
latch.await(30, TimeUnit.SECONDS);
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||
var totalRubMoney = 0.0;
|
||||
var totalCnyMoney = 0.0;
|
||||
|
||||
for (var seller : sellers) {
|
||||
var orders = exchange.getOrders(seller.id()).join();
|
||||
|
||||
totalCnyMoney += orders.stream().mapToDouble(order -> order.originalData().quantity() - order.lastData().quantity()).sum();
|
||||
totalRubMoney += orders.stream().mapToDouble(order -> order.originalData().price() - order.lastData().price()).sum();
|
||||
}
|
||||
|
||||
assertEquals(numOrders * numTraders * sellAmount, totalCnyMoney, 0.001);
|
||||
assertEquals(numOrders * numTraders * buyAmount, totalRubMoney, 0.001);
|
||||
|
||||
for (var buyer : buyers) {
|
||||
var orders = exchange.getOrders(buyer.id()).join();
|
||||
|
||||
totalCnyMoney -= orders.stream().mapToDouble(order -> order.originalData().quantity() - order.lastData().quantity()).sum();
|
||||
totalRubMoney -= orders.stream().mapToDouble(order -> order.originalData().price() - order.lastData().price()).sum();
|
||||
}
|
||||
|
||||
assertEquals(0.0, totalRubMoney, 0.001);
|
||||
assertEquals(0.0, totalCnyMoney, 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBalanceConsistencyWithConcurrentTrades() throws InterruptedException {
|
||||
var numTraders = 100;
|
||||
var numOrders = 1000;
|
||||
var latch = new CountDownLatch(numTraders * 3);
|
||||
|
||||
var sellers = new ArrayList<Client>();
|
||||
var buyers = new ArrayList<Client>();
|
||||
var sellAmount = 100.0;
|
||||
var buyAmount = 50.0;
|
||||
|
||||
for (var i = 0; i < numTraders; i++) {
|
||||
var seller = exchange.createClient("Seller " + i).join();
|
||||
var buyer = exchange.createClient("Buyer " + i).join();
|
||||
var buyer2 = exchange.createClient("Buyer " + i).join();
|
||||
|
||||
sellers.add(seller);
|
||||
buyers.add(buyer);
|
||||
buyers.add(buyer2);
|
||||
}
|
||||
|
||||
var futures = new ArrayList<CompletableFuture<Void>>();
|
||||
|
||||
for (var seller : sellers) {
|
||||
var future = CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
for (var i = 0; i < numOrders; i++) {
|
||||
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, buyAmount, sellAmount).join();
|
||||
}
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
}, executorService);
|
||||
futures.add(future);
|
||||
}
|
||||
|
||||
for (var buyer : buyers) {
|
||||
var future = CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
for (var i = 0; i < numOrders; i++) {
|
||||
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, sellAmount, buyAmount).join();
|
||||
}
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
}, executorService);
|
||||
futures.add(future);
|
||||
}
|
||||
|
||||
latch.await(30, TimeUnit.SECONDS);
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||
var totalRubMoney = 0.0;
|
||||
var totalCnyMoney = 0.0;
|
||||
|
||||
for (var seller : sellers) {
|
||||
var orders = exchange.getOrders(seller.id()).join();
|
||||
|
||||
totalCnyMoney += orders.stream().mapToDouble(order -> order.originalData().quantity() - order.lastData().quantity()).sum();
|
||||
totalRubMoney += orders.stream().mapToDouble(order -> order.originalData().price() - order.lastData().price()).sum();
|
||||
}
|
||||
|
||||
assertEquals(numOrders * numTraders * sellAmount, totalCnyMoney, 0.001);
|
||||
assertEquals(numOrders * numTraders * buyAmount, totalRubMoney, 0.001);
|
||||
|
||||
for (var buyer : buyers) {
|
||||
var orders = exchange.getOrders(buyer.id()).join();
|
||||
|
||||
totalCnyMoney -= orders.stream().mapToDouble(order -> order.originalData().quantity() - order.lastData().quantity()).sum();
|
||||
}
|
||||
|
||||
assertEquals(0.0, totalCnyMoney, 0.001);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
executorService.shutdown();
|
||||
try {
|
||||
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
executorService.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
executorService.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
228
src/test/java/CurrencyExchangeTest.java
Normal file
228
src/test/java/CurrencyExchangeTest.java
Normal file
@@ -0,0 +1,228 @@
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import ru.lionarius.api.CurrencyExchange;
|
||||
import ru.lionarius.api.currency.Currency;
|
||||
import ru.lionarius.api.currency.CurrencyPair;
|
||||
import ru.lionarius.api.order.OrderData;
|
||||
import ru.lionarius.api.order.OrderType;
|
||||
import ru.lionarius.api.order.message.OrderClosedMessage;
|
||||
import ru.lionarius.api.order.message.OrderFilledMessage;
|
||||
import ru.lionarius.api.order.message.OrderMessageType;
|
||||
import ru.lionarius.impl.PlainCurrencyExchange;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class CurrencyExchangeTest {
|
||||
private CurrencyExchange exchange;
|
||||
private Currency RUB;
|
||||
private Currency CNY;
|
||||
private CurrencyPair RUB_CNY;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
RUB = new Currency("RUB");
|
||||
CNY = new Currency("CNY");
|
||||
RUB_CNY = new CurrencyPair(RUB, CNY);
|
||||
|
||||
exchange = new PlainCurrencyExchange(
|
||||
Set.of(RUB, CNY),
|
||||
Set.of(RUB_CNY)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateClient() throws ExecutionException, InterruptedException {
|
||||
var client = exchange.createClient("Trader").get();
|
||||
|
||||
assertNotNull(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateClientWithNullName() {
|
||||
assertThrows(ExecutionException.class, () -> exchange.createClient(null).get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetBalancesNonexistentClient() {
|
||||
var nonexistentId = UUID.randomUUID();
|
||||
assertThrows(Exception.class, () -> exchange.getOrders(nonexistentId).get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPlaceOrder() throws ExecutionException, InterruptedException {
|
||||
var client = exchange.createClient("Trader").get();
|
||||
|
||||
exchange.placeOrder(client.id(), RUB_CNY, OrderType.SELL, 120, 100.0).get();
|
||||
|
||||
exchange.getOrders(client.id()).get().getFirst();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOrderPriceValidation() throws ExecutionException, InterruptedException {
|
||||
var client = exchange.createClient("Trader").get();
|
||||
|
||||
assertThrows(ExecutionException.class, () ->
|
||||
exchange.placeOrder(client.id(), RUB_CNY, OrderType.SELL, -1.0, 100.0).get()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOrderQuantityValidation() throws ExecutionException, InterruptedException {
|
||||
var client = exchange.createClient("Trader").get();
|
||||
|
||||
assertThrows(ExecutionException.class, () ->
|
||||
exchange.placeOrder(client.id(), RUB_CNY, OrderType.SELL, 120.0, -100.0).get()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testMatchingOrders() throws ExecutionException, InterruptedException {
|
||||
var buyer = exchange.createClient("Buyer").get();
|
||||
var seller = exchange.createClient("Seller").get();
|
||||
|
||||
var sellOrderId = exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
|
||||
var buyOrderId = exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 120.0, 100.0).get();
|
||||
|
||||
var sellOrderMessages = exchange.getOrderMessages(seller.id(), sellOrderId).get();
|
||||
var buyOrderMessages = exchange.getOrderMessages(buyer.id(), buyOrderId).get();
|
||||
|
||||
assertEquals(3, sellOrderMessages.size());
|
||||
assertSame(sellOrderMessages.getLast().type(), OrderMessageType.CLOSED);
|
||||
assertEquals(3, buyOrderMessages.size());
|
||||
assertSame(buyOrderMessages.getLast().type(), OrderMessageType.CLOSED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPartialOrderMatching() throws ExecutionException, InterruptedException {
|
||||
var buyer = exchange.createClient("Buyer").get();
|
||||
var seller = exchange.createClient("Seller").get();
|
||||
|
||||
var sellOrderId = exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
|
||||
var buyOrderId = exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 60.0, 50.0).get();
|
||||
|
||||
var buyOrderMessages = exchange.getOrderMessages(buyer.id(), buyOrderId).get();
|
||||
var sellOrderMessages = exchange.getOrderMessages(seller.id(), sellOrderId).get();
|
||||
|
||||
assertEquals(2, sellOrderMessages.size());
|
||||
assertSame(sellOrderMessages.getLast().type(), OrderMessageType.FILLED);
|
||||
assertEquals(new OrderData(60.0, 50.0), ((OrderFilledMessage) sellOrderMessages.getLast()).newData());
|
||||
assertEquals(3, buyOrderMessages.size());
|
||||
assertSame(buyOrderMessages.getLast().type(), OrderMessageType.CLOSED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPartialOrderMatchingMultiple() throws ExecutionException, InterruptedException {
|
||||
var buyer1 = exchange.createClient("Buyer1").get();
|
||||
var buyer2 = exchange.createClient("Buyer2").get();
|
||||
var seller = exchange.createClient("Seller").get();
|
||||
|
||||
var sellOrderId = exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
|
||||
var buyOrderId1 = exchange.placeOrder(buyer1.id(), RUB_CNY, OrderType.BUY, 60.0, 50.0).get();
|
||||
var buyOrderId2 = exchange.placeOrder(buyer2.id(), RUB_CNY, OrderType.BUY, 60.0, 50.0).get();
|
||||
|
||||
var sellOrderMessages = exchange.getOrderMessages(seller.id(), sellOrderId).get();
|
||||
var buyOrder1Messages = exchange.getOrderMessages(buyer1.id(), buyOrderId1).get();
|
||||
var buyOrder2Messages = exchange.getOrderMessages(buyer2.id(), buyOrderId2).get();
|
||||
|
||||
assertEquals(4, sellOrderMessages.size());
|
||||
assertSame(sellOrderMessages.getLast().type(), OrderMessageType.CLOSED);
|
||||
assertEquals(3, buyOrder1Messages.size());
|
||||
assertSame(buyOrder1Messages.getLast().type(), OrderMessageType.CLOSED);
|
||||
assertEquals(3, buyOrder2Messages.size());
|
||||
assertSame(buyOrder2Messages.getLast().type(), OrderMessageType.CLOSED);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testPricePriorityInOrderMatching() throws ExecutionException, InterruptedException {
|
||||
var buyer = exchange.createClient("Buyer").get();
|
||||
var seller1 = exchange.createClient("Seller1").get();
|
||||
var seller2 = exchange.createClient("Seller2").get();
|
||||
|
||||
var sellOrderId1 = exchange.placeOrder(seller1.id(), RUB_CNY, OrderType.SELL, 75, 50.0).get();
|
||||
var sellOrderId2 = exchange.placeOrder(seller2.id(), RUB_CNY, OrderType.SELL, 60, 50.0).get();
|
||||
|
||||
var buyOrderId = exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 75, 50.0).get();
|
||||
|
||||
var buyOrderMessages = exchange.getOrderMessages(buyer.id(), buyOrderId).get();
|
||||
var sellOrder1Messages = exchange.getOrderMessages(seller1.id(), sellOrderId1).get();
|
||||
var sellOrder2Messages = exchange.getOrderMessages(seller2.id(), sellOrderId2).get();
|
||||
|
||||
assertEquals(1, sellOrder1Messages.size());
|
||||
assertSame(sellOrder1Messages.getLast().type(), OrderMessageType.CREATED);
|
||||
assertEquals(3, sellOrder2Messages.size());
|
||||
assertSame(sellOrder2Messages.getLast().type(), OrderMessageType.CLOSED);
|
||||
assertEquals(3, buyOrderMessages.size());
|
||||
assertSame(buyOrderMessages.getLast().type(), OrderMessageType.CLOSED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPricePriorityWithMultipleOrders() throws ExecutionException, InterruptedException {
|
||||
var buyer = exchange.createClient("Buyer").get();
|
||||
var seller1 = exchange.createClient("Seller1").get();
|
||||
var seller2 = exchange.createClient("Seller2").get();
|
||||
var seller3 = exchange.createClient("Seller3").get();
|
||||
|
||||
var sellOrderId1 = exchange.placeOrder(seller1.id(), RUB_CNY, OrderType.SELL, 75.0, 50.0).get();
|
||||
var sellOrderId2 = exchange.placeOrder(seller2.id(), RUB_CNY, OrderType.SELL, 65.0, 50.0).get();
|
||||
var sellOrderId3 = exchange.placeOrder(seller3.id(), RUB_CNY, OrderType.SELL, 60.0, 50.0).get();
|
||||
|
||||
var buyOrderId = exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 150.0, 100.0).get();
|
||||
|
||||
var buyOrderMessages = exchange.getOrderMessages(buyer.id(), buyOrderId).get();
|
||||
var sellOrder1Messages = exchange.getOrderMessages(seller1.id(), sellOrderId1).get();
|
||||
var sellOrder2Messages = exchange.getOrderMessages(seller2.id(), sellOrderId2).get();
|
||||
var sellOrder3Messages = exchange.getOrderMessages(seller3.id(), sellOrderId3).get();
|
||||
|
||||
assertEquals(1, sellOrder1Messages.size());
|
||||
assertSame(sellOrder1Messages.getLast().type(), OrderMessageType.CREATED);
|
||||
assertEquals(3, sellOrder2Messages.size());
|
||||
assertSame(sellOrder2Messages.getLast().type(), OrderMessageType.CLOSED);
|
||||
assertEquals(3, sellOrder3Messages.size());
|
||||
assertSame(sellOrder3Messages.getLast().type(), OrderMessageType.CLOSED);
|
||||
assertEquals(4, buyOrderMessages.size());
|
||||
assertSame(buyOrderMessages.getLast().type(), OrderMessageType.CLOSED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPriceOrderNotMatching() throws ExecutionException, InterruptedException {
|
||||
var buyer1 = exchange.createClient("Buyer1").get();
|
||||
var buyer2 = exchange.createClient("Buyer2").get();
|
||||
var seller = exchange.createClient("Seller").get();
|
||||
|
||||
var buyOrderId1 = exchange.placeOrder(buyer1.id(), RUB_CNY, OrderType.BUY, 80.0, 50.0).get();
|
||||
var buyOrderId2 = exchange.placeOrder(buyer2.id(), RUB_CNY, OrderType.BUY, 75.0, 50.0).get();
|
||||
|
||||
var sellOrderId = exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 85, 50.0).get();
|
||||
|
||||
var buyOrderMessages = exchange.getOrderMessages(buyer1.id(), buyOrderId1).get();
|
||||
var buyOrderMessages2 = exchange.getOrderMessages(buyer2.id(), buyOrderId2).get();
|
||||
var sellOrderMessages = exchange.getOrderMessages(seller.id(), sellOrderId).get();
|
||||
|
||||
assertEquals(1, sellOrderMessages.size());
|
||||
assertEquals(1, buyOrderMessages.size());
|
||||
assertEquals(1, buyOrderMessages2.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelBuyOrder() throws ExecutionException, InterruptedException {
|
||||
var client = exchange.createClient("Trader").get();
|
||||
|
||||
var orderId = exchange.placeOrder(client.id(), RUB_CNY, OrderType.BUY, 100.0, 200.0).get();
|
||||
|
||||
exchange.cancelOrder(client.id(), orderId).get();
|
||||
|
||||
var orderMessages = exchange.getOrderMessages(client.id(), orderId).get();
|
||||
|
||||
assertEquals(2, orderMessages.size());
|
||||
assertSame(orderMessages.getLast().type(), OrderMessageType.CLOSED);
|
||||
assertSame(((OrderClosedMessage) orderMessages.getLast()).reason(), OrderClosedMessage.Reason.CANCELLED);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user