1
0
This commit is contained in:
2024-11-14 12:27:54 +03:00
parent 96b358f428
commit 39f827f3e7
13 changed files with 1465 additions and 0 deletions

View File

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

View 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);
}

View 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);
}
}

View 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();
}
}

View 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);
}

View File

@@ -0,0 +1,4 @@
package ru.lionarius.api.currency;
public record Currency(String name) {
}

View File

@@ -0,0 +1,4 @@
package ru.lionarius.api.currency;
public record CurrencyPair(Currency base, Currency quote) {
}

View 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();
}
}

View File

@@ -0,0 +1,6 @@
package ru.lionarius.api.order;
public enum OrderType {
BUY,
SELL
}

View File

@@ -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)));
}
}

View 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);
}
}

View 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();
}
}
}

View 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);
}
}