339 lines
12 KiB
Java
339 lines
12 KiB
Java
package business;
|
|
|
|
import org.junit.jupiter.api.AfterEach;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.Test;
|
|
import ru.lionarius.api.CurrencyExchange;
|
|
import ru.lionarius.api.client.Client;
|
|
import ru.lionarius.api.currency.Currency;
|
|
import ru.lionarius.api.currency.CurrencyPair;
|
|
import ru.lionarius.api.order.OrderType;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Set;
|
|
import java.util.UUID;
|
|
import java.util.concurrent.*;
|
|
import java.util.stream.Stream;
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
|
|
public abstract class ConcurrentCurrencyExchangeTest {
|
|
protected abstract CurrencyExchange createExchange(Set<CurrencyPair> pairs);
|
|
protected abstract void shutdownExchange(CurrencyExchange exchange);
|
|
|
|
private CurrencyExchange exchange;
|
|
private CurrencyPair RUB_CNY;
|
|
private ExecutorService executorService;
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
Currency RUB = new Currency("RUB");
|
|
Currency CNY = new Currency("CNY");
|
|
RUB_CNY = new CurrencyPair(RUB, CNY);
|
|
|
|
exchange = createExchange(Set.of(RUB_CNY));
|
|
|
|
executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
|
|
}
|
|
|
|
@Test
|
|
void testConcurrentClientCreation() throws InterruptedException {
|
|
int numClients = 100;
|
|
var latch = new CountDownLatch(numClients);
|
|
var futures = new ArrayList<CompletableFuture<Client>>();
|
|
|
|
for (int i = 0; i < numClients; i++) {
|
|
var future = CompletableFuture.supplyAsync(() -> {
|
|
try {
|
|
return exchange.createClient("Client " + UUID.randomUUID()).join();
|
|
} finally {
|
|
latch.countDown();
|
|
}
|
|
}, executorService);
|
|
futures.add(future);
|
|
}
|
|
|
|
latch.await(30, TimeUnit.SECONDS);
|
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
|
|
|
assertEquals(numClients, futures.stream().map(CompletableFuture::join).distinct().count(),
|
|
"All created clients should be unique");
|
|
}
|
|
|
|
|
|
@Test
|
|
void testConcurrentOrderPlacement() throws InterruptedException {
|
|
var numSellers = 10;
|
|
var numBuyers = 10;
|
|
var ordersPerClient = 20;
|
|
var latch = new CountDownLatch(numSellers + numBuyers);
|
|
|
|
var sellers = new ArrayList<Client>();
|
|
for (int i = 0; i < numSellers; i++) {
|
|
var seller = exchange.createClient("Seller " + i).join();
|
|
sellers.add(seller);
|
|
}
|
|
|
|
var buyers = new ArrayList<Client>();
|
|
for (int i = 0; i < numBuyers; i++) {
|
|
var buyer = exchange.createClient("Buyer " + i).join();
|
|
buyers.add(buyer);
|
|
}
|
|
|
|
var sellerFutures = sellers.stream()
|
|
.map(seller -> CompletableFuture.runAsync(() -> {
|
|
try {
|
|
for (int i = 0; i < ordersPerClient; i++) {
|
|
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 10.0, 10.0).join();
|
|
Thread.sleep(10);
|
|
}
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
} finally {
|
|
latch.countDown();
|
|
}
|
|
}, executorService))
|
|
.toList();
|
|
|
|
var buyerFutures = buyers.stream()
|
|
.map(buyer -> CompletableFuture.runAsync(() -> {
|
|
try {
|
|
for (int i = 0; i < ordersPerClient; i++) {
|
|
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 1.2, 10.0).join();
|
|
Thread.sleep(10);
|
|
}
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
} finally {
|
|
latch.countDown();
|
|
}
|
|
}, executorService))
|
|
.toList();
|
|
|
|
latch.await(30, TimeUnit.SECONDS);
|
|
CompletableFuture.allOf(
|
|
Stream.concat(sellerFutures.stream(), buyerFutures.stream())
|
|
.toArray(CompletableFuture[]::new)
|
|
).join();
|
|
|
|
for (var seller : sellers) {
|
|
assertTrue((long) exchange.getOrders(seller.id()).join().size() <= ordersPerClient);
|
|
}
|
|
|
|
for (var buyer : buyers) {
|
|
assertTrue((long) exchange.getOrders(buyer.id()).join().size() <= ordersPerClient);
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void testConcurrentOrderMatchingWithDifferentPrices() throws InterruptedException {
|
|
var numOrders = 50;
|
|
var latch = new CountDownLatch(2);
|
|
|
|
var seller = exchange.createClient("Seller").join();
|
|
var buyer = exchange.createClient("Buyer").join();
|
|
|
|
var sellerFuture = CompletableFuture.runAsync(() -> {
|
|
try {
|
|
for (int i = 0; i < numOrders; i++) {
|
|
var price = 1.0 + (i * 0.01);
|
|
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, price, 10.0).join();
|
|
Thread.sleep(5);
|
|
}
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
} finally {
|
|
latch.countDown();
|
|
}
|
|
}, executorService);
|
|
|
|
var buyerFuture = CompletableFuture.runAsync(() -> {
|
|
try {
|
|
for (var i = 0; i < numOrders; i++) {
|
|
var price = 2.0 - (i * 0.01);
|
|
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, price, 10.0).join();
|
|
Thread.sleep(5);
|
|
}
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
} finally {
|
|
latch.countDown();
|
|
}
|
|
}, executorService);
|
|
|
|
latch.await(30, TimeUnit.SECONDS);
|
|
CompletableFuture.allOf(sellerFuture, buyerFuture).join();
|
|
|
|
var sellerOrders = exchange.getOrders(seller.id()).join();
|
|
var buyerOrders = exchange.getOrders(buyer.id()).join();
|
|
|
|
var remainingSellerOrders = sellerOrders.stream().filter(order -> !order.closed()).count();
|
|
var remainingBuyerOrders = buyerOrders.stream().filter(order -> !order.closed()).count();
|
|
|
|
assertTrue(remainingSellerOrders + remainingBuyerOrders < numOrders * 2,
|
|
"Some orders should have been matched");
|
|
}
|
|
|
|
@Test
|
|
void testBalanceConsistencyWithConcurrentTradesSamePrices() throws InterruptedException {
|
|
var numTraders = 100;
|
|
var numOrders = 1000;
|
|
var latch = new CountDownLatch(numTraders * 2);
|
|
|
|
var sellers = new ArrayList<Client>();
|
|
var buyers = new ArrayList<Client>();
|
|
var sellAmount = 100.0;
|
|
var buyAmount = 100.0;
|
|
|
|
for (var i = 0; i < numTraders; i++) {
|
|
var seller = exchange.createClient("Seller " + i).join();
|
|
var buyer = exchange.createClient("Buyer " + i).join();
|
|
|
|
sellers.add(seller);
|
|
buyers.add(buyer);
|
|
}
|
|
|
|
var futures = new ArrayList<CompletableFuture<Void>>();
|
|
|
|
for (var seller : sellers) {
|
|
var future = CompletableFuture.runAsync(() -> {
|
|
try {
|
|
for (var i = 0; i < numOrders; i++) {
|
|
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, buyAmount, sellAmount).join();
|
|
}
|
|
} finally {
|
|
latch.countDown();
|
|
}
|
|
}, executorService);
|
|
futures.add(future);
|
|
}
|
|
|
|
for (var buyer : buyers) {
|
|
var future = CompletableFuture.runAsync(() -> {
|
|
try {
|
|
for (var i = 0; i < numOrders; i++) {
|
|
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, sellAmount, buyAmount).join();
|
|
}
|
|
} finally {
|
|
latch.countDown();
|
|
}
|
|
}, executorService);
|
|
futures.add(future);
|
|
}
|
|
|
|
latch.await(30, TimeUnit.SECONDS);
|
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
|
var totalRubMoney = 0.0;
|
|
var totalCnyMoney = 0.0;
|
|
|
|
for (var seller : sellers) {
|
|
var orders = exchange.getOrders(seller.id()).join();
|
|
|
|
totalCnyMoney += orders.stream().mapToDouble(order -> order.originalData().quantity() - order.lastData().quantity()).sum();
|
|
totalRubMoney += orders.stream().mapToDouble(order -> order.originalData().price() - order.lastData().price()).sum();
|
|
}
|
|
|
|
assertEquals(numOrders * numTraders * sellAmount, totalCnyMoney, 0.001);
|
|
assertEquals(numOrders * numTraders * buyAmount, totalRubMoney, 0.001);
|
|
|
|
for (var buyer : buyers) {
|
|
var orders = exchange.getOrders(buyer.id()).join();
|
|
|
|
totalCnyMoney -= orders.stream().mapToDouble(order -> order.originalData().quantity() - order.lastData().quantity()).sum();
|
|
totalRubMoney -= orders.stream().mapToDouble(order -> order.originalData().price() - order.lastData().price()).sum();
|
|
}
|
|
|
|
assertEquals(0.0, totalRubMoney, 0.001);
|
|
assertEquals(0.0, totalCnyMoney, 0.001);
|
|
}
|
|
|
|
@Test
|
|
void testBalanceConsistencyWithConcurrentTrades() throws InterruptedException {
|
|
var numTraders = 100;
|
|
var numOrders = 1000;
|
|
var latch = new CountDownLatch(numTraders * 3);
|
|
|
|
var sellers = new ArrayList<Client>();
|
|
var buyers = new ArrayList<Client>();
|
|
var sellAmount = 100.0;
|
|
var buyAmount = 50.0;
|
|
|
|
for (var i = 0; i < numTraders; i++) {
|
|
var seller = exchange.createClient("Seller " + i).join();
|
|
var buyer = exchange.createClient("Buyer " + i).join();
|
|
var buyer2 = exchange.createClient("Buyer " + i).join();
|
|
|
|
sellers.add(seller);
|
|
buyers.add(buyer);
|
|
buyers.add(buyer2);
|
|
}
|
|
|
|
var futures = new ArrayList<CompletableFuture<Void>>();
|
|
|
|
for (var seller : sellers) {
|
|
var future = CompletableFuture.runAsync(() -> {
|
|
try {
|
|
for (var i = 0; i < numOrders; i++) {
|
|
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, buyAmount, sellAmount).join();
|
|
}
|
|
} finally {
|
|
latch.countDown();
|
|
}
|
|
}, executorService);
|
|
futures.add(future);
|
|
}
|
|
|
|
for (var buyer : buyers) {
|
|
var future = CompletableFuture.runAsync(() -> {
|
|
try {
|
|
for (var i = 0; i < numOrders; i++) {
|
|
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, sellAmount, buyAmount).join();
|
|
}
|
|
} finally {
|
|
latch.countDown();
|
|
}
|
|
}, executorService);
|
|
futures.add(future);
|
|
}
|
|
|
|
latch.await(30, TimeUnit.SECONDS);
|
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
|
var totalRubMoney = 0.0;
|
|
var totalCnyMoney = 0.0;
|
|
|
|
for (var seller : sellers) {
|
|
var orders = exchange.getOrders(seller.id()).join();
|
|
|
|
totalCnyMoney += orders.stream().mapToDouble(order -> order.originalData().quantity() - order.lastData().quantity()).sum();
|
|
totalRubMoney += orders.stream().mapToDouble(order -> order.originalData().price() - order.lastData().price()).sum();
|
|
}
|
|
|
|
assertEquals(numOrders * numTraders * sellAmount, totalCnyMoney, 0.001);
|
|
assertEquals(numOrders * numTraders * buyAmount, totalRubMoney, 0.001);
|
|
|
|
for (var buyer : buyers) {
|
|
var orders = exchange.getOrders(buyer.id()).join();
|
|
|
|
totalCnyMoney -= orders.stream().mapToDouble(order -> order.originalData().quantity() - order.lastData().quantity()).sum();
|
|
totalRubMoney -= orders.stream().mapToDouble(order -> order.originalData().price() - order.lastData().price()).sum();
|
|
}
|
|
|
|
assertEquals(0.0, totalCnyMoney, 0.001);
|
|
assertEquals(0.0, totalRubMoney, 0.001);
|
|
}
|
|
|
|
@AfterEach
|
|
void tearDown() {
|
|
executorService.shutdown();
|
|
try {
|
|
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
|
|
executorService.shutdownNow();
|
|
}
|
|
} catch (InterruptedException e) {
|
|
executorService.shutdownNow();
|
|
}
|
|
|
|
shutdownExchange(exchange);
|
|
}
|
|
}
|