lab9 again...
This commit is contained in:
@@ -1,28 +1,23 @@
|
|||||||
package ru.lionarius.api;
|
package ru.lionarius.api;
|
||||||
|
|
||||||
import ru.lionarius.api.client.Client;
|
import ru.lionarius.api.client.Client;
|
||||||
import ru.lionarius.api.currency.Currency;
|
|
||||||
import ru.lionarius.api.currency.CurrencyPair;
|
import ru.lionarius.api.currency.CurrencyPair;
|
||||||
import ru.lionarius.api.order.Order;
|
|
||||||
import ru.lionarius.api.order.OrderType;
|
import ru.lionarius.api.order.OrderType;
|
||||||
|
import ru.lionarius.api.order.OrderView;
|
||||||
|
import ru.lionarius.api.order.message.OrderMessage;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
public interface CurrencyExchange {
|
public interface CurrencyExchange {
|
||||||
CompletableFuture<Client> createClient(String name);
|
CompletableFuture<Client> createClient(String name);
|
||||||
|
|
||||||
CompletableFuture<Void> deposit(UUID clientId, Currency currency, double amount);
|
CompletableFuture<UUID> placeOrder(UUID clientId, CurrencyPair pair, OrderType type, double price, double quantity);
|
||||||
|
|
||||||
CompletableFuture<Void> withdraw(UUID clientId, Currency currency, double amount);
|
|
||||||
|
|
||||||
CompletableFuture<Optional<Order>> placeOrder(UUID clientId, CurrencyPair pair, OrderType type, double price, double quantity);
|
|
||||||
|
|
||||||
CompletableFuture<Void> cancelOrder(UUID clientId, UUID orderId);
|
CompletableFuture<Void> cancelOrder(UUID clientId, UUID orderId);
|
||||||
|
|
||||||
CompletableFuture<Iterable<Order>> getActiveOrders(UUID clientId);
|
CompletableFuture<List<OrderView>> getOrders(UUID clientId);
|
||||||
|
|
||||||
CompletableFuture<Map<Currency, Double>> getBalances(UUID clientId);
|
CompletableFuture<List<OrderMessage>> getOrderMessages(UUID clientId, UUID orderId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
package ru.lionarius.api.client;
|
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
|
||||||
import ru.lionarius.api.currency.Currency;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
public class Balance {
|
|
||||||
private final Map<Currency, Double> balances = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
public Map<Currency, Double> getBalances() {
|
|
||||||
return ImmutableMap.copyOf(balances);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getBalance(Currency currency) {
|
|
||||||
return balances.getOrDefault(currency, 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deposit(Currency currency, double amount) {
|
|
||||||
balances.compute(currency, (k, v) -> v == null ? amount : v + amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void withdraw(Currency currency, double amount) {
|
|
||||||
balances.compute(currency, (k, v) -> v == null ? -amount : v - amount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
package ru.lionarius.api.client;
|
package ru.lionarius.api.client;
|
||||||
|
|
||||||
|
|
||||||
|
import ru.lionarius.api.order.OrderList;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -8,5 +11,5 @@ public interface ClientRepository {
|
|||||||
|
|
||||||
Optional<Client> getClient(UUID clientId);
|
Optional<Client> getClient(UUID clientId);
|
||||||
|
|
||||||
Optional<Balance> getClientBalance(UUID clientId);
|
Optional<OrderList> getClientOrders(UUID clientId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,119 @@
|
|||||||
package ru.lionarius.api.order;
|
package ru.lionarius.api.order;
|
||||||
|
|
||||||
import ru.lionarius.api.client.Client;
|
import com.google.common.collect.ImmutableList;
|
||||||
import ru.lionarius.api.currency.CurrencyPair;
|
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.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.locks.ReadWriteLock;
|
||||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
|
||||||
public record Order(UUID id, Client client, OrderType type, CurrencyPair pair, double price, double quantity,
|
public class Order {
|
||||||
LocalDateTime placedAt) {
|
private final UUID id = UUID.randomUUID();
|
||||||
public Order(Client client, OrderType type, CurrencyPair pair, double price, double quantity) {
|
private final UUID clientId;
|
||||||
this(UUID.randomUUID(), client, type, pair, price, quantity, LocalDateTime.now());
|
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 double rate() {
|
public OrderView getView() {
|
||||||
return price() / quantity();
|
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
|
@Override
|
||||||
@@ -24,9 +124,4 @@ public record Order(UUID id, Client client, OrderType type, CurrencyPair pair, d
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return id.hashCode();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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();
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
package ru.lionarius.impl;
|
package ru.lionarius.impl;
|
||||||
|
|
||||||
import ru.lionarius.api.client.Balance;
|
import com.google.common.collect.ImmutableList;
|
||||||
import ru.lionarius.api.client.Client;
|
import ru.lionarius.api.client.Client;
|
||||||
import ru.lionarius.api.client.ClientRepository;
|
import ru.lionarius.api.client.ClientRepository;
|
||||||
|
import ru.lionarius.api.order.OrderList;
|
||||||
|
import ru.lionarius.api.order.Order;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.*;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
public class InMemoryClientRepository implements ClientRepository {
|
public class InMemoryClientRepository implements ClientRepository {
|
||||||
private final Map<UUID, Client> clients = new ConcurrentHashMap<>();
|
private final Map<UUID, Client> clients = new ConcurrentHashMap<>();
|
||||||
private final Map<Client, Balance> balances = new ConcurrentHashMap<>();
|
private final Map<UUID, OrdersList> orders = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Client createClient(String name) {
|
public Client createClient(String name) {
|
||||||
var client = new Client(name);
|
var client = new Client(name);
|
||||||
|
|
||||||
clients.put(client.id(), client);
|
clients.put(client.id(), client);
|
||||||
balances.put(client, new Balance());
|
orders.put(client.id(), new OrdersList());
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,23 @@ public class InMemoryClientRepository implements ClientRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<Balance> getClientBalance(UUID clientId) {
|
public Optional<OrderList> getClientOrders(UUID clientId) {
|
||||||
return Optional.ofNullable(balances.get(clients.get(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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,39 @@
|
|||||||
package ru.lionarius.impl;
|
package ru.lionarius.impl;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
import ru.lionarius.api.CurrencyExchange;
|
import ru.lionarius.api.CurrencyExchange;
|
||||||
import ru.lionarius.api.client.Balance;
|
|
||||||
import ru.lionarius.api.client.Client;
|
import ru.lionarius.api.client.Client;
|
||||||
import ru.lionarius.api.client.ClientRepository;
|
import ru.lionarius.api.client.ClientRepository;
|
||||||
import ru.lionarius.api.currency.Currency;
|
import ru.lionarius.api.currency.Currency;
|
||||||
import ru.lionarius.api.currency.CurrencyPair;
|
import ru.lionarius.api.currency.CurrencyPair;
|
||||||
import ru.lionarius.api.order.Order;
|
import ru.lionarius.api.order.Order;
|
||||||
|
import ru.lionarius.api.order.OrderData;
|
||||||
import ru.lionarius.api.order.OrderType;
|
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.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.locks.ReadWriteLock;
|
import java.util.concurrent.locks.ReadWriteLock;
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
|
||||||
public class PlainCurrencyExchange implements CurrencyExchange {
|
public class PlainCurrencyExchange implements CurrencyExchange {
|
||||||
private final ClientRepository clientRepository = new InMemoryClientRepository();
|
private final ClientRepository clientRepository = new InMemoryClientRepository();
|
||||||
private final Map<CurrencyPair, List<Order>> buyOrders = new ConcurrentHashMap<>();
|
private final Map<CurrencyPair, OrderBook> orderBooks;
|
||||||
private final Map<CurrencyPair, List<Order>> sellOrders = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
private final ReadWriteLock ordersLock = new ReentrantReadWriteLock();
|
private final Set<Currency> allowedCurrencies;
|
||||||
|
private final Set<CurrencyPair> allowedPairs;
|
||||||
|
|
||||||
private final List<Currency> allowedCurrencies;
|
public PlainCurrencyExchange(Set<Currency> allowedCurrencies, Set<CurrencyPair> allowedPairs) {
|
||||||
private final List<CurrencyPair> allowedPairs;
|
this.allowedCurrencies = ImmutableSet.copyOf(allowedCurrencies);
|
||||||
|
this.allowedPairs = ImmutableSet.copyOf(allowedPairs);
|
||||||
|
|
||||||
public PlainCurrencyExchange(List<Currency> allowedCurrencies, List<CurrencyPair> allowedPairs) {
|
this.orderBooks = allowedPairs.stream()
|
||||||
this.allowedCurrencies = ImmutableList.copyOf(allowedCurrencies);
|
.map(pair -> new AbstractMap.SimpleEntry<>(pair, new OrderBook()))
|
||||||
this.allowedPairs = ImmutableList.copyOf(allowedPairs);
|
.collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -42,322 +47,160 @@ public class PlainCurrencyExchange implements CurrencyExchange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<Void> deposit(UUID clientId, Currency currency, double amount) {
|
public CompletableFuture<UUID> placeOrder(UUID clientId, CurrencyPair pair, OrderType type, double price, double quantity) {
|
||||||
return CompletableFuture.runAsync(() -> {
|
|
||||||
validateDepositWithdrawInputs(currency, amount);
|
|
||||||
|
|
||||||
var client = clientRepository.getClient(clientId).orElseThrow();
|
|
||||||
var balance = clientRepository.getClientBalance(clientId).orElseThrow();
|
|
||||||
|
|
||||||
balance.deposit(currency, amount);
|
|
||||||
}, Runnable::run);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Void> withdraw(UUID clientId, Currency currency, double amount) {
|
|
||||||
return CompletableFuture.runAsync(() -> {
|
|
||||||
validateDepositWithdrawInputs(currency, amount);
|
|
||||||
|
|
||||||
var balance = clientRepository.getClientBalance(clientId).orElseThrow();
|
|
||||||
|
|
||||||
var balanceForCurrency = balance.getBalance(currency);
|
|
||||||
if (balanceForCurrency < amount)
|
|
||||||
throw new IllegalArgumentException("Not enough balance");
|
|
||||||
|
|
||||||
balance.withdraw(currency, amount);
|
|
||||||
}, Runnable::run);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateDepositWithdrawInputs(Currency currency, double amount) {
|
|
||||||
if (currency == null)
|
|
||||||
throw new IllegalArgumentException("Currency cannot be null");
|
|
||||||
|
|
||||||
if (amount < 0)
|
|
||||||
throw new IllegalArgumentException("Amount cannot be negative");
|
|
||||||
|
|
||||||
if (!allowedCurrencies.contains(currency))
|
|
||||||
throw new IllegalArgumentException("Currency is not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Optional<Order>> placeOrder(UUID clientId, CurrencyPair pair, OrderType type, double price, double quantity) {
|
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
validateOrderInputs(pair, type, price, quantity);
|
if (clientId == null)
|
||||||
|
throw new IllegalArgumentException("Client ID cannot be null");
|
||||||
|
|
||||||
var client = clientRepository.getClient(clientId).orElseThrow();
|
if (pair == null)
|
||||||
var balance = clientRepository.getClientBalance(clientId).orElseThrow();
|
throw new IllegalArgumentException("Currency pair cannot be null");
|
||||||
|
|
||||||
reserveFundsForOrder(balance, pair, type, price, quantity);
|
if (type == null)
|
||||||
|
throw new IllegalArgumentException("Order type cannot be null");
|
||||||
|
|
||||||
var newOrder = new Order(client, type, pair, price, quantity);
|
if (price <= 0.0)
|
||||||
Optional<Order> orderToAdd = Optional.empty();
|
throw new IllegalArgumentException("Price must be positive");
|
||||||
|
|
||||||
ordersLock.writeLock().lock();
|
if (quantity <= 0.0)
|
||||||
try {
|
throw new IllegalArgumentException("Quantity must be positive");
|
||||||
// Try to match the order
|
|
||||||
if (type == OrderType.BUY) {
|
|
||||||
orderToAdd = matchBuyOrder(newOrder);
|
|
||||||
} else {
|
|
||||||
orderToAdd = matchSellOrder(newOrder);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If order is not fully filled, add to order books
|
var orders = clientRepository.getClientOrders(clientId).orElseThrow();
|
||||||
if (orderToAdd.isPresent()) {
|
|
||||||
if (type == OrderType.BUY) {
|
|
||||||
buyOrders.computeIfAbsent(pair, k -> new ArrayList<>()).add(orderToAdd.get());
|
|
||||||
} else {
|
|
||||||
sellOrders.computeIfAbsent(pair, k -> new ArrayList<>()).add(orderToAdd.get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return orderToAdd;
|
var order = new Order(clientId, type, pair, new OrderData(price, quantity));
|
||||||
} finally {
|
orders.add(order);
|
||||||
ordersLock.writeLock().unlock();
|
|
||||||
}
|
var orderBook = orderBooks.get(pair);
|
||||||
|
|
||||||
|
orderBook.addOrder(order);
|
||||||
|
orderBook.matchOrders();
|
||||||
|
|
||||||
|
return order.getId();
|
||||||
}, Runnable::run);
|
}, Runnable::run);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<Void> cancelOrder(UUID clientId, UUID orderId) {
|
public CompletableFuture<Void> cancelOrder(UUID clientId, UUID orderId) {
|
||||||
return CompletableFuture.runAsync(() -> {
|
return CompletableFuture.runAsync(() -> {
|
||||||
if (clientId == null || orderId == null)
|
if (clientId == null)
|
||||||
throw new IllegalArgumentException("ClientId and orderId cannot be null");
|
throw new IllegalArgumentException("Client ID cannot be null");
|
||||||
|
|
||||||
var client = clientRepository.getClient(clientId).orElseThrow();
|
if (orderId == null)
|
||||||
var balance = clientRepository.getClientBalance(clientId).orElseThrow();
|
throw new IllegalArgumentException("Order ID cannot be null");
|
||||||
|
|
||||||
ordersLock.writeLock().lock();
|
var orders = clientRepository.getClientOrders(clientId).orElseThrow();
|
||||||
var toCancel = new ArrayList<Order>();
|
|
||||||
try {
|
|
||||||
for (var orderList : buyOrders.values()) {
|
|
||||||
if (orderList != null) {
|
|
||||||
for (var order : orderList) {
|
|
||||||
if (order.id().equals(orderId) && order.client().id().equals(client.id())) {
|
|
||||||
toCancel.add(order);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (var orderList : sellOrders.values()) {
|
|
||||||
for (var order : orderList) {
|
|
||||||
if (order.id().equals(orderId) && order.client().id().equals(client.id())) {
|
|
||||||
toCancel.add(order);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var order : toCancel) {
|
orders.get(orderId).ifPresent(order -> {
|
||||||
if (order.type() == OrderType.BUY)
|
order.pushMessage(new OrderClosedMessage(OrderClosedMessage.Reason.CANCELLED));
|
||||||
buyOrders.get(order.pair()).remove(order);
|
});
|
||||||
else
|
|
||||||
sellOrders.get(order.pair()).remove(order);
|
|
||||||
|
|
||||||
refundOrder(balance, order);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
ordersLock.writeLock().unlock();
|
|
||||||
}
|
|
||||||
}, Runnable::run);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reserveFundsForOrder(Balance balance, CurrencyPair pair, OrderType type, double price, double quantity) {
|
|
||||||
if (type == OrderType.BUY && balance.getBalance(pair.base()) < price) {
|
|
||||||
throw new IllegalArgumentException("Insufficient funds for buy order");
|
|
||||||
} else if (type == OrderType.SELL && balance.getBalance(pair.quote()) < quantity) {
|
|
||||||
throw new IllegalArgumentException("Insufficient funds for sell order");
|
|
||||||
}
|
|
||||||
// Deduct funds for order placement
|
|
||||||
balance.withdraw(type == OrderType.BUY ? pair.base() : pair.quote(), type == OrderType.BUY ? price : quantity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refundOrder(Balance balance, Order order) {
|
|
||||||
if (order.type() == OrderType.BUY) {
|
|
||||||
balance.deposit(order.pair().base(), order.price());
|
|
||||||
} else {
|
|
||||||
balance.deposit(order.pair().quote(), order.quantity());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateOrderInputs(CurrencyPair pair, OrderType type, double price, double quantity) {
|
|
||||||
if (pair == null || type == null || price < 0 || quantity < 0) {
|
|
||||||
throw new IllegalArgumentException("Invalid order parameters");
|
|
||||||
}
|
|
||||||
if (!allowedPairs.contains(pair)) {
|
|
||||||
throw new IllegalArgumentException("Pair is not allowed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Order> getMatchingOrders(Order order) {
|
|
||||||
List<Order> oppositeOrders = order.type() == OrderType.BUY ?
|
|
||||||
sellOrders.getOrDefault(order.pair(), new ArrayList<>()) :
|
|
||||||
buyOrders.getOrDefault(order.pair(), new ArrayList<>());
|
|
||||||
|
|
||||||
double orderRate = order.rate();
|
|
||||||
|
|
||||||
return oppositeOrders.stream()
|
|
||||||
.filter(oppositeOrder -> {
|
|
||||||
double oppositeRate = oppositeOrder.rate();
|
|
||||||
return order.type() == OrderType.BUY ?
|
|
||||||
oppositeRate <= orderRate :
|
|
||||||
oppositeRate >= orderRate;
|
|
||||||
})
|
|
||||||
.filter(oppositeOrder -> oppositeOrder.client() != order.client())
|
|
||||||
.sorted((o1, o2) -> {
|
|
||||||
double rate1 = o1.rate();
|
|
||||||
double rate2 = o2.rate();
|
|
||||||
int rateCompare = order.type() == OrderType.BUY ?
|
|
||||||
Double.compare(rate1, rate2) : // For buy orders - ascending rate
|
|
||||||
Double.compare(rate2, rate1); // For sell orders - descending rate
|
|
||||||
return rateCompare != 0 ? rateCompare : o1.placedAt().compareTo(o2.placedAt());
|
|
||||||
})
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<Order> matchBuyOrder(Order buyOrder) {
|
|
||||||
var remainingBuyOrder = buyOrder;
|
|
||||||
List<Order> matchingOrders = getMatchingOrders(buyOrder);
|
|
||||||
|
|
||||||
for (Order sellOrder : matchingOrders) {
|
|
||||||
if (remainingBuyOrder.quantity() <= 0.0) break;
|
|
||||||
|
|
||||||
// Calculate matching quantity based on remaining quote currency amount
|
|
||||||
double matchQuantity = Math.min(remainingBuyOrder.quantity(), sellOrder.quantity());
|
|
||||||
// Calculate corresponding base currency amount using sell order's rate
|
|
||||||
double matchPrice = matchQuantity * sellOrder.rate();
|
|
||||||
|
|
||||||
// Create new orders with reduced quantities
|
|
||||||
var newBuyOrder = new Order(
|
|
||||||
remainingBuyOrder.client(),
|
|
||||||
remainingBuyOrder.type(),
|
|
||||||
remainingBuyOrder.pair(),
|
|
||||||
remainingBuyOrder.price() - matchQuantity * buyOrder.rate(),
|
|
||||||
remainingBuyOrder.quantity() - matchQuantity
|
|
||||||
);
|
|
||||||
|
|
||||||
var newSellOrder = new Order(
|
|
||||||
sellOrder.client(),
|
|
||||||
sellOrder.type(),
|
|
||||||
sellOrder.pair(),
|
|
||||||
sellOrder.price() - matchPrice,
|
|
||||||
sellOrder.quantity() - matchQuantity
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute the trade
|
|
||||||
executeTrade(remainingBuyOrder, sellOrder, matchQuantity);
|
|
||||||
|
|
||||||
// Update orders in the books
|
|
||||||
remainingBuyOrder = newBuyOrder;
|
|
||||||
sellOrders.get(buyOrder.pair()).remove(sellOrder);
|
|
||||||
if (newSellOrder.quantity() > 0) {
|
|
||||||
sellOrders.get(buyOrder.pair()).add(newSellOrder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remainingBuyOrder.quantity() > 0) {
|
|
||||||
return Optional.of(remainingBuyOrder);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<Order> matchSellOrder(Order sellOrder) {
|
|
||||||
var remainingSellOrder = sellOrder;
|
|
||||||
List<Order> matchingOrders = getMatchingOrders(sellOrder);
|
|
||||||
|
|
||||||
for (Order buyOrder : matchingOrders) {
|
|
||||||
if (remainingSellOrder.quantity() <= 0.0) break;
|
|
||||||
|
|
||||||
// Calculate matching quantity based on remaining quote currency amount
|
|
||||||
double matchQuantity = Math.min(remainingSellOrder.quantity(), buyOrder.quantity());
|
|
||||||
// Calculate corresponding base currency amount using buy order's rate
|
|
||||||
double matchPrice = (matchQuantity * buyOrder.price()) / buyOrder.quantity();
|
|
||||||
|
|
||||||
// Create new orders with reduced quantities
|
|
||||||
var newSellOrder = new Order(
|
|
||||||
remainingSellOrder.client(),
|
|
||||||
remainingSellOrder.type(),
|
|
||||||
remainingSellOrder.pair(),
|
|
||||||
remainingSellOrder.price() - matchPrice,
|
|
||||||
remainingSellOrder.quantity() - matchQuantity
|
|
||||||
);
|
|
||||||
|
|
||||||
var newBuyOrder = new Order(
|
|
||||||
buyOrder.client(),
|
|
||||||
buyOrder.type(),
|
|
||||||
buyOrder.pair(),
|
|
||||||
buyOrder.price() - matchPrice,
|
|
||||||
buyOrder.quantity() - matchQuantity
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute the trade
|
|
||||||
executeTrade(buyOrder, remainingSellOrder, matchQuantity);
|
|
||||||
|
|
||||||
// Update orders in the books
|
|
||||||
remainingSellOrder = newSellOrder;
|
|
||||||
buyOrders.get(sellOrder.pair()).remove(buyOrder);
|
|
||||||
if (newBuyOrder.quantity() > 0) {
|
|
||||||
buyOrders.get(sellOrder.pair()).add(newBuyOrder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remainingSellOrder.quantity() > 0) {
|
|
||||||
return Optional.of(remainingSellOrder);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void executeTrade(Order buyOrder, Order sellOrder, double quantity) {
|
|
||||||
var price = quantity * sellOrder.rate();
|
|
||||||
|
|
||||||
// Get client balances
|
|
||||||
var buyerBalance = clientRepository.getClientBalance(buyOrder.client().id()).orElseThrow();
|
|
||||||
var sellerBalance = clientRepository.getClientBalance(sellOrder.client().id()).orElseThrow();
|
|
||||||
|
|
||||||
// Transfer currencies
|
|
||||||
buyerBalance.deposit(buyOrder.pair().quote(), quantity); // Buyer receives quote currency
|
|
||||||
sellerBalance.deposit(buyOrder.pair().base(), price); // Seller receives base currency
|
|
||||||
|
|
||||||
buyerBalance.deposit(buyOrder.pair().base(), quantity * buyOrder.rate() - price); // Return price difference to buyer
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<Iterable<Order>> getActiveOrders(UUID clientId) {
|
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
|
||||||
var client = clientRepository.getClient(clientId).orElseThrow();
|
|
||||||
|
|
||||||
ordersLock.readLock().lock();
|
|
||||||
try {
|
|
||||||
List<Order> clientOrders = new ArrayList<>();
|
|
||||||
|
|
||||||
// Collect all buy orders for the client
|
|
||||||
buyOrders.values().forEach(orders ->
|
|
||||||
orders.stream()
|
|
||||||
.filter(order -> order.client().id().equals(clientId))
|
|
||||||
.forEach(clientOrders::add)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Collect all sell orders for the client
|
|
||||||
sellOrders.values().forEach(orders ->
|
|
||||||
orders.stream()
|
|
||||||
.filter(order -> order.client().id().equals(clientId))
|
|
||||||
.forEach(clientOrders::add)
|
|
||||||
);
|
|
||||||
|
|
||||||
return clientOrders;
|
|
||||||
} finally {
|
|
||||||
ordersLock.readLock().unlock();
|
|
||||||
}
|
|
||||||
}, Runnable::run);
|
}, Runnable::run);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<Map<Currency, Double>> getBalances(UUID clientId) {
|
public CompletableFuture<List<OrderView>> getOrders(UUID clientId) {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
var client = clientRepository.getClient(clientId).orElseThrow();
|
if (clientId == null)
|
||||||
var balance = clientRepository.getClientBalance(clientId).orElseThrow();
|
throw new IllegalArgumentException("Client ID cannot be null");
|
||||||
|
|
||||||
return balance.getBalances();
|
var orders = clientRepository.getClientOrders(clientId).orElseThrow();
|
||||||
|
|
||||||
|
return orders.get().stream().map(Order::getView).toList();
|
||||||
}, Runnable::run);
|
}, 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,12 @@ import ru.lionarius.api.CurrencyExchange;
|
|||||||
import ru.lionarius.api.client.Client;
|
import ru.lionarius.api.client.Client;
|
||||||
import ru.lionarius.api.currency.Currency;
|
import ru.lionarius.api.currency.Currency;
|
||||||
import ru.lionarius.api.currency.CurrencyPair;
|
import ru.lionarius.api.currency.CurrencyPair;
|
||||||
import ru.lionarius.api.order.Order;
|
|
||||||
import ru.lionarius.api.order.OrderType;
|
import ru.lionarius.api.order.OrderType;
|
||||||
import ru.lionarius.impl.PlainCurrencyExchange;
|
import ru.lionarius.impl.PlainCurrencyExchange;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import java.util.stream.StreamSupport;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
@@ -31,8 +28,8 @@ class ConcurrentCurrencyExchangeTest {
|
|||||||
RUB_CNY = new CurrencyPair(RUB, CNY);
|
RUB_CNY = new CurrencyPair(RUB, CNY);
|
||||||
|
|
||||||
exchange = new PlainCurrencyExchange(
|
exchange = new PlainCurrencyExchange(
|
||||||
Arrays.asList(RUB, CNY),
|
Set.of(RUB, CNY),
|
||||||
Arrays.asList(RUB_CNY)
|
Set.of(RUB_CNY)
|
||||||
);
|
);
|
||||||
|
|
||||||
executorService = Executors.newFixedThreadPool(10);
|
executorService = Executors.newFixedThreadPool(10);
|
||||||
@@ -63,132 +60,6 @@ class ConcurrentCurrencyExchangeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testConcurrentDeposits() throws InterruptedException {
|
|
||||||
int numThreads = 10;
|
|
||||||
int depositsPerThread = 100;
|
|
||||||
double depositAmount = 10.0;
|
|
||||||
var latch = new CountDownLatch(numThreads);
|
|
||||||
|
|
||||||
var client = exchange.createClient("Test Client").join();
|
|
||||||
|
|
||||||
var futures = new ArrayList<CompletableFuture<Void>>();
|
|
||||||
|
|
||||||
for (int i = 0; i < numThreads; i++) {
|
|
||||||
var future = CompletableFuture.runAsync(() -> {
|
|
||||||
try {
|
|
||||||
for (int j = 0; j < depositsPerThread; j++) {
|
|
||||||
exchange.deposit(client.id(), RUB, depositAmount).join();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
latch.countDown();
|
|
||||||
}
|
|
||||||
}, executorService);
|
|
||||||
futures.add(future);
|
|
||||||
}
|
|
||||||
|
|
||||||
latch.await(30, TimeUnit.SECONDS);
|
|
||||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
|
||||||
|
|
||||||
var balances = exchange.getBalances(client.id()).join();
|
|
||||||
assertEquals(numThreads * depositsPerThread * depositAmount, balances.get(RUB));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testConcurrentWithdrawals() throws InterruptedException {
|
|
||||||
var numThreads = 5;
|
|
||||||
var latch = new CountDownLatch(numThreads);
|
|
||||||
var initialBalance = 1000.0;
|
|
||||||
var withdrawalAmount = 10.0;
|
|
||||||
|
|
||||||
var client = exchange.createClient("Test Client").join();
|
|
||||||
exchange.deposit(client.id(), RUB, initialBalance).join();
|
|
||||||
|
|
||||||
var futures = new ArrayList<CompletableFuture<Void>>();
|
|
||||||
var successfulWithdrawals = new AtomicInteger(0);
|
|
||||||
|
|
||||||
for (int i = 0; i < numThreads; i++) {
|
|
||||||
var future = CompletableFuture.runAsync(() -> {
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
exchange.withdraw(client.id(), RUB, withdrawalAmount).join();
|
|
||||||
successfulWithdrawals.incrementAndGet();
|
|
||||||
} catch (CompletionException e) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
latch.countDown();
|
|
||||||
}
|
|
||||||
}, executorService);
|
|
||||||
futures.add(future);
|
|
||||||
}
|
|
||||||
|
|
||||||
latch.await(30, TimeUnit.SECONDS);
|
|
||||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
|
||||||
|
|
||||||
var finalBalance = exchange.getBalances(client.id()).join();
|
|
||||||
assertEquals(
|
|
||||||
initialBalance,
|
|
||||||
successfulWithdrawals.get() * withdrawalAmount + finalBalance.get(RUB),
|
|
||||||
0.001
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testConcurrentDepositsAndWithdrawals() throws InterruptedException {
|
|
||||||
var numThreads = 10;
|
|
||||||
var initialBalance = 1000.0;
|
|
||||||
var transactionAmount = 10.0;
|
|
||||||
var latch = new CountDownLatch(numThreads * 2);
|
|
||||||
|
|
||||||
var client = exchange.createClient("Test Client").join();
|
|
||||||
exchange.deposit(client.id(), RUB, initialBalance).join();
|
|
||||||
|
|
||||||
var futures = new ArrayList<CompletableFuture<Void>>();
|
|
||||||
var successfulWithdrawals = new AtomicInteger(0);
|
|
||||||
|
|
||||||
for (int i = 0; i < numThreads; i++) {
|
|
||||||
futures.add(CompletableFuture.runAsync(() -> {
|
|
||||||
try {
|
|
||||||
for (int j = 0; j < 100; j++) {
|
|
||||||
exchange.deposit(client.id(), RUB, transactionAmount).join();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
latch.countDown();
|
|
||||||
}
|
|
||||||
}, executorService));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < numThreads; i++) {
|
|
||||||
futures.add(CompletableFuture.runAsync(() -> {
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
exchange.withdraw(client.id(), RUB, transactionAmount).join();
|
|
||||||
successfulWithdrawals.incrementAndGet();
|
|
||||||
} catch (CompletionException e) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
latch.countDown();
|
|
||||||
}
|
|
||||||
}, executorService));
|
|
||||||
}
|
|
||||||
|
|
||||||
latch.await(30, TimeUnit.SECONDS);
|
|
||||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
|
||||||
|
|
||||||
var finalBalances = exchange.getBalances(client.id()).join();
|
|
||||||
assertEquals(
|
|
||||||
initialBalance + (numThreads * 100 * transactionAmount) - (successfulWithdrawals.get() * transactionAmount),
|
|
||||||
finalBalances.get(RUB),
|
|
||||||
0.001
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testConcurrentOrderPlacement() throws InterruptedException {
|
void testConcurrentOrderPlacement() throws InterruptedException {
|
||||||
var numSellers = 10;
|
var numSellers = 10;
|
||||||
@@ -199,14 +70,12 @@ class ConcurrentCurrencyExchangeTest {
|
|||||||
var sellers = new ArrayList<Client>();
|
var sellers = new ArrayList<Client>();
|
||||||
for (int i = 0; i < numSellers; i++) {
|
for (int i = 0; i < numSellers; i++) {
|
||||||
var seller = exchange.createClient("Seller " + i).join();
|
var seller = exchange.createClient("Seller " + i).join();
|
||||||
exchange.deposit(seller.id(), CNY, 1000000.0).join();
|
|
||||||
sellers.add(seller);
|
sellers.add(seller);
|
||||||
}
|
}
|
||||||
|
|
||||||
var buyers = new ArrayList<Client>();
|
var buyers = new ArrayList<Client>();
|
||||||
for (int i = 0; i < numBuyers; i++) {
|
for (int i = 0; i < numBuyers; i++) {
|
||||||
var buyer = exchange.createClient("Buyer " + i).join();
|
var buyer = exchange.createClient("Buyer " + i).join();
|
||||||
exchange.deposit(buyer.id(), RUB, 1200000.0).join();
|
|
||||||
buyers.add(buyer);
|
buyers.add(buyer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,15 +116,11 @@ class ConcurrentCurrencyExchangeTest {
|
|||||||
).join();
|
).join();
|
||||||
|
|
||||||
for (var seller : sellers) {
|
for (var seller : sellers) {
|
||||||
assertTrue(StreamSupport.stream(
|
assertTrue((long) exchange.getOrders(seller.id()).join().size() <= ordersPerClient);
|
||||||
exchange.getActiveOrders(seller.id()).join().spliterator(), false
|
|
||||||
).count() <= ordersPerClient);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var buyer : buyers) {
|
for (var buyer : buyers) {
|
||||||
assertTrue(StreamSupport.stream(
|
assertTrue((long) exchange.getOrders(buyer.id()).join().size() <= ordersPerClient);
|
||||||
exchange.getActiveOrders(buyer.id()).join().spliterator(), false
|
|
||||||
).count() <= ordersPerClient);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,9 +132,6 @@ class ConcurrentCurrencyExchangeTest {
|
|||||||
var seller = exchange.createClient("Seller").join();
|
var seller = exchange.createClient("Seller").join();
|
||||||
var buyer = exchange.createClient("Buyer").join();
|
var buyer = exchange.createClient("Buyer").join();
|
||||||
|
|
||||||
exchange.deposit(seller.id(), CNY, 1000.0).join();
|
|
||||||
exchange.deposit(buyer.id(), RUB, 2000.0).join();
|
|
||||||
|
|
||||||
var sellerFuture = CompletableFuture.runAsync(() -> {
|
var sellerFuture = CompletableFuture.runAsync(() -> {
|
||||||
try {
|
try {
|
||||||
for (int i = 0; i < numOrders; i++) {
|
for (int i = 0; i < numOrders; i++) {
|
||||||
@@ -301,57 +163,19 @@ class ConcurrentCurrencyExchangeTest {
|
|||||||
latch.await(30, TimeUnit.SECONDS);
|
latch.await(30, TimeUnit.SECONDS);
|
||||||
CompletableFuture.allOf(sellerFuture, buyerFuture).join();
|
CompletableFuture.allOf(sellerFuture, buyerFuture).join();
|
||||||
|
|
||||||
var sellerOrders = exchange.getActiveOrders(seller.id()).join();
|
var sellerOrders = exchange.getOrders(seller.id()).join();
|
||||||
var buyerOrders = exchange.getActiveOrders(buyer.id()).join();
|
var buyerOrders = exchange.getOrders(buyer.id()).join();
|
||||||
|
|
||||||
var remainingSellerOrders = StreamSupport.stream(sellerOrders.spliterator(), false).count();
|
var remainingSellerOrders = sellerOrders.stream().filter(order -> !order.closed()).count();
|
||||||
var remainingBuyerOrders = StreamSupport.stream(buyerOrders.spliterator(), false).count();
|
var remainingBuyerOrders = buyerOrders.stream().filter(order -> !order.closed()).count();
|
||||||
|
|
||||||
assertTrue(remainingSellerOrders + remainingBuyerOrders < numOrders * 2,
|
assertTrue(remainingSellerOrders + remainingBuyerOrders < numOrders * 2,
|
||||||
"Some orders should have been matched");
|
"Some orders should have been matched");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testConcurrentBalanceCheck() throws InterruptedException {
|
void testBalanceConsistencyWithConcurrentTradesSamePrices() throws InterruptedException {
|
||||||
var numThreads = 10;
|
var numTraders = 100;
|
||||||
var startLatch = new CountDownLatch(1);
|
|
||||||
var completionLatch = new CountDownLatch(numThreads);
|
|
||||||
|
|
||||||
var client = exchange.createClient("Test Client").join();
|
|
||||||
var initialBalance = 1000.0;
|
|
||||||
exchange.deposit(client.id(), RUB, initialBalance).join();
|
|
||||||
|
|
||||||
var successfulChecks = new AtomicInteger(0);
|
|
||||||
var futures = new ArrayList<CompletableFuture<Void>>();
|
|
||||||
|
|
||||||
for (int i = 0; i < numThreads; i++) {
|
|
||||||
var future = CompletableFuture.runAsync(() -> {
|
|
||||||
try {
|
|
||||||
startLatch.await();
|
|
||||||
var balance = exchange.getBalances(client.id()).join();
|
|
||||||
if (Math.abs(balance.get(RUB) - initialBalance) < 0.001) {
|
|
||||||
successfulChecks.incrementAndGet();
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
} finally {
|
|
||||||
completionLatch.countDown();
|
|
||||||
}
|
|
||||||
}, executorService);
|
|
||||||
futures.add(future);
|
|
||||||
}
|
|
||||||
|
|
||||||
startLatch.countDown();
|
|
||||||
completionLatch.await(30, TimeUnit.SECONDS);
|
|
||||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
|
||||||
|
|
||||||
assertEquals(numThreads, successfulChecks.get(),
|
|
||||||
"All concurrent balance checks should return the correct balance");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testBalanceConsistencyWithConcurrentTrades() throws InterruptedException {
|
|
||||||
var numTraders = 10;
|
|
||||||
var numOrders = 1000;
|
var numOrders = 1000;
|
||||||
var latch = new CountDownLatch(numTraders * 2);
|
var latch = new CountDownLatch(numTraders * 2);
|
||||||
|
|
||||||
@@ -359,14 +183,11 @@ class ConcurrentCurrencyExchangeTest {
|
|||||||
var buyers = new ArrayList<Client>();
|
var buyers = new ArrayList<Client>();
|
||||||
var sellAmount = 100.0;
|
var sellAmount = 100.0;
|
||||||
var buyAmount = 100.0;
|
var buyAmount = 100.0;
|
||||||
|
|
||||||
for (var i = 0; i < numTraders; i++) {
|
for (var i = 0; i < numTraders; i++) {
|
||||||
var seller = exchange.createClient("Seller " + i).join();
|
var seller = exchange.createClient("Seller " + i).join();
|
||||||
var buyer = exchange.createClient("Buyer " + i).join();
|
var buyer = exchange.createClient("Buyer " + i).join();
|
||||||
|
|
||||||
exchange.deposit(seller.id(), CNY, sellAmount * numOrders).join();
|
|
||||||
exchange.deposit(buyer.id(), RUB, buyAmount * numOrders).join();
|
|
||||||
|
|
||||||
sellers.add(seller);
|
sellers.add(seller);
|
||||||
buyers.add(buyer);
|
buyers.add(buyer);
|
||||||
}
|
}
|
||||||
@@ -376,8 +197,7 @@ class ConcurrentCurrencyExchangeTest {
|
|||||||
for (var seller : sellers) {
|
for (var seller : sellers) {
|
||||||
var future = CompletableFuture.runAsync(() -> {
|
var future = CompletableFuture.runAsync(() -> {
|
||||||
try {
|
try {
|
||||||
for (var i = 0; i < numOrders; i++)
|
for (var i = 0; i < numOrders; i++) {
|
||||||
{
|
|
||||||
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, buyAmount, sellAmount).join();
|
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, buyAmount, sellAmount).join();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -390,8 +210,7 @@ class ConcurrentCurrencyExchangeTest {
|
|||||||
for (var buyer : buyers) {
|
for (var buyer : buyers) {
|
||||||
var future = CompletableFuture.runAsync(() -> {
|
var future = CompletableFuture.runAsync(() -> {
|
||||||
try {
|
try {
|
||||||
for (var i = 0; i < numOrders; i++)
|
for (var i = 0; i < numOrders; i++) {
|
||||||
{
|
|
||||||
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, sellAmount, buyAmount).join();
|
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, sellAmount, buyAmount).join();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -405,20 +224,99 @@ class ConcurrentCurrencyExchangeTest {
|
|||||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||||
var totalRubMoney = 0.0;
|
var totalRubMoney = 0.0;
|
||||||
var totalCnyMoney = 0.0;
|
var totalCnyMoney = 0.0;
|
||||||
|
|
||||||
for (var seller : sellers) {
|
for (var seller : sellers) {
|
||||||
var balances = exchange.getBalances(seller.id()).join();
|
var orders = exchange.getOrders(seller.id()).join();
|
||||||
totalCnyMoney += balances.get(CNY);
|
|
||||||
totalRubMoney += balances.get(RUB);
|
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) {
|
for (var buyer : buyers) {
|
||||||
var balances = exchange.getBalances(buyer.id()).join();
|
var orders = exchange.getOrders(buyer.id()).join();
|
||||||
totalCnyMoney += balances.get(CNY);
|
|
||||||
totalRubMoney += balances.get(RUB);
|
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(totalRubMoney, sellAmount * numOrders * numTraders, 0.001);
|
assertEquals(0.0, totalRubMoney, 0.001);
|
||||||
assertEquals(totalCnyMoney, sellAmount * numOrders * numTraders, 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
|
@AfterEach
|
||||||
|
|||||||
@@ -3,11 +3,16 @@ import org.junit.jupiter.api.Test;
|
|||||||
import ru.lionarius.api.CurrencyExchange;
|
import ru.lionarius.api.CurrencyExchange;
|
||||||
import ru.lionarius.api.currency.Currency;
|
import ru.lionarius.api.currency.Currency;
|
||||||
import ru.lionarius.api.currency.CurrencyPair;
|
import ru.lionarius.api.currency.CurrencyPair;
|
||||||
|
import ru.lionarius.api.order.OrderData;
|
||||||
import ru.lionarius.api.order.OrderType;
|
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 ru.lionarius.impl.PlainCurrencyExchange;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
@@ -26,18 +31,16 @@ class CurrencyExchangeTest {
|
|||||||
RUB_CNY = new CurrencyPair(RUB, CNY);
|
RUB_CNY = new CurrencyPair(RUB, CNY);
|
||||||
|
|
||||||
exchange = new PlainCurrencyExchange(
|
exchange = new PlainCurrencyExchange(
|
||||||
Arrays.asList(RUB, CNY),
|
Set.of(RUB, CNY),
|
||||||
Arrays.asList(RUB_CNY)
|
Set.of(RUB_CNY)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCreateClient() throws ExecutionException, InterruptedException {
|
void testCreateClient() throws ExecutionException, InterruptedException {
|
||||||
var client = exchange.createClient("Trader").get();
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
assertNotNull(client);
|
assertNotNull(client);
|
||||||
assertNotNull(client.id());
|
|
||||||
assertEquals("Trader", client.name());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -48,97 +51,22 @@ class CurrencyExchangeTest {
|
|||||||
@Test
|
@Test
|
||||||
void testGetBalancesNonexistentClient() {
|
void testGetBalancesNonexistentClient() {
|
||||||
var nonexistentId = UUID.randomUUID();
|
var nonexistentId = UUID.randomUUID();
|
||||||
assertThrows(Exception.class, () -> exchange.getBalances(nonexistentId).get());
|
assertThrows(Exception.class, () -> exchange.getOrders(nonexistentId).get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testDeposit() throws ExecutionException, InterruptedException {
|
|
||||||
var client = exchange.createClient("Trader").get();
|
|
||||||
|
|
||||||
exchange.deposit(client.id(), RUB, 1000.0).get();
|
|
||||||
|
|
||||||
var balances = exchange.getBalances(client.id()).get();
|
|
||||||
assertEquals(1000.0, balances.get(RUB));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testDepositNegativeAmount() throws ExecutionException, InterruptedException {
|
|
||||||
var client = exchange.createClient("Trader").get();
|
|
||||||
|
|
||||||
assertThrows(ExecutionException.class, () -> exchange.deposit(client.id(), RUB, -100.0).get());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testDepositWithNullCurrency() throws ExecutionException, InterruptedException {
|
|
||||||
var client = exchange.createClient("Trader").get();
|
|
||||||
|
|
||||||
assertThrows(ExecutionException.class, () -> exchange.deposit(client.id(), null, 100.0).get());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testWithdraw() throws ExecutionException, InterruptedException {
|
|
||||||
var client = exchange.createClient("Trader").get();
|
|
||||||
|
|
||||||
exchange.deposit(client.id(), RUB, 1000.0).get();
|
|
||||||
exchange.withdraw(client.id(), RUB, 500.0).get();
|
|
||||||
|
|
||||||
var balances = exchange.getBalances(client.id()).get();
|
|
||||||
assertEquals(500.0, balances.get(RUB));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testWithdrawInsufficientFunds() throws ExecutionException, InterruptedException {
|
|
||||||
var client = exchange.createClient("Trader").get();
|
|
||||||
|
|
||||||
exchange.deposit(client.id(), RUB, 100.0).get();
|
|
||||||
|
|
||||||
assertThrows(ExecutionException.class, () -> exchange.withdraw(client.id(), RUB, 200.0).get());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testSequentialDepositWithdraw() throws ExecutionException, InterruptedException {
|
|
||||||
var client = exchange.createClient("Trader").get();
|
|
||||||
|
|
||||||
exchange.deposit(client.id(), RUB, 500.0).get();
|
|
||||||
var balancesAfterDeposit = exchange.getBalances(client.id()).get();
|
|
||||||
assertEquals(500.0, balancesAfterDeposit.get(RUB));
|
|
||||||
|
|
||||||
exchange.withdraw(client.id(), RUB, 200.0).get();
|
|
||||||
var balancesAfterWithdraw = exchange.getBalances(client.id()).get();
|
|
||||||
assertEquals(300.0, balancesAfterWithdraw.get(RUB));
|
|
||||||
|
|
||||||
assertThrows(ExecutionException.class, () -> exchange.withdraw(client.id(), RUB, 400.0).get());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPlaceOrder() throws ExecutionException, InterruptedException {
|
void testPlaceOrder() throws ExecutionException, InterruptedException {
|
||||||
var client = exchange.createClient("Trader").get();
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
exchange.deposit(client.id(), CNY, 1000.0).get();
|
|
||||||
exchange.placeOrder(client.id(), RUB_CNY, OrderType.SELL, 120, 100.0).get();
|
exchange.placeOrder(client.id(), RUB_CNY, OrderType.SELL, 120, 100.0).get();
|
||||||
|
|
||||||
var activeOrders = exchange.getActiveOrders(client.id()).get();
|
exchange.getOrders(client.id()).get().getFirst();
|
||||||
assertTrue(activeOrders.iterator().hasNext());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testPlaceOrderInsufficientFunds() 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
|
@Test
|
||||||
void testOrderPriceValidation() throws ExecutionException, InterruptedException {
|
void testOrderPriceValidation() throws ExecutionException, InterruptedException {
|
||||||
var client = exchange.createClient("Trader").get();
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
exchange.deposit(client.id(), CNY, 100.0).get();
|
|
||||||
|
|
||||||
assertThrows(ExecutionException.class, () ->
|
assertThrows(ExecutionException.class, () ->
|
||||||
exchange.placeOrder(client.id(), RUB_CNY, OrderType.SELL, -1.0, 100.0).get()
|
exchange.placeOrder(client.id(), RUB_CNY, OrderType.SELL, -1.0, 100.0).get()
|
||||||
);
|
);
|
||||||
@@ -148,32 +76,27 @@ class CurrencyExchangeTest {
|
|||||||
void testOrderQuantityValidation() throws ExecutionException, InterruptedException {
|
void testOrderQuantityValidation() throws ExecutionException, InterruptedException {
|
||||||
var client = exchange.createClient("Trader").get();
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
exchange.deposit(client.id(), CNY, 100.0).get();
|
|
||||||
|
|
||||||
assertThrows(ExecutionException.class, () ->
|
assertThrows(ExecutionException.class, () ->
|
||||||
exchange.placeOrder(client.id(), RUB_CNY, OrderType.SELL, 120.0, -100.0).get()
|
exchange.placeOrder(client.id(), RUB_CNY, OrderType.SELL, 120.0, -100.0).get()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testMatchingOrders() throws ExecutionException, InterruptedException {
|
void testMatchingOrders() throws ExecutionException, InterruptedException {
|
||||||
var buyer = exchange.createClient("Buyer").get();
|
var buyer = exchange.createClient("Buyer").get();
|
||||||
var seller = exchange.createClient("Seller").get();
|
var seller = exchange.createClient("Seller").get();
|
||||||
|
|
||||||
exchange.deposit(buyer.id(), RUB, 120.0).get();
|
var sellOrderId = exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
|
||||||
exchange.deposit(seller.id(), CNY, 100.0).get();
|
var buyOrderId = exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 120.0, 100.0).get();
|
||||||
|
|
||||||
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
|
var sellOrderMessages = exchange.getOrderMessages(seller.id(), sellOrderId).get();
|
||||||
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 120.0, 100.0).get();
|
var buyOrderMessages = exchange.getOrderMessages(buyer.id(), buyOrderId).get();
|
||||||
|
|
||||||
Map<Currency, Double> buyerBalances = exchange.getBalances(buyer.id()).get();
|
assertEquals(3, sellOrderMessages.size());
|
||||||
Map<Currency, Double> sellerBalances = exchange.getBalances(seller.id()).get();
|
assertSame(sellOrderMessages.getLast().type(), OrderMessageType.CLOSED);
|
||||||
|
assertEquals(3, buyOrderMessages.size());
|
||||||
assertEquals(100.0, buyerBalances.get(CNY));
|
assertSame(buyOrderMessages.getLast().type(), OrderMessageType.CLOSED);
|
||||||
assertEquals(0.0, buyerBalances.get(RUB));
|
|
||||||
assertEquals(120.0, sellerBalances.get(RUB));
|
|
||||||
assertEquals(0.0, sellerBalances.get(CNY));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -181,25 +104,17 @@ class CurrencyExchangeTest {
|
|||||||
var buyer = exchange.createClient("Buyer").get();
|
var buyer = exchange.createClient("Buyer").get();
|
||||||
var seller = exchange.createClient("Seller").get();
|
var seller = exchange.createClient("Seller").get();
|
||||||
|
|
||||||
exchange.deposit(buyer.id(), RUB, 120.0).get();
|
var sellOrderId = exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
|
||||||
exchange.deposit(seller.id(), CNY, 100.0).get();
|
var buyOrderId = exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 60.0, 50.0).get();
|
||||||
|
|
||||||
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
|
var buyOrderMessages = exchange.getOrderMessages(buyer.id(), buyOrderId).get();
|
||||||
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 60.0, 50.0).get();
|
var sellOrderMessages = exchange.getOrderMessages(seller.id(), sellOrderId).get();
|
||||||
|
|
||||||
var buyerBalances = exchange.getBalances(buyer.id()).get();
|
assertEquals(2, sellOrderMessages.size());
|
||||||
var sellerBalances = exchange.getBalances(seller.id()).get();
|
assertSame(sellOrderMessages.getLast().type(), OrderMessageType.FILLED);
|
||||||
|
assertEquals(new OrderData(60.0, 50.0), ((OrderFilledMessage) sellOrderMessages.getLast()).newData());
|
||||||
assertEquals(50.0, buyerBalances.get(CNY));
|
assertEquals(3, buyOrderMessages.size());
|
||||||
assertEquals(60.0, sellerBalances.get(RUB));
|
assertSame(buyOrderMessages.getLast().type(), OrderMessageType.CLOSED);
|
||||||
|
|
||||||
var sellerOrders = exchange.getActiveOrders(seller.id()).get();
|
|
||||||
var remainingOrder = sellerOrders.iterator().next();
|
|
||||||
|
|
||||||
|
|
||||||
assertEquals(RUB_CNY, remainingOrder.pair());
|
|
||||||
assertEquals(50.0, remainingOrder.quantity());
|
|
||||||
assertEquals(60.0, remainingOrder.price());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -208,100 +123,22 @@ class CurrencyExchangeTest {
|
|||||||
var buyer2 = exchange.createClient("Buyer2").get();
|
var buyer2 = exchange.createClient("Buyer2").get();
|
||||||
var seller = exchange.createClient("Seller").get();
|
var seller = exchange.createClient("Seller").get();
|
||||||
|
|
||||||
exchange.deposit(buyer1.id(), RUB, 120.0).get();
|
var sellOrderId = exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
|
||||||
exchange.deposit(buyer2.id(), RUB, 120.0).get();
|
var buyOrderId1 = exchange.placeOrder(buyer1.id(), RUB_CNY, OrderType.BUY, 60.0, 50.0).get();
|
||||||
exchange.deposit(seller.id(), CNY, 100.0).get();
|
var buyOrderId2 = exchange.placeOrder(buyer2.id(), RUB_CNY, OrderType.BUY, 60.0, 50.0).get();
|
||||||
|
|
||||||
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
|
var sellOrderMessages = exchange.getOrderMessages(seller.id(), sellOrderId).get();
|
||||||
exchange.placeOrder(buyer1.id(), RUB_CNY, OrderType.BUY, 60.0, 50.0).get();
|
var buyOrder1Messages = exchange.getOrderMessages(buyer1.id(), buyOrderId1).get();
|
||||||
exchange.placeOrder(buyer2.id(), RUB_CNY, OrderType.BUY, 60.0, 50.0).get();
|
var buyOrder2Messages = exchange.getOrderMessages(buyer2.id(), buyOrderId2).get();
|
||||||
|
|
||||||
var buyer1Balances = exchange.getBalances(buyer1.id()).get();
|
assertEquals(4, sellOrderMessages.size());
|
||||||
var buyer2Balances = exchange.getBalances(buyer2.id()).get();
|
assertSame(sellOrderMessages.getLast().type(), OrderMessageType.CLOSED);
|
||||||
var sellerBalances = exchange.getBalances(seller.id()).get();
|
assertEquals(3, buyOrder1Messages.size());
|
||||||
|
assertSame(buyOrder1Messages.getLast().type(), OrderMessageType.CLOSED);
|
||||||
assertEquals(50.0, buyer1Balances.get(CNY));
|
assertEquals(3, buyOrder2Messages.size());
|
||||||
assertEquals(50.0, buyer2Balances.get(CNY));
|
assertSame(buyOrder2Messages.getLast().type(), OrderMessageType.CLOSED);
|
||||||
assertEquals(120.0, sellerBalances.get(RUB));
|
|
||||||
|
|
||||||
var sellerOrders = exchange.getActiveOrders(seller.id()).get();
|
|
||||||
assertFalse(sellerOrders.iterator().hasNext());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void testSequentialOrderPlacementAndMatching() throws ExecutionException, InterruptedException {
|
|
||||||
var buyer = exchange.createClient("Buyer").get();
|
|
||||||
var seller = exchange.createClient("Seller").get();
|
|
||||||
|
|
||||||
exchange.deposit(buyer.id(), RUB, 300.0).get();
|
|
||||||
exchange.deposit(seller.id(), CNY, 200.0).get();
|
|
||||||
|
|
||||||
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
|
|
||||||
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 120.0, 100.0).get();
|
|
||||||
|
|
||||||
var buyerBalances = exchange.getBalances(buyer.id()).get();
|
|
||||||
var sellerBalances = exchange.getBalances(seller.id()).get();
|
|
||||||
|
|
||||||
assertEquals(100.0, buyerBalances.get(CNY));
|
|
||||||
assertEquals(180.0, buyerBalances.get(RUB));
|
|
||||||
assertEquals(120.0, sellerBalances.get(RUB));
|
|
||||||
assertEquals(100.0, sellerBalances.get(CNY));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testSequentialMultipleClientsWithPartialMatches() throws ExecutionException, InterruptedException {
|
|
||||||
var buyer1 = exchange.createClient("Buyer1").get();
|
|
||||||
var buyer2 = exchange.createClient("Buyer2").get();
|
|
||||||
var seller = exchange.createClient("Seller").get();
|
|
||||||
|
|
||||||
exchange.deposit(buyer1.id(), RUB, 60.0).get();
|
|
||||||
exchange.deposit(buyer2.id(), RUB, 60.0).get();
|
|
||||||
exchange.deposit(seller.id(), CNY, 100.0).get();
|
|
||||||
|
|
||||||
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
|
|
||||||
|
|
||||||
exchange.placeOrder(buyer1.id(), RUB_CNY, OrderType.BUY, 36.0, 30.0).get();
|
|
||||||
exchange.placeOrder(buyer2.id(), RUB_CNY, OrderType.BUY, 36.0, 30.0).get();
|
|
||||||
|
|
||||||
var buyer1Balances = exchange.getBalances(buyer1.id()).get();
|
|
||||||
var buyer2Balances = exchange.getBalances(buyer2.id()).get();
|
|
||||||
var sellerBalances = exchange.getBalances(seller.id()).get();
|
|
||||||
|
|
||||||
assertEquals(30.0, buyer1Balances.get(CNY));
|
|
||||||
assertEquals(24.0, buyer1Balances.get(RUB));
|
|
||||||
|
|
||||||
assertEquals(30.0, buyer2Balances.get(CNY));
|
|
||||||
assertEquals(24.0, buyer2Balances.get(RUB));
|
|
||||||
|
|
||||||
var sellerOrders = exchange.getActiveOrders(seller.id()).get();
|
|
||||||
var sellerOrder = sellerOrders.iterator().next();
|
|
||||||
|
|
||||||
assertEquals(40.0, sellerOrder.quantity());
|
|
||||||
assertEquals(48.0, sellerOrder.price());
|
|
||||||
assertEquals(72.0, sellerBalances.get(RUB));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testSequentialScenarioWithInsufficientFunds() throws ExecutionException, InterruptedException {
|
|
||||||
var buyer = exchange.createClient("Buyer").get();
|
|
||||||
var seller = exchange.createClient("Seller").get();
|
|
||||||
|
|
||||||
exchange.deposit(buyer.id(), RUB, 100.0).get();
|
|
||||||
exchange.deposit(seller.id(), CNY, 50.0).get();
|
|
||||||
|
|
||||||
assertThrows(ExecutionException.class, () ->
|
|
||||||
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get()
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThrows(ExecutionException.class, () ->
|
|
||||||
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 240.0, 200.0).get()
|
|
||||||
);
|
|
||||||
|
|
||||||
var buyerOrders = exchange.getActiveOrders(buyer.id()).get();
|
|
||||||
var sellerOrders = exchange.getActiveOrders(seller.id()).get();
|
|
||||||
assertFalse(buyerOrders.iterator().hasNext());
|
|
||||||
assertFalse(sellerOrders.iterator().hasNext());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPricePriorityInOrderMatching() throws ExecutionException, InterruptedException {
|
void testPricePriorityInOrderMatching() throws ExecutionException, InterruptedException {
|
||||||
@@ -309,27 +146,21 @@ class CurrencyExchangeTest {
|
|||||||
var seller1 = exchange.createClient("Seller1").get();
|
var seller1 = exchange.createClient("Seller1").get();
|
||||||
var seller2 = exchange.createClient("Seller2").get();
|
var seller2 = exchange.createClient("Seller2").get();
|
||||||
|
|
||||||
exchange.deposit(buyer.id(), RUB, 200.0).get();
|
var sellOrderId1 = exchange.placeOrder(seller1.id(), RUB_CNY, OrderType.SELL, 75, 50.0).get();
|
||||||
exchange.deposit(seller1.id(), CNY, 50.0).get();
|
var sellOrderId2 = exchange.placeOrder(seller2.id(), RUB_CNY, OrderType.SELL, 60, 50.0).get();
|
||||||
exchange.deposit(seller2.id(), CNY, 50.0).get();
|
|
||||||
|
|
||||||
exchange.placeOrder(seller1.id(), RUB_CNY, OrderType.SELL, 75, 50.0).get();
|
var buyOrderId = exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 75, 50.0).get();
|
||||||
exchange.placeOrder(seller2.id(), RUB_CNY, OrderType.SELL, 60, 50.0).get();
|
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
var buyerBalances = exchange.getBalances(buyer.id()).get();
|
assertEquals(1, sellOrder1Messages.size());
|
||||||
var seller1Balances = exchange.getBalances(seller1.id()).get();
|
assertSame(sellOrder1Messages.getLast().type(), OrderMessageType.CREATED);
|
||||||
var seller2Balances = exchange.getBalances(seller2.id()).get();
|
assertEquals(3, sellOrder2Messages.size());
|
||||||
|
assertSame(sellOrder2Messages.getLast().type(), OrderMessageType.CLOSED);
|
||||||
assertEquals(50.0, buyerBalances.get(CNY));
|
assertEquals(3, buyOrderMessages.size());
|
||||||
assertEquals(140.0, buyerBalances.get(RUB));
|
assertSame(buyOrderMessages.getLast().type(), OrderMessageType.CLOSED);
|
||||||
|
|
||||||
assertEquals(60.0, seller2Balances.get(RUB));
|
|
||||||
assertEquals(0.0, seller2Balances.get(CNY));
|
|
||||||
|
|
||||||
assertNull(seller1Balances.get(RUB));
|
|
||||||
assertEquals(0.0, seller1Balances.get(CNY));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -339,173 +170,59 @@ class CurrencyExchangeTest {
|
|||||||
var seller2 = exchange.createClient("Seller2").get();
|
var seller2 = exchange.createClient("Seller2").get();
|
||||||
var seller3 = exchange.createClient("Seller3").get();
|
var seller3 = exchange.createClient("Seller3").get();
|
||||||
|
|
||||||
exchange.deposit(buyer.id(), RUB, 300.0).get();
|
var sellOrderId1 = exchange.placeOrder(seller1.id(), RUB_CNY, OrderType.SELL, 75.0, 50.0).get();
|
||||||
exchange.deposit(seller1.id(), CNY, 50.0).get();
|
var sellOrderId2 = exchange.placeOrder(seller2.id(), RUB_CNY, OrderType.SELL, 65.0, 50.0).get();
|
||||||
exchange.deposit(seller2.id(), CNY, 50.0).get();
|
var sellOrderId3 = exchange.placeOrder(seller3.id(), RUB_CNY, OrderType.SELL, 60.0, 50.0).get();
|
||||||
exchange.deposit(seller3.id(), CNY, 50.0).get();
|
|
||||||
|
|
||||||
exchange.placeOrder(seller1.id(), RUB_CNY, OrderType.SELL, 75.0, 50.0).get();
|
var buyOrderId = exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 150.0, 100.0).get();
|
||||||
exchange.placeOrder(seller2.id(), RUB_CNY, OrderType.SELL, 65.0, 50.0).get();
|
|
||||||
exchange.placeOrder(seller3.id(), RUB_CNY, OrderType.SELL, 60.0, 50.0).get();
|
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
var buyerBalances = exchange.getBalances(buyer.id()).get();
|
assertEquals(1, sellOrder1Messages.size());
|
||||||
var seller1Balances = exchange.getBalances(seller1.id()).get();
|
assertSame(sellOrder1Messages.getLast().type(), OrderMessageType.CREATED);
|
||||||
var seller2Balances = exchange.getBalances(seller2.id()).get();
|
assertEquals(3, sellOrder2Messages.size());
|
||||||
var seller3Balances = exchange.getBalances(seller3.id()).get();
|
assertSame(sellOrder2Messages.getLast().type(), OrderMessageType.CLOSED);
|
||||||
|
assertEquals(3, sellOrder3Messages.size());
|
||||||
assertEquals(100.0, buyerBalances.get(CNY));
|
assertSame(sellOrder3Messages.getLast().type(), OrderMessageType.CLOSED);
|
||||||
assertEquals(175.0, buyerBalances.get(RUB));
|
assertEquals(4, buyOrderMessages.size());
|
||||||
|
assertSame(buyOrderMessages.getLast().type(), OrderMessageType.CLOSED);
|
||||||
assertEquals(60.0, seller3Balances.get(RUB));
|
|
||||||
assertEquals(0.0, seller3Balances.get(CNY));
|
|
||||||
|
|
||||||
assertEquals(65.0, seller2Balances.get(RUB));
|
|
||||||
assertEquals(0.0, seller2Balances.get(CNY));
|
|
||||||
|
|
||||||
assertNull(seller1Balances.get(RUB));
|
|
||||||
assertEquals(0.0, seller1Balances.get(CNY));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testTimePriorityWithEqualPrices() throws ExecutionException, InterruptedException {
|
|
||||||
var buyer = exchange.createClient("Buyer").get();
|
|
||||||
var seller1 = exchange.createClient("Seller1").get();
|
|
||||||
var seller2 = exchange.createClient("Seller2").get();
|
|
||||||
|
|
||||||
exchange.deposit(buyer.id(), RUB, 200.0).get();
|
|
||||||
exchange.deposit(seller1.id(), CNY, 50.0).get();
|
|
||||||
exchange.deposit(seller2.id(), CNY, 50.0).get();
|
|
||||||
|
|
||||||
exchange.placeOrder(seller1.id(), RUB_CNY, OrderType.SELL, 60, 50.0).get();
|
|
||||||
exchange.placeOrder(seller2.id(), RUB_CNY, OrderType.SELL, 60, 50.0).get();
|
|
||||||
|
|
||||||
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 60, 50.0).get();
|
|
||||||
|
|
||||||
var buyerBalances = exchange.getBalances(buyer.id()).get();
|
|
||||||
var seller1Balances = exchange.getBalances(seller1.id()).get();
|
|
||||||
var seller2Balances = exchange.getBalances(seller2.id()).get();
|
|
||||||
|
|
||||||
assertEquals(50.0, buyerBalances.get(CNY));
|
|
||||||
assertEquals(140.0, buyerBalances.get(RUB));
|
|
||||||
|
|
||||||
assertEquals(60.0, seller1Balances.get(RUB));
|
|
||||||
assertEquals(0.0, seller1Balances.get(CNY));
|
|
||||||
|
|
||||||
assertNull(seller2Balances.get(RUB));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPriceOrderNotMatching() throws ExecutionException, InterruptedException {
|
void testPriceOrderNotMatching() throws ExecutionException, InterruptedException {
|
||||||
var buyer1 = exchange.createClient("Buyer1").get();
|
var buyer1 = exchange.createClient("Buyer1").get();
|
||||||
var buyer2 = exchange.createClient("Buyer2").get();
|
var buyer2 = exchange.createClient("Buyer2").get();
|
||||||
var seller = exchange.createClient("Seller2").get();
|
var seller = exchange.createClient("Seller").get();
|
||||||
|
|
||||||
exchange.deposit(buyer1.id(), RUB, 200.0).get();
|
var buyOrderId1 = exchange.placeOrder(buyer1.id(), RUB_CNY, OrderType.BUY, 80.0, 50.0).get();
|
||||||
exchange.deposit(buyer2.id(), RUB, 200.0).get();
|
var buyOrderId2 = exchange.placeOrder(buyer2.id(), RUB_CNY, OrderType.BUY, 75.0, 50.0).get();
|
||||||
exchange.deposit(seller.id(), CNY, 50.0).get();
|
|
||||||
|
|
||||||
exchange.placeOrder(buyer1.id(), RUB_CNY, OrderType.BUY, 80.0, 50.0).get();
|
var sellOrderId = exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 85, 50.0).get();
|
||||||
exchange.placeOrder(buyer2.id(), RUB_CNY, OrderType.BUY, 75.0, 50.0).get();
|
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
var buyer1Balances = exchange.getBalances(buyer1.id()).get();
|
assertEquals(1, sellOrderMessages.size());
|
||||||
var buyer2Balances = exchange.getBalances(buyer2.id()).get();
|
assertEquals(1, buyOrderMessages.size());
|
||||||
var sellerBalances = exchange.getBalances(seller.id()).get();
|
assertEquals(1, buyOrderMessages2.size());
|
||||||
|
|
||||||
assertNull(sellerBalances.get(RUB));
|
|
||||||
assertEquals(0.0, sellerBalances.get(CNY));
|
|
||||||
|
|
||||||
assertNull(buyer1Balances.get(CNY));
|
|
||||||
assertEquals(120.0, buyer1Balances.get(RUB));
|
|
||||||
|
|
||||||
assertNull(buyer2Balances.get(CNY));
|
|
||||||
assertEquals(125.0, buyer2Balances.get(RUB));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCancelBuyOrder() throws ExecutionException, InterruptedException {
|
public void testCancelBuyOrder() throws ExecutionException, InterruptedException {
|
||||||
var client = exchange.createClient("Client1").get();
|
|
||||||
|
|
||||||
exchange.deposit(client.id(), RUB, 100.0);
|
|
||||||
|
|
||||||
var order = exchange.placeOrder(client.id(), RUB_CNY, OrderType.BUY, 100.0, 200.0).get().orElseThrow();
|
|
||||||
|
|
||||||
var balances = exchange.getBalances(client.id()).get();
|
|
||||||
assertEquals(0.0, balances.get(RUB));
|
|
||||||
|
|
||||||
exchange.cancelOrder(client.id(), order.id()).get();
|
|
||||||
|
|
||||||
balances = exchange.getBalances(client.id()).get();
|
|
||||||
assertEquals(100.0, balances.get(RUB));
|
|
||||||
var orders = exchange.getActiveOrders(client.id()).get();
|
|
||||||
|
|
||||||
assertFalse(orders.iterator().hasNext());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCancelSellOrder() throws ExecutionException, InterruptedException {
|
|
||||||
var client = exchange.createClient("Client1").get();
|
|
||||||
|
|
||||||
exchange.deposit(client.id(), CNY, 200.0);
|
|
||||||
|
|
||||||
var order = exchange.placeOrder(client.id(), RUB_CNY, OrderType.SELL, 100.0, 200.0).get().orElseThrow();
|
|
||||||
|
|
||||||
var balances = exchange.getBalances(client.id()).get();
|
|
||||||
assertEquals(0.0, balances.get(CNY));
|
|
||||||
|
|
||||||
exchange.cancelOrder(client.id(), order.id()).get();
|
|
||||||
|
|
||||||
balances = exchange.getBalances(client.id()).get();
|
|
||||||
assertEquals(200.0, balances.get(CNY));
|
|
||||||
var orders = exchange.getActiveOrders(client.id()).get();
|
|
||||||
|
|
||||||
assertFalse(orders.iterator().hasNext());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCancelPartialOrder() throws ExecutionException, InterruptedException {
|
|
||||||
var buyer = exchange.createClient("Buyer").get();
|
|
||||||
var seller = exchange.createClient("Seller").get();
|
|
||||||
|
|
||||||
exchange.deposit(buyer.id(), RUB, 1000.0).get();
|
|
||||||
exchange.deposit(seller.id(), CNY, 500.0).get();
|
|
||||||
|
|
||||||
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
|
|
||||||
|
|
||||||
var order = exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 200.0, 120.0).get().orElseThrow();
|
|
||||||
|
|
||||||
var buyerOrders = exchange.getActiveOrders(buyer.id()).get();
|
|
||||||
var sellerOrders = exchange.getActiveOrders(seller.id()).get();
|
|
||||||
|
|
||||||
assertTrue(buyerOrders.iterator().hasNext());
|
|
||||||
assertFalse(sellerOrders.iterator().hasNext());
|
|
||||||
|
|
||||||
exchange.cancelOrder(buyer.id(), order.id());
|
|
||||||
|
|
||||||
var buyerBalances = exchange.getBalances(buyer.id()).get();
|
|
||||||
assertEquals(100.0, buyerBalances.get(CNY), 0.001);
|
|
||||||
assertEquals(880.0, buyerBalances.get(RUB), 0.001);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testSameClientSellBuy() throws ExecutionException, InterruptedException {
|
|
||||||
var client = exchange.createClient("Trader").get();
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
exchange.deposit(client.id(), RUB, 1000.0).get();
|
var orderId = exchange.placeOrder(client.id(), RUB_CNY, OrderType.BUY, 100.0, 200.0).get();
|
||||||
exchange.deposit(client.id(), CNY, 1000.0).get();
|
|
||||||
|
exchange.cancelOrder(client.id(), orderId).get();
|
||||||
exchange.placeOrder(client.id(), RUB_CNY, OrderType.BUY, 100.0, 100.0).get();
|
|
||||||
exchange.placeOrder(client.id(), RUB_CNY, OrderType.SELL, 90.0, 100.0).get();
|
var orderMessages = exchange.getOrderMessages(client.id(), orderId).get();
|
||||||
|
|
||||||
var balances = exchange.getBalances(client.id()).get();
|
assertEquals(2, orderMessages.size());
|
||||||
assertEquals(900.0, balances.get(RUB), 0.001);
|
assertSame(orderMessages.getLast().type(), OrderMessageType.CLOSED);
|
||||||
assertEquals(900.0, balances.get(CNY), 0.001);
|
assertSame(((OrderClosedMessage) orderMessages.getLast()).reason(), OrderClosedMessage.Reason.CANCELLED);
|
||||||
|
|
||||||
var orders = exchange.getActiveOrders(client.id()).get();
|
|
||||||
assertNotNull(orders.iterator().next());
|
|
||||||
assertNotNull(orders.iterator().next());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user