lab9
This commit is contained in:
@@ -12,6 +12,8 @@ repositories {
|
|||||||
dependencies {
|
dependencies {
|
||||||
testImplementation platform('org.junit:junit-bom:5.10.0')
|
testImplementation platform('org.junit:junit-bom:5.10.0')
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||||
|
|
||||||
|
implementation 'com.google.guava:guava:33.2.1-jre'
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
|||||||
28
src/main/java/ru/lionarius/api/CurrencyExchange.java
Normal file
28
src/main/java/ru/lionarius/api/CurrencyExchange.java
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package ru.lionarius.api;
|
||||||
|
|
||||||
|
import ru.lionarius.api.client.Client;
|
||||||
|
import ru.lionarius.api.currency.Currency;
|
||||||
|
import ru.lionarius.api.currency.CurrencyPair;
|
||||||
|
import ru.lionarius.api.order.Order;
|
||||||
|
import ru.lionarius.api.order.OrderType;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public interface CurrencyExchange {
|
||||||
|
CompletableFuture<Client> createClient(String name);
|
||||||
|
|
||||||
|
CompletableFuture<Void> deposit(UUID clientId, Currency currency, double amount);
|
||||||
|
|
||||||
|
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<Iterable<Order>> getActiveOrders(UUID clientId);
|
||||||
|
|
||||||
|
CompletableFuture<Map<Currency, Double>> getBalances(UUID clientId);
|
||||||
|
}
|
||||||
27
src/main/java/ru/lionarius/api/client/Balance.java
Normal file
27
src/main/java/ru/lionarius/api/client/Balance.java
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/main/java/ru/lionarius/api/client/ClientRepository.java
Normal file
12
src/main/java/ru/lionarius/api/client/ClientRepository.java
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package ru.lionarius.api.client;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface ClientRepository {
|
||||||
|
Client createClient(String name);
|
||||||
|
|
||||||
|
Optional<Client> getClient(UUID clientId);
|
||||||
|
|
||||||
|
Optional<Balance> getClientBalance(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) {
|
||||||
|
}
|
||||||
32
src/main/java/ru/lionarius/api/order/Order.java
Normal file
32
src/main/java/ru/lionarius/api/order/Order.java
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package ru.lionarius.api.order;
|
||||||
|
|
||||||
|
import ru.lionarius.api.client.Client;
|
||||||
|
import ru.lionarius.api.currency.CurrencyPair;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record Order(UUID id, Client client, OrderType type, CurrencyPair pair, double price, double quantity,
|
||||||
|
LocalDateTime placedAt) {
|
||||||
|
public Order(Client client, OrderType type, CurrencyPair pair, double price, double quantity) {
|
||||||
|
this(UUID.randomUUID(), client, type, pair, price, quantity, LocalDateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
public double rate() {
|
||||||
|
return price() / quantity();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj instanceof Order order) {
|
||||||
|
return id.equals(order.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return id.hashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package ru.lionarius.impl;
|
||||||
|
|
||||||
|
import ru.lionarius.api.client.Balance;
|
||||||
|
import ru.lionarius.api.client.Client;
|
||||||
|
import ru.lionarius.api.client.ClientRepository;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class InMemoryClientRepository implements ClientRepository {
|
||||||
|
private final Map<UUID, Client> clients = new ConcurrentHashMap<>();
|
||||||
|
private final Map<Client, Balance> balances = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Client createClient(String name) {
|
||||||
|
var client = new Client(name);
|
||||||
|
|
||||||
|
clients.put(client.id(), client);
|
||||||
|
balances.put(client, new Balance());
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Client> getClient(UUID clientId) {
|
||||||
|
return Optional.ofNullable(clients.get(clientId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Balance> getClientBalance(UUID clientId) {
|
||||||
|
return Optional.ofNullable(balances.get(clients.get(clientId)));
|
||||||
|
}
|
||||||
|
}
|
||||||
362
src/main/java/ru/lionarius/impl/PlainCurrencyExchange.java
Normal file
362
src/main/java/ru/lionarius/impl/PlainCurrencyExchange.java
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
package ru.lionarius.impl;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import ru.lionarius.api.CurrencyExchange;
|
||||||
|
import ru.lionarius.api.client.Balance;
|
||||||
|
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.OrderType;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
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, List<Order>> buyOrders = new ConcurrentHashMap<>();
|
||||||
|
private final Map<CurrencyPair, List<Order>> sellOrders = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final ReadWriteLock ordersLock = new ReentrantReadWriteLock();
|
||||||
|
|
||||||
|
private final List<Currency> allowedCurrencies;
|
||||||
|
private final List<CurrencyPair> allowedPairs;
|
||||||
|
|
||||||
|
public PlainCurrencyExchange(List<Currency> allowedCurrencies, List<CurrencyPair> allowedPairs) {
|
||||||
|
this.allowedCurrencies = ImmutableList.copyOf(allowedCurrencies);
|
||||||
|
this.allowedPairs = ImmutableList.copyOf(allowedPairs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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<Void> deposit(UUID clientId, Currency currency, double amount) {
|
||||||
|
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(() -> {
|
||||||
|
validateOrderInputs(pair, type, price, quantity);
|
||||||
|
|
||||||
|
var client = clientRepository.getClient(clientId).orElseThrow();
|
||||||
|
var balance = clientRepository.getClientBalance(clientId).orElseThrow();
|
||||||
|
|
||||||
|
reserveFundsForOrder(balance, pair, type, price, quantity);
|
||||||
|
|
||||||
|
var newOrder = new Order(client, type, pair, price, quantity);
|
||||||
|
Optional<Order> orderToAdd = Optional.empty();
|
||||||
|
|
||||||
|
ordersLock.writeLock().lock();
|
||||||
|
try {
|
||||||
|
// 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
|
||||||
|
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;
|
||||||
|
} finally {
|
||||||
|
ordersLock.writeLock().unlock();
|
||||||
|
}
|
||||||
|
}, Runnable::run);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Void> cancelOrder(UUID clientId, UUID orderId) {
|
||||||
|
return CompletableFuture.runAsync(() -> {
|
||||||
|
if (clientId == null || orderId == null)
|
||||||
|
throw new IllegalArgumentException("ClientId and orderId cannot be null");
|
||||||
|
|
||||||
|
var client = clientRepository.getClient(clientId).orElseThrow();
|
||||||
|
var balance = clientRepository.getClientBalance(clientId).orElseThrow();
|
||||||
|
|
||||||
|
ordersLock.writeLock().lock();
|
||||||
|
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) {
|
||||||
|
if (order.type() == OrderType.BUY)
|
||||||
|
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;
|
||||||
|
})
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Map<Currency, Double>> getBalances(UUID clientId) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
var client = clientRepository.getClient(clientId).orElseThrow();
|
||||||
|
var balance = clientRepository.getClientBalance(clientId).orElseThrow();
|
||||||
|
|
||||||
|
return balance.getBalances();
|
||||||
|
}, Runnable::run);
|
||||||
|
}
|
||||||
|
}
|
||||||
437
src/test/java/ConcurrentCurrencyExchangeTest.java
Normal file
437
src/test/java/ConcurrentCurrencyExchangeTest.java
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
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.Order;
|
||||||
|
import ru.lionarius.api.order.OrderType;
|
||||||
|
import ru.lionarius.impl.PlainCurrencyExchange;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class ConcurrentCurrencyExchangeTest {
|
||||||
|
private CurrencyExchange exchange;
|
||||||
|
private Currency USD;
|
||||||
|
private Currency EUR;
|
||||||
|
private CurrencyPair USD_EUR;
|
||||||
|
private ExecutorService executorService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
USD = new Currency("USD");
|
||||||
|
EUR = new Currency("EUR");
|
||||||
|
USD_EUR = new CurrencyPair(USD, EUR);
|
||||||
|
|
||||||
|
exchange = new PlainCurrencyExchange(
|
||||||
|
Arrays.asList(USD, EUR),
|
||||||
|
Arrays.asList(USD_EUR)
|
||||||
|
);
|
||||||
|
|
||||||
|
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 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(), USD, 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(USD));
|
||||||
|
}
|
||||||
|
|
||||||
|
@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(), USD, 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(), USD, 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(USD),
|
||||||
|
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(), USD, 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(), USD, 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(), USD, 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(USD),
|
||||||
|
0.001
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testConcurrentOrderPlacement() throws InterruptedException {
|
||||||
|
var numSellers = 5;
|
||||||
|
var numBuyers = 5;
|
||||||
|
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();
|
||||||
|
exchange.deposit(seller.id(), EUR, 1000.0).join();
|
||||||
|
sellers.add(seller);
|
||||||
|
}
|
||||||
|
|
||||||
|
var buyers = new ArrayList<Client>();
|
||||||
|
for (int i = 0; i < numBuyers; i++) {
|
||||||
|
var buyer = exchange.createClient("Buyer " + i).join();
|
||||||
|
exchange.deposit(buyer.id(), USD, 1200.0).join();
|
||||||
|
buyers.add(buyer);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sellerFutures = sellers.stream()
|
||||||
|
.map(seller -> CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < ordersPerClient; i++) {
|
||||||
|
exchange.placeOrder(seller.id(), USD_EUR, OrderType.SELL, 1.2, 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(), USD_EUR, 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(StreamSupport.stream(
|
||||||
|
exchange.getActiveOrders(seller.id()).join().spliterator(), false
|
||||||
|
).count() <= ordersPerClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var buyer : buyers) {
|
||||||
|
assertTrue(StreamSupport.stream(
|
||||||
|
exchange.getActiveOrders(buyer.id()).join().spliterator(), false
|
||||||
|
).count() <= 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();
|
||||||
|
|
||||||
|
exchange.deposit(seller.id(), EUR, 1000.0).join();
|
||||||
|
exchange.deposit(buyer.id(), USD, 2000.0).join();
|
||||||
|
|
||||||
|
var sellerFuture = CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < numOrders; i++) {
|
||||||
|
var price = 1.0 + (i * 0.01);
|
||||||
|
exchange.placeOrder(seller.id(), USD_EUR, 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(), USD_EUR, 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.getActiveOrders(seller.id()).join();
|
||||||
|
var buyerOrders = exchange.getActiveOrders(buyer.id()).join();
|
||||||
|
|
||||||
|
var remainingSellerOrders = StreamSupport.stream(sellerOrders.spliterator(), false).count();
|
||||||
|
var remainingBuyerOrders = StreamSupport.stream(buyerOrders.spliterator(), false).count();
|
||||||
|
|
||||||
|
assertTrue(remainingSellerOrders + remainingBuyerOrders < numOrders * 2,
|
||||||
|
"Some orders should have been matched");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testConcurrentBalanceCheck() throws InterruptedException {
|
||||||
|
var numThreads = 10;
|
||||||
|
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(), USD, 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(USD) - 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 testBalanceConsistencyWithConcurrentTradesAndWithdrawals() throws InterruptedException {
|
||||||
|
var numTraders = 5;
|
||||||
|
var latch = new CountDownLatch(numTraders * 2);
|
||||||
|
|
||||||
|
var sellers = new ArrayList<Client>();
|
||||||
|
var buyers = new ArrayList<Client>();
|
||||||
|
var initialAmount = 1000.0;
|
||||||
|
|
||||||
|
for (var i = 0; i < numTraders; i++) {
|
||||||
|
var seller = exchange.createClient("Seller " + i).join();
|
||||||
|
var buyer = exchange.createClient("Buyer " + i).join();
|
||||||
|
|
||||||
|
exchange.deposit(seller.id(), EUR, initialAmount).join();
|
||||||
|
exchange.deposit(buyer.id(), USD, initialAmount).join();
|
||||||
|
|
||||||
|
sellers.add(seller);
|
||||||
|
buyers.add(buyer);
|
||||||
|
}
|
||||||
|
|
||||||
|
var futures = new ArrayList<CompletableFuture<Void>>();
|
||||||
|
|
||||||
|
for (var seller : sellers) {
|
||||||
|
var future = CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
exchange.placeOrder(seller.id(), USD_EUR, OrderType.SELL, 1.2, 10.0).join();
|
||||||
|
try {
|
||||||
|
exchange.withdraw(seller.id(), EUR, 1.0).join();
|
||||||
|
} catch (CompletionException ignored) {
|
||||||
|
|
||||||
|
}
|
||||||
|
Thread.sleep(10);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
} finally {
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
}, executorService);
|
||||||
|
futures.add(future);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var buyer : buyers) {
|
||||||
|
var future = CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
exchange.placeOrder(buyer.id(), USD_EUR, OrderType.BUY, 1.2, 10.0).join();
|
||||||
|
try {
|
||||||
|
exchange.withdraw(buyer.id(), USD, 1.0).join();
|
||||||
|
} catch (CompletionException ignored) {
|
||||||
|
}
|
||||||
|
Thread.sleep(10);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
} finally {
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
}, executorService);
|
||||||
|
futures.add(future);
|
||||||
|
}
|
||||||
|
|
||||||
|
latch.await(30, TimeUnit.SECONDS);
|
||||||
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||||
|
|
||||||
|
for (var client : Stream.concat(sellers.stream(), buyers.stream()).toList()) {
|
||||||
|
var balances = exchange.getBalances(client.id()).join();
|
||||||
|
assertTrue(balances.get(USD) >= 0, "USD balance should not be negative");
|
||||||
|
assertTrue(balances.get(EUR) >= 0, "EUR balance should not be negative");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
executorService.shutdown();
|
||||||
|
try {
|
||||||
|
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
|
executorService.shutdownNow();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
executorService.shutdownNow();
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
493
src/test/java/CurrencyExchangeTest.java
Normal file
493
src/test/java/CurrencyExchangeTest.java
Normal file
@@ -0,0 +1,493 @@
|
|||||||
|
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.OrderType;
|
||||||
|
import ru.lionarius.impl.PlainCurrencyExchange;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class CurrencyExchangeTest {
|
||||||
|
private CurrencyExchange exchange;
|
||||||
|
private Currency USD;
|
||||||
|
private Currency EUR;
|
||||||
|
private CurrencyPair USD_EUR;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
USD = new Currency("USD");
|
||||||
|
EUR = new Currency("EUR");
|
||||||
|
USD_EUR = new CurrencyPair(USD, EUR);
|
||||||
|
|
||||||
|
exchange = new PlainCurrencyExchange(
|
||||||
|
Arrays.asList(USD, EUR),
|
||||||
|
Arrays.asList(USD_EUR)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client Tests
|
||||||
|
@Test
|
||||||
|
void testCreateClient() throws ExecutionException, InterruptedException {
|
||||||
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
|
assertNotNull(client);
|
||||||
|
assertNotNull(client.id());
|
||||||
|
assertEquals("Trader", client.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateClientWithNullName() {
|
||||||
|
assertThrows(ExecutionException.class, () -> exchange.createClient(null).get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetBalancesNonexistentClient() {
|
||||||
|
var nonexistentId = UUID.randomUUID();
|
||||||
|
assertThrows(Exception.class, () -> exchange.getBalances(nonexistentId).get());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deposit Tests
|
||||||
|
@Test
|
||||||
|
void testDeposit() throws ExecutionException, InterruptedException {
|
||||||
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
|
exchange.deposit(client.id(), USD, 1000.0).get();
|
||||||
|
|
||||||
|
var balances = exchange.getBalances(client.id()).get();
|
||||||
|
assertEquals(1000.0, balances.get(USD));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDepositNegativeAmount() throws ExecutionException, InterruptedException {
|
||||||
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
|
assertThrows(ExecutionException.class, () -> exchange.deposit(client.id(), USD, -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Withdraw Tests
|
||||||
|
@Test
|
||||||
|
void testWithdraw() throws ExecutionException, InterruptedException {
|
||||||
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
|
exchange.deposit(client.id(), USD, 1000.0).get();
|
||||||
|
exchange.withdraw(client.id(), USD, 500.0).get();
|
||||||
|
|
||||||
|
var balances = exchange.getBalances(client.id()).get();
|
||||||
|
assertEquals(500.0, balances.get(USD));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testWithdrawInsufficientFunds() throws ExecutionException, InterruptedException {
|
||||||
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
|
exchange.deposit(client.id(), USD, 100.0).get();
|
||||||
|
|
||||||
|
assertThrows(ExecutionException.class, () -> exchange.withdraw(client.id(), USD, 200.0).get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSequentialDepositWithdraw() throws ExecutionException, InterruptedException {
|
||||||
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
|
exchange.deposit(client.id(), USD, 500.0).get();
|
||||||
|
var balancesAfterDeposit = exchange.getBalances(client.id()).get();
|
||||||
|
assertEquals(500.0, balancesAfterDeposit.get(USD)); // Trader has 500 USD
|
||||||
|
|
||||||
|
exchange.withdraw(client.id(), USD, 200.0).get();
|
||||||
|
var balancesAfterWithdraw = exchange.getBalances(client.id()).get();
|
||||||
|
assertEquals(300.0, balancesAfterWithdraw.get(USD)); // Trader has 300 USD
|
||||||
|
|
||||||
|
assertThrows(ExecutionException.class, () -> exchange.withdraw(client.id(), USD, 400.0).get()); // Trader has only 300 USD left
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order Tests
|
||||||
|
@Test
|
||||||
|
void testPlaceOrder() throws ExecutionException, InterruptedException {
|
||||||
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
|
exchange.deposit(client.id(), EUR, 1000.0).get();
|
||||||
|
exchange.placeOrder(client.id(), USD_EUR, OrderType.SELL, 120, 100.0).get();
|
||||||
|
|
||||||
|
var activeOrders = exchange.getActiveOrders(client.id()).get();
|
||||||
|
assertTrue(activeOrders.iterator().hasNext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPlaceOrderInsufficientFunds() throws ExecutionException, InterruptedException {
|
||||||
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
|
assertThrows(ExecutionException.class, () ->
|
||||||
|
exchange.placeOrder(client.id(), USD_EUR, OrderType.SELL, 120.0, 100.0).get()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testOrderPriceValidation() throws ExecutionException, InterruptedException {
|
||||||
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
|
exchange.deposit(client.id(), EUR, 100.0).get();
|
||||||
|
|
||||||
|
assertThrows(ExecutionException.class, () ->
|
||||||
|
exchange.placeOrder(client.id(), USD_EUR, OrderType.SELL, -1.0, 100.0).get()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testOrderQuantityValidation() throws ExecutionException, InterruptedException {
|
||||||
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
|
exchange.deposit(client.id(), EUR, 100.0).get();
|
||||||
|
|
||||||
|
assertThrows(ExecutionException.class, () ->
|
||||||
|
exchange.placeOrder(client.id(), USD_EUR, OrderType.SELL, 120.0, -100.0).get()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order Matching Tests
|
||||||
|
@Test
|
||||||
|
void testMatchingOrders() throws ExecutionException, InterruptedException {
|
||||||
|
var buyer = exchange.createClient("Buyer").get();
|
||||||
|
var seller = exchange.createClient("Seller").get();
|
||||||
|
|
||||||
|
exchange.deposit(buyer.id(), USD, 120.0).get();
|
||||||
|
exchange.deposit(seller.id(), EUR, 100.0).get();
|
||||||
|
|
||||||
|
exchange.placeOrder(seller.id(), USD_EUR, OrderType.SELL, 120.0, 100.0).get();
|
||||||
|
exchange.placeOrder(buyer.id(), USD_EUR, OrderType.BUY, 120.0, 100.0).get();
|
||||||
|
|
||||||
|
Map<Currency, Double> buyerBalances = exchange.getBalances(buyer.id()).get();
|
||||||
|
Map<Currency, Double> sellerBalances = exchange.getBalances(seller.id()).get();
|
||||||
|
|
||||||
|
assertEquals(100.0, buyerBalances.get(EUR)); // Buyer bought 100 EUR
|
||||||
|
assertEquals(0.0, buyerBalances.get(USD)); // Buyer sold 120 USD so he has no USD left
|
||||||
|
assertEquals(120.0, sellerBalances.get(USD)); // Seller bought 120 USD
|
||||||
|
assertEquals(0.0, sellerBalances.get(EUR)); // Seller sold 100 EUR so he has no EUR left
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPartialOrderMatching() throws ExecutionException, InterruptedException {
|
||||||
|
var buyer = exchange.createClient("Buyer").get();
|
||||||
|
var seller = exchange.createClient("Seller").get();
|
||||||
|
|
||||||
|
exchange.deposit(buyer.id(), USD, 120.0).get();
|
||||||
|
exchange.deposit(seller.id(), EUR, 100.0).get();
|
||||||
|
|
||||||
|
exchange.placeOrder(seller.id(), USD_EUR, OrderType.SELL, 120.0, 100.0).get();
|
||||||
|
exchange.placeOrder(buyer.id(), USD_EUR, OrderType.BUY, 60.0, 50.0).get();
|
||||||
|
|
||||||
|
var buyerBalances = exchange.getBalances(buyer.id()).get();
|
||||||
|
var sellerBalances = exchange.getBalances(seller.id()).get();
|
||||||
|
|
||||||
|
assertEquals(50.0, buyerBalances.get(EUR)); // Buyer bought 50 EUR
|
||||||
|
assertEquals(60.0, sellerBalances.get(USD)); // Seller bought 60 USD
|
||||||
|
|
||||||
|
var sellerOrders = exchange.getActiveOrders(seller.id()).get();
|
||||||
|
var remainingOrder = sellerOrders.iterator().next();
|
||||||
|
|
||||||
|
// Seller has 60/50 USD/EUR order left
|
||||||
|
assertEquals(USD_EUR, remainingOrder.pair());
|
||||||
|
assertEquals(50.0, remainingOrder.quantity());
|
||||||
|
assertEquals(60.0, remainingOrder.price());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPartialOrderMatchingMultiple() 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(), USD, 120.0).get();
|
||||||
|
exchange.deposit(buyer2.id(), USD, 120.0).get();
|
||||||
|
exchange.deposit(seller.id(), EUR, 100.0).get();
|
||||||
|
|
||||||
|
exchange.placeOrder(seller.id(), USD_EUR, OrderType.SELL, 120.0, 100.0).get();
|
||||||
|
exchange.placeOrder(buyer1.id(), USD_EUR, OrderType.BUY, 60.0, 50.0).get();
|
||||||
|
exchange.placeOrder(buyer2.id(), USD_EUR, OrderType.BUY, 60.0, 50.0).get();
|
||||||
|
|
||||||
|
var buyer1Balances = exchange.getBalances(buyer1.id()).get();
|
||||||
|
var buyer2Balances = exchange.getBalances(buyer2.id()).get();
|
||||||
|
var sellerBalances = exchange.getBalances(seller.id()).get();
|
||||||
|
|
||||||
|
assertEquals(50.0, buyer1Balances.get(EUR)); // Buyer1 bought 50 EUR
|
||||||
|
assertEquals(50.0, buyer2Balances.get(EUR)); // Buyer2 bought 50 EUR
|
||||||
|
assertEquals(120.0, sellerBalances.get(USD)); // Seller bought 120 USD
|
||||||
|
|
||||||
|
var sellerOrders = exchange.getActiveOrders(seller.id()).get();
|
||||||
|
assertFalse(sellerOrders.iterator().hasNext()); // Seller has no orders left
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSequentialOrderPlacementAndMatching() throws ExecutionException, InterruptedException {
|
||||||
|
var buyer = exchange.createClient("Buyer").get();
|
||||||
|
var seller = exchange.createClient("Seller").get();
|
||||||
|
|
||||||
|
exchange.deposit(buyer.id(), USD, 300.0).get();
|
||||||
|
exchange.deposit(seller.id(), EUR, 200.0).get();
|
||||||
|
|
||||||
|
exchange.placeOrder(seller.id(), USD_EUR, OrderType.SELL, 120.0, 100.0).get();
|
||||||
|
exchange.placeOrder(buyer.id(), USD_EUR, 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(EUR)); // Buyer bought 100 EUR
|
||||||
|
assertEquals(180.0, buyerBalances.get(USD)); // Buyer spends 120 USD
|
||||||
|
assertEquals(120.0, sellerBalances.get(USD)); // Seller bought 120 USD
|
||||||
|
assertEquals(100.0, sellerBalances.get(EUR)); // Seller retains remaining EUR
|
||||||
|
}
|
||||||
|
|
||||||
|
@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(), USD, 60.0).get();
|
||||||
|
exchange.deposit(buyer2.id(), USD, 60.0).get();
|
||||||
|
exchange.deposit(seller.id(), EUR, 100.0).get();
|
||||||
|
|
||||||
|
exchange.placeOrder(seller.id(), USD_EUR, OrderType.SELL, 120.0, 100.0).get();
|
||||||
|
|
||||||
|
exchange.placeOrder(buyer1.id(), USD_EUR, OrderType.BUY, 36.0, 30.0).get();
|
||||||
|
exchange.placeOrder(buyer2.id(), USD_EUR, 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(EUR)); // Buyer1 bought 30 EUR
|
||||||
|
assertEquals(24.0, buyer1Balances.get(USD)); // Buyer1 sold 36 EUR
|
||||||
|
|
||||||
|
assertEquals(30.0, buyer2Balances.get(EUR)); // Buyer2 bought 30 EUR
|
||||||
|
assertEquals(24.0, buyer2Balances.get(USD)); // Buyer2 sold 36 EUR
|
||||||
|
|
||||||
|
var sellerOrders = exchange.getActiveOrders(seller.id()).get();
|
||||||
|
var sellerOrder = sellerOrders.iterator().next();
|
||||||
|
|
||||||
|
assertEquals(40.0, sellerOrder.quantity()); // 40 EUR reserved
|
||||||
|
assertEquals(48.0, sellerOrder.price()); // 1.2 USD per EUR
|
||||||
|
assertEquals(72.0, sellerBalances.get(USD)); // 72 USD earned
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSequentialScenarioWithInsufficientFunds() throws ExecutionException, InterruptedException {
|
||||||
|
var buyer = exchange.createClient("Buyer").get();
|
||||||
|
var seller = exchange.createClient("Seller").get();
|
||||||
|
|
||||||
|
exchange.deposit(buyer.id(), USD, 100.0).get();
|
||||||
|
exchange.deposit(seller.id(), EUR, 50.0).get();
|
||||||
|
|
||||||
|
assertThrows(ExecutionException.class, () ->
|
||||||
|
exchange.placeOrder(seller.id(), USD_EUR, OrderType.SELL, 120.0, 100.0).get()
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThrows(ExecutionException.class, () ->
|
||||||
|
exchange.placeOrder(buyer.id(), USD_EUR, 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
|
||||||
|
void testPricePriorityInOrderMatching() 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(), USD, 200.0).get();
|
||||||
|
exchange.deposit(seller1.id(), EUR, 50.0).get();
|
||||||
|
exchange.deposit(seller2.id(), EUR, 50.0).get();
|
||||||
|
|
||||||
|
exchange.placeOrder(seller1.id(), USD_EUR, OrderType.SELL, 75, 50.0).get(); // Higher price
|
||||||
|
exchange.placeOrder(seller2.id(), USD_EUR, OrderType.SELL, 60, 50.0).get(); // Lower price
|
||||||
|
|
||||||
|
exchange.placeOrder(buyer.id(), USD_EUR, OrderType.BUY, 75, 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(EUR)); // Buyer gets 50 EUR from the lower-priced seller
|
||||||
|
assertEquals(140.0, buyerBalances.get(USD)); // Buyer spends 60 USD
|
||||||
|
|
||||||
|
assertEquals(60.0, seller2Balances.get(USD)); // Seller2 sells at 60 price
|
||||||
|
assertEquals(0.0, seller2Balances.get(EUR)); // Seller2 has no EUR left
|
||||||
|
|
||||||
|
assertNull(seller1Balances.get(USD)); // Seller1 remains untouched
|
||||||
|
assertEquals(0.0, seller1Balances.get(EUR)); // Seller1 still has their EUR reserved
|
||||||
|
}
|
||||||
|
|
||||||
|
@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();
|
||||||
|
|
||||||
|
exchange.deposit(buyer.id(), USD, 300.0).get();
|
||||||
|
exchange.deposit(seller1.id(), EUR, 50.0).get();
|
||||||
|
exchange.deposit(seller2.id(), EUR, 50.0).get();
|
||||||
|
exchange.deposit(seller3.id(), EUR, 50.0).get();
|
||||||
|
|
||||||
|
exchange.placeOrder(seller1.id(), USD_EUR, OrderType.SELL, 75.0, 50.0).get(); // Highest price
|
||||||
|
exchange.placeOrder(seller2.id(), USD_EUR, OrderType.SELL, 65.0, 50.0).get(); // Medium price
|
||||||
|
exchange.placeOrder(seller3.id(), USD_EUR, OrderType.SELL, 60.0, 50.0).get(); // Lowest price
|
||||||
|
|
||||||
|
exchange.placeOrder(buyer.id(), USD_EUR, OrderType.BUY, 150.0, 100.0).get();
|
||||||
|
|
||||||
|
var buyerBalances = exchange.getBalances(buyer.id()).get();
|
||||||
|
var seller1Balances = exchange.getBalances(seller1.id()).get();
|
||||||
|
var seller2Balances = exchange.getBalances(seller2.id()).get();
|
||||||
|
var seller3Balances = exchange.getBalances(seller3.id()).get();
|
||||||
|
|
||||||
|
assertEquals(100.0, buyerBalances.get(EUR)); // Buyer receives 100 EUR (50 from seller3 and 50 from seller2)
|
||||||
|
assertEquals(175.0, buyerBalances.get(USD)); // Buyer spends 125 USD (60 + 65)
|
||||||
|
|
||||||
|
assertEquals(60.0, seller3Balances.get(USD)); // Seller3 sells at 60 price
|
||||||
|
assertEquals(0.0, seller3Balances.get(EUR)); // Seller3's EUR is reduced to 0
|
||||||
|
|
||||||
|
assertEquals(65.0, seller2Balances.get(USD)); // Seller2 sells at 65 price
|
||||||
|
assertEquals(0.0, seller2Balances.get(EUR)); // Seller2's EUR is reduced to 0
|
||||||
|
|
||||||
|
assertNull(seller1Balances.get(USD)); // Seller1 remains untouched
|
||||||
|
assertEquals(0.0, seller1Balances.get(EUR)); // Seller1 still has their EUR reserved
|
||||||
|
}
|
||||||
|
|
||||||
|
@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(), USD, 200.0).get();
|
||||||
|
exchange.deposit(seller1.id(), EUR, 50.0).get();
|
||||||
|
exchange.deposit(seller2.id(), EUR, 50.0).get();
|
||||||
|
|
||||||
|
exchange.placeOrder(seller1.id(), USD_EUR, OrderType.SELL, 60, 50.0).get(); // Placed first
|
||||||
|
exchange.placeOrder(seller2.id(), USD_EUR, OrderType.SELL, 60, 50.0).get(); // Placed second
|
||||||
|
|
||||||
|
exchange.placeOrder(buyer.id(), USD_EUR, 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(EUR)); // Buyer gets 50 EUR
|
||||||
|
assertEquals(140.0, buyerBalances.get(USD)); // Buyer spends 60 USD
|
||||||
|
|
||||||
|
assertEquals(60.0, seller1Balances.get(USD)); // Seller1 sells first
|
||||||
|
assertEquals(0.0, seller1Balances.get(EUR)); // Seller1's EUR is reduced to 0
|
||||||
|
|
||||||
|
assertNull(seller2Balances.get(USD)); // Seller2 remains untouched
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPriceOrderNotMatching() throws ExecutionException, InterruptedException {
|
||||||
|
var buyer1 = exchange.createClient("Buyer1").get();
|
||||||
|
var buyer2 = exchange.createClient("Buyer2").get();
|
||||||
|
var seller = exchange.createClient("Seller2").get();
|
||||||
|
|
||||||
|
exchange.deposit(buyer1.id(), USD, 200.0).get();
|
||||||
|
exchange.deposit(buyer2.id(), USD, 200.0).get();
|
||||||
|
exchange.deposit(seller.id(), EUR, 50.0).get();
|
||||||
|
|
||||||
|
exchange.placeOrder(buyer1.id(), USD_EUR, OrderType.BUY, 80.0, 50.0).get();
|
||||||
|
exchange.placeOrder(buyer2.id(), USD_EUR, OrderType.BUY, 75.0, 50.0).get();
|
||||||
|
|
||||||
|
exchange.placeOrder(seller.id(), USD_EUR, OrderType.SELL, 85, 50.0).get();
|
||||||
|
|
||||||
|
var buyer1Balances = exchange.getBalances(buyer1.id()).get();
|
||||||
|
var buyer2Balances = exchange.getBalances(buyer2.id()).get();
|
||||||
|
var sellerBalances = exchange.getBalances(seller.id()).get();
|
||||||
|
|
||||||
|
assertNull(sellerBalances.get(USD)); // Seller did not sell anything
|
||||||
|
assertEquals(0.0, sellerBalances.get(EUR)); // Seller has no EUR left
|
||||||
|
|
||||||
|
assertNull(buyer1Balances.get(EUR)); // Buyer1 did not buy anything
|
||||||
|
assertEquals(120.0, buyer1Balances.get(USD));
|
||||||
|
|
||||||
|
assertNull(buyer2Balances.get(EUR)); // Buyer2 did not buy anything
|
||||||
|
assertEquals(125.0, buyer2Balances.get(USD));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCancelBuyOrder() throws ExecutionException, InterruptedException {
|
||||||
|
var client = exchange.createClient("Client1").get();
|
||||||
|
|
||||||
|
exchange.deposit(client.id(), USD, 100.0);
|
||||||
|
|
||||||
|
var order = exchange.placeOrder(client.id(), USD_EUR, OrderType.BUY, 100.0, 200.0).get().orElseThrow();
|
||||||
|
|
||||||
|
var balances = exchange.getBalances(client.id()).get();
|
||||||
|
assertEquals(0.0, balances.get(USD));
|
||||||
|
|
||||||
|
exchange.cancelOrder(client.id(), order.id()).get();
|
||||||
|
|
||||||
|
balances = exchange.getBalances(client.id()).get();
|
||||||
|
assertEquals(100.0, balances.get(USD));
|
||||||
|
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(), EUR, 200.0);
|
||||||
|
|
||||||
|
var order = exchange.placeOrder(client.id(), USD_EUR, OrderType.SELL, 100.0, 200.0).get().orElseThrow();
|
||||||
|
|
||||||
|
var balances = exchange.getBalances(client.id()).get();
|
||||||
|
assertEquals(0.0, balances.get(EUR));
|
||||||
|
|
||||||
|
exchange.cancelOrder(client.id(), order.id()).get();
|
||||||
|
|
||||||
|
balances = exchange.getBalances(client.id()).get();
|
||||||
|
assertEquals(200.0, balances.get(EUR));
|
||||||
|
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(), USD, 1000.0).get();
|
||||||
|
exchange.deposit(seller.id(), EUR, 500.0).get();
|
||||||
|
|
||||||
|
exchange.placeOrder(seller.id(), USD_EUR, OrderType.SELL, 120.0, 100.0).get();
|
||||||
|
|
||||||
|
var order = exchange.placeOrder(buyer.id(), USD_EUR, 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(EUR), 0.001);
|
||||||
|
assertEquals(880.0, buyerBalances.get(USD), 0.001);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user