1
0
This commit is contained in:
2024-11-17 23:05:53 +03:00
parent 9ab033bd58
commit 045a5a9f64
2 changed files with 94 additions and 96 deletions

View File

@@ -191,22 +191,22 @@ class ConcurrentCurrencyExchangeTest {
@Test @Test
void testConcurrentOrderPlacement() throws InterruptedException { void testConcurrentOrderPlacement() throws InterruptedException {
var numSellers = 5; var numSellers = 10;
var numBuyers = 5; var numBuyers = 10;
var ordersPerClient = 20; var ordersPerClient = 20;
var latch = new CountDownLatch(numSellers + numBuyers); var latch = new CountDownLatch(numSellers + numBuyers);
var sellers = new ArrayList<Client>(); var sellers = new ArrayList<Client>();
for (int i = 0; i < numSellers; i++) { for (int i = 0; i < numSellers; i++) {
var seller = exchange.createClient("Seller " + i).join(); var seller = exchange.createClient("Seller " + i).join();
exchange.deposit(seller.id(), CNY, 1000.0).join(); exchange.deposit(seller.id(), CNY, 1000000.0).join();
sellers.add(seller); sellers.add(seller);
} }
var buyers = new ArrayList<Client>(); var buyers = new ArrayList<Client>();
for (int i = 0; i < numBuyers; i++) { for (int i = 0; i < numBuyers; i++) {
var buyer = exchange.createClient("Buyer " + i).join(); var buyer = exchange.createClient("Buyer " + i).join();
exchange.deposit(buyer.id(), RUB, 1200.0).join(); exchange.deposit(buyer.id(), RUB, 1200000.0).join();
buyers.add(buyer); buyers.add(buyer);
} }
@@ -214,7 +214,7 @@ class ConcurrentCurrencyExchangeTest {
.map(seller -> CompletableFuture.runAsync(() -> { .map(seller -> CompletableFuture.runAsync(() -> {
try { try {
for (int i = 0; i < ordersPerClient; i++) { for (int i = 0; i < ordersPerClient; i++) {
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 1.2, 10.0).join(); exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 10.0, 10.0).join();
Thread.sleep(10); Thread.sleep(10);
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
@@ -350,20 +350,22 @@ class ConcurrentCurrencyExchangeTest {
} }
@Test @Test
void testBalanceConsistencyWithConcurrentTradesAndWithdrawals() throws InterruptedException { void testBalanceConsistencyWithConcurrentTrades() throws InterruptedException {
var numTraders = 5; var numTraders = 10;
var numOrders = 1000;
var latch = new CountDownLatch(numTraders * 2); var latch = new CountDownLatch(numTraders * 2);
var sellers = new ArrayList<Client>(); var sellers = new ArrayList<Client>();
var buyers = new ArrayList<Client>(); var buyers = new ArrayList<Client>();
var initialAmount = 1000.0; var sellAmount = 100.0;
var buyAmount = 100.0;
for (var i = 0; i < numTraders; i++) { for (var i = 0; i < numTraders; i++) {
var seller = exchange.createClient("Seller " + i).join(); var seller = exchange.createClient("Seller " + i).join();
var buyer = exchange.createClient("Buyer " + i).join(); var buyer = exchange.createClient("Buyer " + i).join();
exchange.deposit(seller.id(), CNY, initialAmount).join(); exchange.deposit(seller.id(), CNY, sellAmount * numOrders).join();
exchange.deposit(buyer.id(), RUB, initialAmount).join(); exchange.deposit(buyer.id(), RUB, buyAmount * numOrders).join();
sellers.add(seller); sellers.add(seller);
buyers.add(buyer); buyers.add(buyer);
@@ -374,17 +376,10 @@ class ConcurrentCurrencyExchangeTest {
for (var seller : sellers) { for (var seller : sellers) {
var future = CompletableFuture.runAsync(() -> { var future = CompletableFuture.runAsync(() -> {
try { try {
for (int i = 0; i < 10; i++) { for (var i = 0; i < numOrders; i++)
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 1.2, 10.0).join(); {
try { exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, buyAmount, sellAmount).join();
exchange.withdraw(seller.id(), CNY, 1.0).join();
} catch (CompletionException ignored) {
} }
Thread.sleep(10);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally { } finally {
latch.countDown(); latch.countDown();
} }
@@ -395,16 +390,10 @@ class ConcurrentCurrencyExchangeTest {
for (var buyer : buyers) { for (var buyer : buyers) {
var future = CompletableFuture.runAsync(() -> { var future = CompletableFuture.runAsync(() -> {
try { try {
for (int i = 0; i < 10; i++) { for (var i = 0; i < numOrders; i++)
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 1.2, 10.0).join(); {
try { exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, sellAmount, buyAmount).join();
exchange.withdraw(buyer.id(), RUB, 1.0).join();
} catch (CompletionException ignored) {
} }
Thread.sleep(10);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally { } finally {
latch.countDown(); latch.countDown();
} }
@@ -414,12 +403,22 @@ class ConcurrentCurrencyExchangeTest {
latch.await(30, TimeUnit.SECONDS); latch.await(30, TimeUnit.SECONDS);
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
var totalRubMoney = 0.0;
var totalCnyMoney = 0.0;
for (var client : Stream.concat(sellers.stream(), buyers.stream()).toList()) { for (var seller : sellers) {
var balances = exchange.getBalances(client.id()).join(); var balances = exchange.getBalances(seller.id()).join();
assertTrue(balances.get(RUB) >= 0, "RUB balance should not be negative"); totalCnyMoney += balances.get(CNY);
assertTrue(balances.get(CNY) >= 0, "CNY balance should not be negative"); totalRubMoney += balances.get(RUB);
} }
for (var buyer : buyers) {
var balances = exchange.getBalances(buyer.id()).join();
totalCnyMoney += balances.get(CNY);
totalRubMoney += balances.get(RUB);
}
assertEquals(totalRubMoney, sellAmount * numOrders * numTraders, 0.001);
assertEquals(totalCnyMoney, sellAmount * numOrders * numTraders, 0.001);
} }
@AfterEach @AfterEach

View File

@@ -31,7 +31,6 @@ class CurrencyExchangeTest {
); );
} }
// Client Tests
@Test @Test
void testCreateClient() throws ExecutionException, InterruptedException { void testCreateClient() throws ExecutionException, InterruptedException {
var client = exchange.createClient("Trader").get(); var client = exchange.createClient("Trader").get();
@@ -52,7 +51,7 @@ class CurrencyExchangeTest {
assertThrows(Exception.class, () -> exchange.getBalances(nonexistentId).get()); assertThrows(Exception.class, () -> exchange.getBalances(nonexistentId).get());
} }
// Deposit Tests
@Test @Test
void testDeposit() throws ExecutionException, InterruptedException { void testDeposit() throws ExecutionException, InterruptedException {
var client = exchange.createClient("Trader").get(); var client = exchange.createClient("Trader").get();
@@ -77,7 +76,7 @@ class CurrencyExchangeTest {
assertThrows(ExecutionException.class, () -> exchange.deposit(client.id(), null, 100.0).get()); assertThrows(ExecutionException.class, () -> exchange.deposit(client.id(), null, 100.0).get());
} }
// Withdraw Tests
@Test @Test
void testWithdraw() throws ExecutionException, InterruptedException { void testWithdraw() throws ExecutionException, InterruptedException {
var client = exchange.createClient("Trader").get(); var client = exchange.createClient("Trader").get();
@@ -104,16 +103,16 @@ class CurrencyExchangeTest {
exchange.deposit(client.id(), RUB, 500.0).get(); exchange.deposit(client.id(), RUB, 500.0).get();
var balancesAfterDeposit = exchange.getBalances(client.id()).get(); var balancesAfterDeposit = exchange.getBalances(client.id()).get();
assertEquals(500.0, balancesAfterDeposit.get(RUB)); // Trader has 500 RUB assertEquals(500.0, balancesAfterDeposit.get(RUB));
exchange.withdraw(client.id(), RUB, 200.0).get(); exchange.withdraw(client.id(), RUB, 200.0).get();
var balancesAfterWithdraw = exchange.getBalances(client.id()).get(); var balancesAfterWithdraw = exchange.getBalances(client.id()).get();
assertEquals(300.0, balancesAfterWithdraw.get(RUB)); // Trader has 300 RUB assertEquals(300.0, balancesAfterWithdraw.get(RUB));
assertThrows(ExecutionException.class, () -> exchange.withdraw(client.id(), RUB, 400.0).get()); // Trader has only 300 RUB left assertThrows(ExecutionException.class, () -> exchange.withdraw(client.id(), RUB, 400.0).get());
} }
// Order Tests
@Test @Test
void testPlaceOrder() throws ExecutionException, InterruptedException { void testPlaceOrder() throws ExecutionException, InterruptedException {
var client = exchange.createClient("Trader").get(); var client = exchange.createClient("Trader").get();
@@ -156,7 +155,7 @@ class CurrencyExchangeTest {
); );
} }
// Order Matching Tests
@Test @Test
void testMatchingOrders() throws ExecutionException, InterruptedException { void testMatchingOrders() throws ExecutionException, InterruptedException {
var buyer = exchange.createClient("Buyer").get(); var buyer = exchange.createClient("Buyer").get();
@@ -171,10 +170,10 @@ class CurrencyExchangeTest {
Map<Currency, Double> buyerBalances = exchange.getBalances(buyer.id()).get(); Map<Currency, Double> buyerBalances = exchange.getBalances(buyer.id()).get();
Map<Currency, Double> sellerBalances = exchange.getBalances(seller.id()).get(); Map<Currency, Double> sellerBalances = exchange.getBalances(seller.id()).get();
assertEquals(100.0, buyerBalances.get(CNY)); // Buyer bought 100 CNY assertEquals(100.0, buyerBalances.get(CNY));
assertEquals(0.0, buyerBalances.get(RUB)); // Buyer sold 120 RUB so he has no RUB left assertEquals(0.0, buyerBalances.get(RUB));
assertEquals(120.0, sellerBalances.get(RUB)); // Seller bought 120 RUB assertEquals(120.0, sellerBalances.get(RUB));
assertEquals(0.0, sellerBalances.get(CNY)); // Seller sold 100 CNY so he has no CNY left assertEquals(0.0, sellerBalances.get(CNY));
} }
@Test @Test
@@ -191,13 +190,13 @@ class CurrencyExchangeTest {
var buyerBalances = exchange.getBalances(buyer.id()).get(); var buyerBalances = exchange.getBalances(buyer.id()).get();
var sellerBalances = exchange.getBalances(seller.id()).get(); var sellerBalances = exchange.getBalances(seller.id()).get();
assertEquals(50.0, buyerBalances.get(CNY)); // Buyer bought 50 CNY assertEquals(50.0, buyerBalances.get(CNY));
assertEquals(60.0, sellerBalances.get(RUB)); // Seller bought 60 RUB assertEquals(60.0, sellerBalances.get(RUB));
var sellerOrders = exchange.getActiveOrders(seller.id()).get(); var sellerOrders = exchange.getActiveOrders(seller.id()).get();
var remainingOrder = sellerOrders.iterator().next(); var remainingOrder = sellerOrders.iterator().next();
// Seller has 60/50 RUB/CNY order left
assertEquals(RUB_CNY, remainingOrder.pair()); assertEquals(RUB_CNY, remainingOrder.pair());
assertEquals(50.0, remainingOrder.quantity()); assertEquals(50.0, remainingOrder.quantity());
assertEquals(60.0, remainingOrder.price()); assertEquals(60.0, remainingOrder.price());
@@ -221,12 +220,12 @@ class CurrencyExchangeTest {
var buyer2Balances = exchange.getBalances(buyer2.id()).get(); var buyer2Balances = exchange.getBalances(buyer2.id()).get();
var sellerBalances = exchange.getBalances(seller.id()).get(); var sellerBalances = exchange.getBalances(seller.id()).get();
assertEquals(50.0, buyer1Balances.get(CNY)); // Buyer1 bought 50 CNY assertEquals(50.0, buyer1Balances.get(CNY));
assertEquals(50.0, buyer2Balances.get(CNY)); // Buyer2 bought 50 CNY assertEquals(50.0, buyer2Balances.get(CNY));
assertEquals(120.0, sellerBalances.get(RUB)); // Seller bought 120 RUB assertEquals(120.0, sellerBalances.get(RUB));
var sellerOrders = exchange.getActiveOrders(seller.id()).get(); var sellerOrders = exchange.getActiveOrders(seller.id()).get();
assertFalse(sellerOrders.iterator().hasNext()); // Seller has no orders left assertFalse(sellerOrders.iterator().hasNext());
} }
@Test @Test
@@ -243,10 +242,10 @@ class CurrencyExchangeTest {
var buyerBalances = exchange.getBalances(buyer.id()).get(); var buyerBalances = exchange.getBalances(buyer.id()).get();
var sellerBalances = exchange.getBalances(seller.id()).get(); var sellerBalances = exchange.getBalances(seller.id()).get();
assertEquals(100.0, buyerBalances.get(CNY)); // Buyer bought 100 CNY assertEquals(100.0, buyerBalances.get(CNY));
assertEquals(180.0, buyerBalances.get(RUB)); // Buyer spends 120 RUB assertEquals(180.0, buyerBalances.get(RUB));
assertEquals(120.0, sellerBalances.get(RUB)); // Seller bought 120 RUB assertEquals(120.0, sellerBalances.get(RUB));
assertEquals(100.0, sellerBalances.get(CNY)); // Seller retains remaining CNY assertEquals(100.0, sellerBalances.get(CNY));
} }
@Test @Test
@@ -268,18 +267,18 @@ class CurrencyExchangeTest {
var buyer2Balances = exchange.getBalances(buyer2.id()).get(); var buyer2Balances = exchange.getBalances(buyer2.id()).get();
var sellerBalances = exchange.getBalances(seller.id()).get(); var sellerBalances = exchange.getBalances(seller.id()).get();
assertEquals(30.0, buyer1Balances.get(CNY)); // Buyer1 bought 30 CNY assertEquals(30.0, buyer1Balances.get(CNY));
assertEquals(24.0, buyer1Balances.get(RUB)); // Buyer1 sold 36 CNY assertEquals(24.0, buyer1Balances.get(RUB));
assertEquals(30.0, buyer2Balances.get(CNY)); // Buyer2 bought 30 CNY assertEquals(30.0, buyer2Balances.get(CNY));
assertEquals(24.0, buyer2Balances.get(RUB)); // Buyer2 sold 36 CNY assertEquals(24.0, buyer2Balances.get(RUB));
var sellerOrders = exchange.getActiveOrders(seller.id()).get(); var sellerOrders = exchange.getActiveOrders(seller.id()).get();
var sellerOrder = sellerOrders.iterator().next(); var sellerOrder = sellerOrders.iterator().next();
assertEquals(40.0, sellerOrder.quantity()); // 40 CNY reserved assertEquals(40.0, sellerOrder.quantity());
assertEquals(48.0, sellerOrder.price()); // 1.2 RUB per CNY assertEquals(48.0, sellerOrder.price());
assertEquals(72.0, sellerBalances.get(RUB)); // 72 RUB earned assertEquals(72.0, sellerBalances.get(RUB));
} }
@Test @Test
@@ -314,8 +313,8 @@ class CurrencyExchangeTest {
exchange.deposit(seller1.id(), CNY, 50.0).get(); exchange.deposit(seller1.id(), CNY, 50.0).get();
exchange.deposit(seller2.id(), CNY, 50.0).get(); exchange.deposit(seller2.id(), CNY, 50.0).get();
exchange.placeOrder(seller1.id(), RUB_CNY, OrderType.SELL, 75, 50.0).get(); // Higher price exchange.placeOrder(seller1.id(), RUB_CNY, OrderType.SELL, 75, 50.0).get();
exchange.placeOrder(seller2.id(), RUB_CNY, OrderType.SELL, 60, 50.0).get(); // Lower price exchange.placeOrder(seller2.id(), RUB_CNY, OrderType.SELL, 60, 50.0).get();
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 75, 50.0).get(); exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 75, 50.0).get();
@@ -323,14 +322,14 @@ class CurrencyExchangeTest {
var seller1Balances = exchange.getBalances(seller1.id()).get(); var seller1Balances = exchange.getBalances(seller1.id()).get();
var seller2Balances = exchange.getBalances(seller2.id()).get(); var seller2Balances = exchange.getBalances(seller2.id()).get();
assertEquals(50.0, buyerBalances.get(CNY)); // Buyer gets 50 CNY from the lower-priced seller assertEquals(50.0, buyerBalances.get(CNY));
assertEquals(140.0, buyerBalances.get(RUB)); // Buyer spends 60 RUB assertEquals(140.0, buyerBalances.get(RUB));
assertEquals(60.0, seller2Balances.get(RUB)); // Seller2 sells at 60 price assertEquals(60.0, seller2Balances.get(RUB));
assertEquals(0.0, seller2Balances.get(CNY)); // Seller2 has no CNY left assertEquals(0.0, seller2Balances.get(CNY));
assertNull(seller1Balances.get(RUB)); // Seller1 remains untouched assertNull(seller1Balances.get(RUB));
assertEquals(0.0, seller1Balances.get(CNY)); // Seller1 still has their CNY reserved assertEquals(0.0, seller1Balances.get(CNY));
} }
@Test @Test
@@ -345,9 +344,9 @@ class CurrencyExchangeTest {
exchange.deposit(seller2.id(), CNY, 50.0).get(); exchange.deposit(seller2.id(), CNY, 50.0).get();
exchange.deposit(seller3.id(), CNY, 50.0).get(); exchange.deposit(seller3.id(), CNY, 50.0).get();
exchange.placeOrder(seller1.id(), RUB_CNY, OrderType.SELL, 75.0, 50.0).get(); // Highest price exchange.placeOrder(seller1.id(), RUB_CNY, OrderType.SELL, 75.0, 50.0).get();
exchange.placeOrder(seller2.id(), RUB_CNY, OrderType.SELL, 65.0, 50.0).get(); // Medium price exchange.placeOrder(seller2.id(), RUB_CNY, OrderType.SELL, 65.0, 50.0).get();
exchange.placeOrder(seller3.id(), RUB_CNY, OrderType.SELL, 60.0, 50.0).get(); // Lowest price exchange.placeOrder(seller3.id(), RUB_CNY, OrderType.SELL, 60.0, 50.0).get();
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 150.0, 100.0).get(); exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 150.0, 100.0).get();
@@ -356,17 +355,17 @@ class CurrencyExchangeTest {
var seller2Balances = exchange.getBalances(seller2.id()).get(); var seller2Balances = exchange.getBalances(seller2.id()).get();
var seller3Balances = exchange.getBalances(seller3.id()).get(); var seller3Balances = exchange.getBalances(seller3.id()).get();
assertEquals(100.0, buyerBalances.get(CNY)); // Buyer receives 100 CNY (50 from seller3 and 50 from seller2) assertEquals(100.0, buyerBalances.get(CNY));
assertEquals(175.0, buyerBalances.get(RUB)); // Buyer spends 125 RUB (60 + 65) assertEquals(175.0, buyerBalances.get(RUB));
assertEquals(60.0, seller3Balances.get(RUB)); // Seller3 sells at 60 price assertEquals(60.0, seller3Balances.get(RUB));
assertEquals(0.0, seller3Balances.get(CNY)); // Seller3's CNY is reduced to 0 assertEquals(0.0, seller3Balances.get(CNY));
assertEquals(65.0, seller2Balances.get(RUB)); // Seller2 sells at 65 price assertEquals(65.0, seller2Balances.get(RUB));
assertEquals(0.0, seller2Balances.get(CNY)); // Seller2's CNY is reduced to 0 assertEquals(0.0, seller2Balances.get(CNY));
assertNull(seller1Balances.get(RUB)); // Seller1 remains untouched assertNull(seller1Balances.get(RUB));
assertEquals(0.0, seller1Balances.get(CNY)); // Seller1 still has their CNY reserved assertEquals(0.0, seller1Balances.get(CNY));
} }
@Test @Test
@@ -379,8 +378,8 @@ class CurrencyExchangeTest {
exchange.deposit(seller1.id(), CNY, 50.0).get(); exchange.deposit(seller1.id(), CNY, 50.0).get();
exchange.deposit(seller2.id(), CNY, 50.0).get(); exchange.deposit(seller2.id(), CNY, 50.0).get();
exchange.placeOrder(seller1.id(), RUB_CNY, OrderType.SELL, 60, 50.0).get(); // Placed first exchange.placeOrder(seller1.id(), RUB_CNY, OrderType.SELL, 60, 50.0).get();
exchange.placeOrder(seller2.id(), RUB_CNY, OrderType.SELL, 60, 50.0).get(); // Placed second exchange.placeOrder(seller2.id(), RUB_CNY, OrderType.SELL, 60, 50.0).get();
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 60, 50.0).get(); exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 60, 50.0).get();
@@ -388,13 +387,13 @@ class CurrencyExchangeTest {
var seller1Balances = exchange.getBalances(seller1.id()).get(); var seller1Balances = exchange.getBalances(seller1.id()).get();
var seller2Balances = exchange.getBalances(seller2.id()).get(); var seller2Balances = exchange.getBalances(seller2.id()).get();
assertEquals(50.0, buyerBalances.get(CNY)); // Buyer gets 50 CNY assertEquals(50.0, buyerBalances.get(CNY));
assertEquals(140.0, buyerBalances.get(RUB)); // Buyer spends 60 RUB assertEquals(140.0, buyerBalances.get(RUB));
assertEquals(60.0, seller1Balances.get(RUB)); // Seller1 sells first assertEquals(60.0, seller1Balances.get(RUB));
assertEquals(0.0, seller1Balances.get(CNY)); // Seller1's CNY is reduced to 0 assertEquals(0.0, seller1Balances.get(CNY));
assertNull(seller2Balances.get(RUB)); // Seller2 remains untouched assertNull(seller2Balances.get(RUB));
} }
@Test @Test
@@ -416,13 +415,13 @@ class CurrencyExchangeTest {
var buyer2Balances = exchange.getBalances(buyer2.id()).get(); var buyer2Balances = exchange.getBalances(buyer2.id()).get();
var sellerBalances = exchange.getBalances(seller.id()).get(); var sellerBalances = exchange.getBalances(seller.id()).get();
assertNull(sellerBalances.get(RUB)); // Seller did not sell anything assertNull(sellerBalances.get(RUB));
assertEquals(0.0, sellerBalances.get(CNY)); // Seller has no CNY left assertEquals(0.0, sellerBalances.get(CNY));
assertNull(buyer1Balances.get(CNY)); // Buyer1 did not buy anything assertNull(buyer1Balances.get(CNY));
assertEquals(120.0, buyer1Balances.get(RUB)); assertEquals(120.0, buyer1Balances.get(RUB));
assertNull(buyer2Balances.get(CNY)); // Buyer2 did not buy anything assertNull(buyer2Balances.get(CNY));
assertEquals(125.0, buyer2Balances.get(RUB)); assertEquals(125.0, buyer2Balances.get(RUB));
} }