1
0

lab9 again...

This commit is contained in:
2024-11-18 09:11:19 +03:00
parent 045a5a9f64
commit 826389b3c3
16 changed files with 559 additions and 939 deletions

View File

@@ -1,28 +1,23 @@
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 ru.lionarius.api.order.OrderView;
import ru.lionarius.api.order.message.OrderMessage;
import java.util.Map;
import java.util.Optional;
import java.util.List;
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<UUID> 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<List<OrderView>> getOrders(UUID clientId);
CompletableFuture<Map<Currency, Double>> getBalances(UUID clientId);
CompletableFuture<List<OrderMessage>> getOrderMessages(UUID clientId, UUID orderId);
}

View File

@@ -1,27 +0,0 @@
package ru.lionarius.api.client;
import com.google.common.collect.ImmutableMap;
import ru.lionarius.api.currency.Currency;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class Balance {
private final Map<Currency, Double> balances = new ConcurrentHashMap<>();
public Map<Currency, Double> getBalances() {
return ImmutableMap.copyOf(balances);
}
public double getBalance(Currency currency) {
return balances.getOrDefault(currency, 0.0);
}
public void deposit(Currency currency, double amount) {
balances.compute(currency, (k, v) -> v == null ? amount : v + amount);
}
public void withdraw(Currency currency, double amount) {
balances.compute(currency, (k, v) -> v == null ? -amount : v - amount);
}
}

View File

@@ -1,5 +1,8 @@
package ru.lionarius.api.client;
import ru.lionarius.api.order.OrderList;
import java.util.Optional;
import java.util.UUID;
@@ -8,5 +11,5 @@ public interface ClientRepository {
Optional<Client> getClient(UUID clientId);
Optional<Balance> getClientBalance(UUID clientId);
Optional<OrderList> getClientOrders(UUID clientId);
}

View File

@@ -1,19 +1,119 @@
package ru.lionarius.api.order;
import ru.lionarius.api.client.Client;
import com.google.common.collect.ImmutableList;
import ru.lionarius.api.currency.CurrencyPair;
import ru.lionarius.api.order.message.OrderCreatedMessage;
import ru.lionarius.api.order.message.OrderFilledMessage;
import ru.lionarius.api.order.message.OrderMessage;
import ru.lionarius.api.order.message.OrderMessageType;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public record Order(UUID id, Client client, OrderType type, CurrencyPair pair, double price, double quantity,
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 class Order {
private final UUID id = UUID.randomUUID();
private final UUID clientId;
private final OrderType type;
private final CurrencyPair pair;
private final OrderData originalData;
private OrderData lastData;
private boolean closed = false;
private final List<OrderMessage> messages = new ArrayList<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public Order(UUID clientId, OrderType type, CurrencyPair pair, OrderData data) {
this.clientId = clientId;
this.type = type;
this.pair = pair;
this.originalData = data;
pushMessage(new OrderCreatedMessage(data, LocalDateTime.now()));
}
public double rate() {
return price() / quantity();
public OrderView getView() {
lock.readLock().lock();
try {
return new OrderView(id, clientId, type, pair, originalData, lastData, closed);
} finally {
lock.readLock().unlock();
}
}
public UUID getId() {
return id;
}
public UUID getClientId() {
return clientId;
}
public OrderType getType() {
return type;
}
public CurrencyPair getPair() {
return pair;
}
public boolean isClosed() {
lock.readLock().lock();
try {
return closed;
} finally {
lock.readLock().unlock();
}
}
public OrderData getLastData() {
lock.readLock().lock();
try {
return lastData;
} finally {
lock.readLock().unlock();
}
}
public void pushMessage(OrderMessage message) {
if (closed)
throw new IllegalStateException("Order is closed");
lock.writeLock().lock();
try {
if (message.type() == OrderMessageType.CREATED) {
if (lastData != null)
throw new IllegalStateException("Order already has data");
lastData = ((OrderCreatedMessage) message).data();
} else if (message.type() == OrderMessageType.FILLED) {
lastData = ((OrderFilledMessage) message).newData();
} else if (message.type() == OrderMessageType.CLOSED) {
closed = true;
}
messages.add(message);
} finally {
lock.writeLock().unlock();
}
}
public List<OrderMessage> getMessages() {
lock.readLock().lock();
try {
return ImmutableList.copyOf(messages);
} finally {
lock.readLock().unlock();
}
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
@@ -24,9 +124,4 @@ public record Order(UUID id, Client client, OrderType type, CurrencyPair pair, d
return false;
}
@Override
public int hashCode() {
return id.hashCode();
}
}

View File

@@ -0,0 +1,7 @@
package ru.lionarius.api.order;
public record OrderData(double price, double quantity) {
public double rate() {
return price() / quantity();
}
}

View File

@@ -0,0 +1,13 @@
package ru.lionarius.api.order;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface OrderList {
void add(Order order);
Optional<Order> get(UUID orderId);
List<Order> get();
}

View File

@@ -0,0 +1,9 @@
package ru.lionarius.api.order;
import ru.lionarius.api.currency.CurrencyPair;
import java.util.UUID;
public record OrderView(UUID id, UUID clientId, OrderType type, CurrencyPair pair, OrderData originalData,
OrderData lastData, boolean closed) {
}

View File

@@ -0,0 +1,14 @@
package ru.lionarius.api.order.message;
public record OrderClosedMessage(Reason reason) implements OrderMessage {
@Override
public OrderMessageType type() {
return OrderMessageType.CLOSED;
}
public enum Reason {
CANCELLED,
FULFILLED
}
}

View File

@@ -0,0 +1,13 @@
package ru.lionarius.api.order.message;
import ru.lionarius.api.order.OrderData;
import java.time.LocalDateTime;
public record OrderCreatedMessage(OrderData data, LocalDateTime timestamp) implements OrderMessage {
@Override
public OrderMessageType type() {
return OrderMessageType.CREATED;
}
}

View File

@@ -0,0 +1,12 @@
package ru.lionarius.api.order.message;
import ru.lionarius.api.order.OrderData;
import java.util.UUID;
public record OrderFilledMessage(UUID otherOrderId, OrderData newData) implements OrderMessage {
@Override
public OrderMessageType type() {
return OrderMessageType.FILLED;
}
}

View File

@@ -0,0 +1,5 @@
package ru.lionarius.api.order.message;
public interface OrderMessage {
OrderMessageType type();
}

View File

@@ -0,0 +1,7 @@
package ru.lionarius.api.order.message;
public enum OrderMessageType {
CREATED,
CLOSED,
FILLED
}

View File

@@ -1,24 +1,24 @@
package ru.lionarius.impl;
import ru.lionarius.api.client.Balance;
import com.google.common.collect.ImmutableList;
import ru.lionarius.api.client.Client;
import ru.lionarius.api.client.ClientRepository;
import ru.lionarius.api.order.OrderList;
import ru.lionarius.api.order.Order;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.*;
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<>();
private final Map<UUID, OrdersList> orders = new ConcurrentHashMap<>();
@Override
public Client createClient(String name) {
var client = new Client(name);
clients.put(client.id(), client);
balances.put(client, new Balance());
orders.put(client.id(), new OrdersList());
return client;
}
@@ -29,7 +29,23 @@ public class InMemoryClientRepository implements ClientRepository {
}
@Override
public Optional<Balance> getClientBalance(UUID clientId) {
return Optional.ofNullable(balances.get(clients.get(clientId)));
public Optional<OrderList> getClientOrders(UUID clientId) {
return Optional.ofNullable(orders.get(clientId));
}
private static class OrdersList implements OrderList {
private final Map<UUID, Order> orders = new ConcurrentHashMap<>();
public void add(Order order) {
orders.put(order.getId(), order);
}
public Optional<Order> get(UUID orderId) {
return Optional.ofNullable(orders.get(orderId));
}
public List<Order> get() {
return ImmutableList.copyOf(orders.values());
}
}
}

View File

@@ -1,34 +1,39 @@
package ru.lionarius.impl;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import ru.lionarius.api.CurrencyExchange;
import ru.lionarius.api.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.OrderData;
import ru.lionarius.api.order.OrderType;
import ru.lionarius.api.order.OrderView;
import ru.lionarius.api.order.message.OrderClosedMessage;
import ru.lionarius.api.order.message.OrderFilledMessage;
import ru.lionarius.api.order.message.OrderMessage;
import java.util.*;
import java.util.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 Map<CurrencyPair, OrderBook> orderBooks;
private final ReadWriteLock ordersLock = new ReentrantReadWriteLock();
private final Set<Currency> allowedCurrencies;
private final Set<CurrencyPair> allowedPairs;
private final List<Currency> allowedCurrencies;
private final List<CurrencyPair> allowedPairs;
public PlainCurrencyExchange(Set<Currency> allowedCurrencies, Set<CurrencyPair> allowedPairs) {
this.allowedCurrencies = ImmutableSet.copyOf(allowedCurrencies);
this.allowedPairs = ImmutableSet.copyOf(allowedPairs);
public PlainCurrencyExchange(List<Currency> allowedCurrencies, List<CurrencyPair> allowedPairs) {
this.allowedCurrencies = ImmutableList.copyOf(allowedCurrencies);
this.allowedPairs = ImmutableList.copyOf(allowedPairs);
this.orderBooks = allowedPairs.stream()
.map(pair -> new AbstractMap.SimpleEntry<>(pair, new OrderBook()))
.collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
}
@Override
@@ -42,322 +47,160 @@ public class PlainCurrencyExchange implements CurrencyExchange {
}
@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) {
public CompletableFuture<UUID> placeOrder(UUID clientId, CurrencyPair pair, OrderType type, double price, double quantity) {
return CompletableFuture.supplyAsync(() -> {
validateOrderInputs(pair, type, price, quantity);
if (clientId == null)
throw new IllegalArgumentException("Client ID cannot be null");
var client = clientRepository.getClient(clientId).orElseThrow();
var balance = clientRepository.getClientBalance(clientId).orElseThrow();
if (pair == null)
throw new IllegalArgumentException("Currency pair cannot be null");
reserveFundsForOrder(balance, pair, type, price, quantity);
if (type == null)
throw new IllegalArgumentException("Order type cannot be null");
var newOrder = new Order(client, type, pair, price, quantity);
Optional<Order> orderToAdd = Optional.empty();
if (price <= 0.0)
throw new IllegalArgumentException("Price must be positive");
ordersLock.writeLock().lock();
try {
// Try to match the order
if (type == OrderType.BUY) {
orderToAdd = matchBuyOrder(newOrder);
} else {
orderToAdd = matchSellOrder(newOrder);
}
if (quantity <= 0.0)
throw new IllegalArgumentException("Quantity must be positive");
// 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());
}
}
var orders = clientRepository.getClientOrders(clientId).orElseThrow();
return orderToAdd;
} finally {
ordersLock.writeLock().unlock();
}
var order = new Order(clientId, type, pair, new OrderData(price, quantity));
orders.add(order);
var orderBook = orderBooks.get(pair);
orderBook.addOrder(order);
orderBook.matchOrders();
return order.getId();
}, 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");
if (clientId == null)
throw new IllegalArgumentException("Client ID cannot be null");
var client = clientRepository.getClient(clientId).orElseThrow();
var balance = clientRepository.getClientBalance(clientId).orElseThrow();
if (orderId == null)
throw new IllegalArgumentException("Order ID cannot be null");
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);
}
}
}
var orders = clientRepository.getClientOrders(clientId).orElseThrow();
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;
})
.filter(oppositeOrder -> oppositeOrder.client() != order.client())
.sorted((o1, o2) -> {
double rate1 = o1.rate();
double rate2 = o2.rate();
int rateCompare = order.type() == OrderType.BUY ?
Double.compare(rate1, rate2) : // For buy orders - ascending rate
Double.compare(rate2, rate1); // For sell orders - descending rate
return rateCompare != 0 ? rateCompare : o1.placedAt().compareTo(o2.placedAt());
})
.toList();
}
private Optional<Order> matchBuyOrder(Order buyOrder) {
var remainingBuyOrder = buyOrder;
List<Order> matchingOrders = getMatchingOrders(buyOrder);
for (Order sellOrder : matchingOrders) {
if (remainingBuyOrder.quantity() <= 0.0) break;
// Calculate matching quantity based on remaining quote currency amount
double matchQuantity = Math.min(remainingBuyOrder.quantity(), sellOrder.quantity());
// Calculate corresponding base currency amount using sell order's rate
double matchPrice = matchQuantity * sellOrder.rate();
// Create new orders with reduced quantities
var newBuyOrder = new Order(
remainingBuyOrder.client(),
remainingBuyOrder.type(),
remainingBuyOrder.pair(),
remainingBuyOrder.price() - matchQuantity * buyOrder.rate(),
remainingBuyOrder.quantity() - matchQuantity
);
var newSellOrder = new Order(
sellOrder.client(),
sellOrder.type(),
sellOrder.pair(),
sellOrder.price() - matchPrice,
sellOrder.quantity() - matchQuantity
);
// Execute the trade
executeTrade(remainingBuyOrder, sellOrder, matchQuantity);
// Update orders in the books
remainingBuyOrder = newBuyOrder;
sellOrders.get(buyOrder.pair()).remove(sellOrder);
if (newSellOrder.quantity() > 0) {
sellOrders.get(buyOrder.pair()).add(newSellOrder);
}
}
if (remainingBuyOrder.quantity() > 0) {
return Optional.of(remainingBuyOrder);
}
return Optional.empty();
}
private Optional<Order> matchSellOrder(Order sellOrder) {
var remainingSellOrder = sellOrder;
List<Order> matchingOrders = getMatchingOrders(sellOrder);
for (Order buyOrder : matchingOrders) {
if (remainingSellOrder.quantity() <= 0.0) break;
// Calculate matching quantity based on remaining quote currency amount
double matchQuantity = Math.min(remainingSellOrder.quantity(), buyOrder.quantity());
// Calculate corresponding base currency amount using buy order's rate
double matchPrice = (matchQuantity * buyOrder.price()) / buyOrder.quantity();
// Create new orders with reduced quantities
var newSellOrder = new Order(
remainingSellOrder.client(),
remainingSellOrder.type(),
remainingSellOrder.pair(),
remainingSellOrder.price() - matchPrice,
remainingSellOrder.quantity() - matchQuantity
);
var newBuyOrder = new Order(
buyOrder.client(),
buyOrder.type(),
buyOrder.pair(),
buyOrder.price() - matchPrice,
buyOrder.quantity() - matchQuantity
);
// Execute the trade
executeTrade(buyOrder, remainingSellOrder, matchQuantity);
// Update orders in the books
remainingSellOrder = newSellOrder;
buyOrders.get(sellOrder.pair()).remove(buyOrder);
if (newBuyOrder.quantity() > 0) {
buyOrders.get(sellOrder.pair()).add(newBuyOrder);
}
}
if (remainingSellOrder.quantity() > 0) {
return Optional.of(remainingSellOrder);
}
return Optional.empty();
}
private void executeTrade(Order buyOrder, Order sellOrder, double quantity) {
var price = quantity * sellOrder.rate();
// Get client balances
var buyerBalance = clientRepository.getClientBalance(buyOrder.client().id()).orElseThrow();
var sellerBalance = clientRepository.getClientBalance(sellOrder.client().id()).orElseThrow();
// Transfer currencies
buyerBalance.deposit(buyOrder.pair().quote(), quantity); // Buyer receives quote currency
sellerBalance.deposit(buyOrder.pair().base(), price); // Seller receives base currency
buyerBalance.deposit(buyOrder.pair().base(), quantity * buyOrder.rate() - price); // Return price difference to buyer
}
@Override
public CompletableFuture<Iterable<Order>> getActiveOrders(UUID clientId) {
return CompletableFuture.supplyAsync(() -> {
var client = clientRepository.getClient(clientId).orElseThrow();
ordersLock.readLock().lock();
try {
List<Order> clientOrders = new ArrayList<>();
// Collect all buy orders for the client
buyOrders.values().forEach(orders ->
orders.stream()
.filter(order -> order.client().id().equals(clientId))
.forEach(clientOrders::add)
);
// Collect all sell orders for the client
sellOrders.values().forEach(orders ->
orders.stream()
.filter(order -> order.client().id().equals(clientId))
.forEach(clientOrders::add)
);
return clientOrders;
} finally {
ordersLock.readLock().unlock();
}
orders.get(orderId).ifPresent(order -> {
order.pushMessage(new OrderClosedMessage(OrderClosedMessage.Reason.CANCELLED));
});
}, Runnable::run);
}
@Override
public CompletableFuture<Map<Currency, Double>> getBalances(UUID clientId) {
public CompletableFuture<List<OrderView>> getOrders(UUID clientId) {
return CompletableFuture.supplyAsync(() -> {
var client = clientRepository.getClient(clientId).orElseThrow();
var balance = clientRepository.getClientBalance(clientId).orElseThrow();
if (clientId == null)
throw new IllegalArgumentException("Client ID cannot be null");
return balance.getBalances();
var orders = clientRepository.getClientOrders(clientId).orElseThrow();
return orders.get().stream().map(Order::getView).toList();
}, Runnable::run);
}
@Override
public CompletableFuture<List<OrderMessage>> getOrderMessages(UUID clientId, UUID orderId) {
return CompletableFuture.supplyAsync(() -> {
if (clientId == null)
throw new IllegalArgumentException("Client ID cannot be null");
if (orderId == null)
throw new IllegalArgumentException("Order ID cannot be null");
var orders = clientRepository.getClientOrders(clientId).orElseThrow();
return orders.get(orderId).map(Order::getMessages).orElseThrow();
}, Runnable::run);
}
private static class OrderBook {
private final TreeMap<Double, List<Order>> buyOrders = new TreeMap<>(Collections.reverseOrder());
private final TreeMap<Double, List<Order>> sellOrders = new TreeMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public void addOrder(Order order) {
var orders = order.getType() == OrderType.BUY ? buyOrders : sellOrders;
lock.writeLock().lock();
try {
orders.computeIfAbsent(order.getLastData().rate(), (k) -> new ArrayList<>()).add(order);
} finally {
lock.writeLock().unlock();
}
}
public void matchOrders() {
lock.writeLock().lock();
try {
while (!buyOrders.isEmpty() && !sellOrders.isEmpty()) {
var bestBuyEntry = buyOrders.firstEntry();
var bestSellEntry = sellOrders.firstEntry();
var bestBuy = bestBuyEntry.getValue().getFirst();
var bestSell = bestSellEntry.getValue().getFirst();
if (bestBuy.isClosed()) {
bestBuyEntry.getValue().remove(bestBuy);
if (bestBuyEntry.getValue().isEmpty())
buyOrders.remove(bestBuyEntry.getKey());
continue;
}
if (bestSell.isClosed()) {
bestSellEntry.getValue().remove(bestSell);
if (bestSellEntry.getValue().isEmpty())
sellOrders.remove(bestSellEntry.getKey());
continue;
}
if (bestBuy.getLastData().rate() < bestSell.getLastData().rate())
break;
var matchQuantity = Math.min(bestBuy.getLastData().quantity(), bestSell.getLastData().quantity());
var newBuyOrderData = new OrderData(
bestBuy.getLastData().price() - matchQuantity * bestBuy.getLastData().rate(),
bestBuy.getLastData().quantity() - matchQuantity
);
var newSellOrderData = new OrderData(
bestSell.getLastData().price() - matchQuantity * bestSell.getLastData().rate(),
bestSell.getLastData().quantity() - matchQuantity
);
bestBuy.pushMessage(new OrderFilledMessage(bestSell.getId(), newBuyOrderData));
bestSell.pushMessage(new OrderFilledMessage(bestBuy.getId(), newSellOrderData));
if (bestBuy.getLastData().quantity() <= 0) {
bestBuyEntry.getValue().remove(bestBuy);
bestBuy.pushMessage(new OrderClosedMessage(OrderClosedMessage.Reason.FULFILLED));
if (bestBuyEntry.getValue().isEmpty())
buyOrders.remove(bestBuyEntry.getKey());
}
if (bestSell.getLastData().quantity() <= 0) {
bestSellEntry.getValue().remove(bestSell);
bestSell.pushMessage(new OrderClosedMessage(OrderClosedMessage.Reason.FULFILLED));
if (bestSellEntry.getValue().isEmpty())
sellOrders.remove(bestSellEntry.getKey());
}
}
} finally {
lock.writeLock().unlock();
}
}
}
}

View File

@@ -5,15 +5,12 @@ 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.*;
@@ -31,8 +28,8 @@ class ConcurrentCurrencyExchangeTest {
RUB_CNY = new CurrencyPair(RUB, CNY);
exchange = new PlainCurrencyExchange(
Arrays.asList(RUB, CNY),
Arrays.asList(RUB_CNY)
Set.of(RUB, CNY),
Set.of(RUB_CNY)
);
executorService = Executors.newFixedThreadPool(10);
@@ -63,132 +60,6 @@ class ConcurrentCurrencyExchangeTest {
}
@Test
void testConcurrentDeposits() throws InterruptedException {
int numThreads = 10;
int depositsPerThread = 100;
double depositAmount = 10.0;
var latch = new CountDownLatch(numThreads);
var client = exchange.createClient("Test Client").join();
var futures = new ArrayList<CompletableFuture<Void>>();
for (int i = 0; i < numThreads; i++) {
var future = CompletableFuture.runAsync(() -> {
try {
for (int j = 0; j < depositsPerThread; j++) {
exchange.deposit(client.id(), RUB, depositAmount).join();
}
} finally {
latch.countDown();
}
}, executorService);
futures.add(future);
}
latch.await(30, TimeUnit.SECONDS);
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
var balances = exchange.getBalances(client.id()).join();
assertEquals(numThreads * depositsPerThread * depositAmount, balances.get(RUB));
}
@Test
void testConcurrentWithdrawals() throws InterruptedException {
var numThreads = 5;
var latch = new CountDownLatch(numThreads);
var initialBalance = 1000.0;
var withdrawalAmount = 10.0;
var client = exchange.createClient("Test Client").join();
exchange.deposit(client.id(), RUB, initialBalance).join();
var futures = new ArrayList<CompletableFuture<Void>>();
var successfulWithdrawals = new AtomicInteger(0);
for (int i = 0; i < numThreads; i++) {
var future = CompletableFuture.runAsync(() -> {
try {
while (true) {
try {
exchange.withdraw(client.id(), RUB, withdrawalAmount).join();
successfulWithdrawals.incrementAndGet();
} catch (CompletionException e) {
break;
}
}
} finally {
latch.countDown();
}
}, executorService);
futures.add(future);
}
latch.await(30, TimeUnit.SECONDS);
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
var finalBalance = exchange.getBalances(client.id()).join();
assertEquals(
initialBalance,
successfulWithdrawals.get() * withdrawalAmount + finalBalance.get(RUB),
0.001
);
}
@Test
void testConcurrentDepositsAndWithdrawals() throws InterruptedException {
var numThreads = 10;
var initialBalance = 1000.0;
var transactionAmount = 10.0;
var latch = new CountDownLatch(numThreads * 2);
var client = exchange.createClient("Test Client").join();
exchange.deposit(client.id(), RUB, initialBalance).join();
var futures = new ArrayList<CompletableFuture<Void>>();
var successfulWithdrawals = new AtomicInteger(0);
for (int i = 0; i < numThreads; i++) {
futures.add(CompletableFuture.runAsync(() -> {
try {
for (int j = 0; j < 100; j++) {
exchange.deposit(client.id(), RUB, transactionAmount).join();
}
} finally {
latch.countDown();
}
}, executorService));
}
for (int i = 0; i < numThreads; i++) {
futures.add(CompletableFuture.runAsync(() -> {
try {
while (true) {
try {
exchange.withdraw(client.id(), RUB, transactionAmount).join();
successfulWithdrawals.incrementAndGet();
} catch (CompletionException e) {
break;
}
}
} finally {
latch.countDown();
}
}, executorService));
}
latch.await(30, TimeUnit.SECONDS);
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
var finalBalances = exchange.getBalances(client.id()).join();
assertEquals(
initialBalance + (numThreads * 100 * transactionAmount) - (successfulWithdrawals.get() * transactionAmount),
finalBalances.get(RUB),
0.001
);
}
@Test
void testConcurrentOrderPlacement() throws InterruptedException {
var numSellers = 10;
@@ -199,14 +70,12 @@ class ConcurrentCurrencyExchangeTest {
var sellers = new ArrayList<Client>();
for (int i = 0; i < numSellers; i++) {
var seller = exchange.createClient("Seller " + i).join();
exchange.deposit(seller.id(), CNY, 1000000.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(), RUB, 1200000.0).join();
buyers.add(buyer);
}
@@ -247,15 +116,11 @@ class ConcurrentCurrencyExchangeTest {
).join();
for (var seller : sellers) {
assertTrue(StreamSupport.stream(
exchange.getActiveOrders(seller.id()).join().spliterator(), false
).count() <= ordersPerClient);
assertTrue((long) exchange.getOrders(seller.id()).join().size() <= ordersPerClient);
}
for (var buyer : buyers) {
assertTrue(StreamSupport.stream(
exchange.getActiveOrders(buyer.id()).join().spliterator(), false
).count() <= ordersPerClient);
assertTrue((long) exchange.getOrders(buyer.id()).join().size() <= ordersPerClient);
}
}
@@ -267,9 +132,6 @@ class ConcurrentCurrencyExchangeTest {
var seller = exchange.createClient("Seller").join();
var buyer = exchange.createClient("Buyer").join();
exchange.deposit(seller.id(), CNY, 1000.0).join();
exchange.deposit(buyer.id(), RUB, 2000.0).join();
var sellerFuture = CompletableFuture.runAsync(() -> {
try {
for (int i = 0; i < numOrders; i++) {
@@ -301,57 +163,19 @@ class ConcurrentCurrencyExchangeTest {
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 sellerOrders = exchange.getOrders(seller.id()).join();
var buyerOrders = exchange.getOrders(buyer.id()).join();
var remainingSellerOrders = StreamSupport.stream(sellerOrders.spliterator(), false).count();
var remainingBuyerOrders = StreamSupport.stream(buyerOrders.spliterator(), false).count();
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 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(), RUB, initialBalance).join();
var successfulChecks = new AtomicInteger(0);
var futures = new ArrayList<CompletableFuture<Void>>();
for (int i = 0; i < numThreads; i++) {
var future = CompletableFuture.runAsync(() -> {
try {
startLatch.await();
var balance = exchange.getBalances(client.id()).join();
if (Math.abs(balance.get(RUB) - initialBalance) < 0.001) {
successfulChecks.incrementAndGet();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
completionLatch.countDown();
}
}, executorService);
futures.add(future);
}
startLatch.countDown();
completionLatch.await(30, TimeUnit.SECONDS);
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
assertEquals(numThreads, successfulChecks.get(),
"All concurrent balance checks should return the correct balance");
}
@Test
void testBalanceConsistencyWithConcurrentTrades() throws InterruptedException {
var numTraders = 10;
void testBalanceConsistencyWithConcurrentTradesSamePrices() throws InterruptedException {
var numTraders = 100;
var numOrders = 1000;
var latch = new CountDownLatch(numTraders * 2);
@@ -364,9 +188,6 @@ class ConcurrentCurrencyExchangeTest {
var seller = exchange.createClient("Seller " + i).join();
var buyer = exchange.createClient("Buyer " + i).join();
exchange.deposit(seller.id(), CNY, sellAmount * numOrders).join();
exchange.deposit(buyer.id(), RUB, buyAmount * numOrders).join();
sellers.add(seller);
buyers.add(buyer);
}
@@ -376,8 +197,7 @@ class ConcurrentCurrencyExchangeTest {
for (var seller : sellers) {
var future = CompletableFuture.runAsync(() -> {
try {
for (var i = 0; i < numOrders; i++)
{
for (var i = 0; i < numOrders; i++) {
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, buyAmount, sellAmount).join();
}
} finally {
@@ -390,8 +210,7 @@ class ConcurrentCurrencyExchangeTest {
for (var buyer : buyers) {
var future = CompletableFuture.runAsync(() -> {
try {
for (var i = 0; i < numOrders; i++)
{
for (var i = 0; i < numOrders; i++) {
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, sellAmount, buyAmount).join();
}
} finally {
@@ -407,18 +226,97 @@ class ConcurrentCurrencyExchangeTest {
var totalCnyMoney = 0.0;
for (var seller : sellers) {
var balances = exchange.getBalances(seller.id()).join();
totalCnyMoney += balances.get(CNY);
totalRubMoney += balances.get(RUB);
}
for (var buyer : buyers) {
var balances = exchange.getBalances(buyer.id()).join();
totalCnyMoney += balances.get(CNY);
totalRubMoney += balances.get(RUB);
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(totalRubMoney, sellAmount * numOrders * numTraders, 0.001);
assertEquals(totalCnyMoney, sellAmount * numOrders * numTraders, 0.001);
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();
}
assertEquals(0.0, totalCnyMoney, 0.001);
}
@AfterEach

View File

@@ -3,11 +3,16 @@ 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.OrderData;
import ru.lionarius.api.order.OrderType;
import ru.lionarius.api.order.message.OrderClosedMessage;
import ru.lionarius.api.order.message.OrderFilledMessage;
import ru.lionarius.api.order.message.OrderMessageType;
import ru.lionarius.impl.PlainCurrencyExchange;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
@@ -26,8 +31,8 @@ class CurrencyExchangeTest {
RUB_CNY = new CurrencyPair(RUB, CNY);
exchange = new PlainCurrencyExchange(
Arrays.asList(RUB, CNY),
Arrays.asList(RUB_CNY)
Set.of(RUB, CNY),
Set.of(RUB_CNY)
);
}
@@ -36,8 +41,6 @@ class CurrencyExchangeTest {
var client = exchange.createClient("Trader").get();
assertNotNull(client);
assertNotNull(client.id());
assertEquals("Trader", client.name());
}
@Test
@@ -48,97 +51,22 @@ class CurrencyExchangeTest {
@Test
void testGetBalancesNonexistentClient() {
var nonexistentId = UUID.randomUUID();
assertThrows(Exception.class, () -> exchange.getBalances(nonexistentId).get());
assertThrows(Exception.class, () -> exchange.getOrders(nonexistentId).get());
}
@Test
void testDeposit() throws ExecutionException, InterruptedException {
var client = exchange.createClient("Trader").get();
exchange.deposit(client.id(), RUB, 1000.0).get();
var balances = exchange.getBalances(client.id()).get();
assertEquals(1000.0, balances.get(RUB));
}
@Test
void testDepositNegativeAmount() throws ExecutionException, InterruptedException {
var client = exchange.createClient("Trader").get();
assertThrows(ExecutionException.class, () -> exchange.deposit(client.id(), RUB, -100.0).get());
}
@Test
void testDepositWithNullCurrency() throws ExecutionException, InterruptedException {
var client = exchange.createClient("Trader").get();
assertThrows(ExecutionException.class, () -> exchange.deposit(client.id(), null, 100.0).get());
}
@Test
void testWithdraw() throws ExecutionException, InterruptedException {
var client = exchange.createClient("Trader").get();
exchange.deposit(client.id(), RUB, 1000.0).get();
exchange.withdraw(client.id(), RUB, 500.0).get();
var balances = exchange.getBalances(client.id()).get();
assertEquals(500.0, balances.get(RUB));
}
@Test
void testWithdrawInsufficientFunds() throws ExecutionException, InterruptedException {
var client = exchange.createClient("Trader").get();
exchange.deposit(client.id(), RUB, 100.0).get();
assertThrows(ExecutionException.class, () -> exchange.withdraw(client.id(), RUB, 200.0).get());
}
@Test
void testSequentialDepositWithdraw() throws ExecutionException, InterruptedException {
var client = exchange.createClient("Trader").get();
exchange.deposit(client.id(), RUB, 500.0).get();
var balancesAfterDeposit = exchange.getBalances(client.id()).get();
assertEquals(500.0, balancesAfterDeposit.get(RUB));
exchange.withdraw(client.id(), RUB, 200.0).get();
var balancesAfterWithdraw = exchange.getBalances(client.id()).get();
assertEquals(300.0, balancesAfterWithdraw.get(RUB));
assertThrows(ExecutionException.class, () -> exchange.withdraw(client.id(), RUB, 400.0).get());
}
@Test
void testPlaceOrder() throws ExecutionException, InterruptedException {
var client = exchange.createClient("Trader").get();
exchange.deposit(client.id(), CNY, 1000.0).get();
exchange.placeOrder(client.id(), RUB_CNY, OrderType.SELL, 120, 100.0).get();
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(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get()
);
exchange.getOrders(client.id()).get().getFirst();
}
@Test
void testOrderPriceValidation() throws ExecutionException, InterruptedException {
var client = exchange.createClient("Trader").get();
exchange.deposit(client.id(), CNY, 100.0).get();
assertThrows(ExecutionException.class, () ->
exchange.placeOrder(client.id(), RUB_CNY, OrderType.SELL, -1.0, 100.0).get()
);
@@ -148,8 +76,6 @@ class CurrencyExchangeTest {
void testOrderQuantityValidation() throws ExecutionException, InterruptedException {
var client = exchange.createClient("Trader").get();
exchange.deposit(client.id(), CNY, 100.0).get();
assertThrows(ExecutionException.class, () ->
exchange.placeOrder(client.id(), RUB_CNY, OrderType.SELL, 120.0, -100.0).get()
);
@@ -161,19 +87,16 @@ class CurrencyExchangeTest {
var buyer = exchange.createClient("Buyer").get();
var seller = exchange.createClient("Seller").get();
exchange.deposit(buyer.id(), RUB, 120.0).get();
exchange.deposit(seller.id(), CNY, 100.0).get();
var sellOrderId = exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
var buyOrderId = exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 120.0, 100.0).get();
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 120.0, 100.0).get();
var sellOrderMessages = exchange.getOrderMessages(seller.id(), sellOrderId).get();
var buyOrderMessages = exchange.getOrderMessages(buyer.id(), buyOrderId).get();
Map<Currency, Double> buyerBalances = exchange.getBalances(buyer.id()).get();
Map<Currency, Double> sellerBalances = exchange.getBalances(seller.id()).get();
assertEquals(100.0, buyerBalances.get(CNY));
assertEquals(0.0, buyerBalances.get(RUB));
assertEquals(120.0, sellerBalances.get(RUB));
assertEquals(0.0, sellerBalances.get(CNY));
assertEquals(3, sellOrderMessages.size());
assertSame(sellOrderMessages.getLast().type(), OrderMessageType.CLOSED);
assertEquals(3, buyOrderMessages.size());
assertSame(buyOrderMessages.getLast().type(), OrderMessageType.CLOSED);
}
@Test
@@ -181,25 +104,17 @@ class CurrencyExchangeTest {
var buyer = exchange.createClient("Buyer").get();
var seller = exchange.createClient("Seller").get();
exchange.deposit(buyer.id(), RUB, 120.0).get();
exchange.deposit(seller.id(), CNY, 100.0).get();
var sellOrderId = exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
var buyOrderId = exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 60.0, 50.0).get();
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 60.0, 50.0).get();
var buyOrderMessages = exchange.getOrderMessages(buyer.id(), buyOrderId).get();
var sellOrderMessages = exchange.getOrderMessages(seller.id(), sellOrderId).get();
var buyerBalances = exchange.getBalances(buyer.id()).get();
var sellerBalances = exchange.getBalances(seller.id()).get();
assertEquals(50.0, buyerBalances.get(CNY));
assertEquals(60.0, sellerBalances.get(RUB));
var sellerOrders = exchange.getActiveOrders(seller.id()).get();
var remainingOrder = sellerOrders.iterator().next();
assertEquals(RUB_CNY, remainingOrder.pair());
assertEquals(50.0, remainingOrder.quantity());
assertEquals(60.0, remainingOrder.price());
assertEquals(2, sellOrderMessages.size());
assertSame(sellOrderMessages.getLast().type(), OrderMessageType.FILLED);
assertEquals(new OrderData(60.0, 50.0), ((OrderFilledMessage) sellOrderMessages.getLast()).newData());
assertEquals(3, buyOrderMessages.size());
assertSame(buyOrderMessages.getLast().type(), OrderMessageType.CLOSED);
}
@Test
@@ -208,100 +123,22 @@ class CurrencyExchangeTest {
var buyer2 = exchange.createClient("Buyer2").get();
var seller = exchange.createClient("Seller").get();
exchange.deposit(buyer1.id(), RUB, 120.0).get();
exchange.deposit(buyer2.id(), RUB, 120.0).get();
exchange.deposit(seller.id(), CNY, 100.0).get();
var sellOrderId = exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
var buyOrderId1 = exchange.placeOrder(buyer1.id(), RUB_CNY, OrderType.BUY, 60.0, 50.0).get();
var buyOrderId2 = exchange.placeOrder(buyer2.id(), RUB_CNY, OrderType.BUY, 60.0, 50.0).get();
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
exchange.placeOrder(buyer1.id(), RUB_CNY, OrderType.BUY, 60.0, 50.0).get();
exchange.placeOrder(buyer2.id(), RUB_CNY, OrderType.BUY, 60.0, 50.0).get();
var sellOrderMessages = exchange.getOrderMessages(seller.id(), sellOrderId).get();
var buyOrder1Messages = exchange.getOrderMessages(buyer1.id(), buyOrderId1).get();
var buyOrder2Messages = exchange.getOrderMessages(buyer2.id(), buyOrderId2).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(CNY));
assertEquals(50.0, buyer2Balances.get(CNY));
assertEquals(120.0, sellerBalances.get(RUB));
var sellerOrders = exchange.getActiveOrders(seller.id()).get();
assertFalse(sellerOrders.iterator().hasNext());
assertEquals(4, sellOrderMessages.size());
assertSame(sellOrderMessages.getLast().type(), OrderMessageType.CLOSED);
assertEquals(3, buyOrder1Messages.size());
assertSame(buyOrder1Messages.getLast().type(), OrderMessageType.CLOSED);
assertEquals(3, buyOrder2Messages.size());
assertSame(buyOrder2Messages.getLast().type(), OrderMessageType.CLOSED);
}
@Test
void testSequentialOrderPlacementAndMatching() throws ExecutionException, InterruptedException {
var buyer = exchange.createClient("Buyer").get();
var seller = exchange.createClient("Seller").get();
exchange.deposit(buyer.id(), RUB, 300.0).get();
exchange.deposit(seller.id(), CNY, 200.0).get();
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 120.0, 100.0).get();
var buyerBalances = exchange.getBalances(buyer.id()).get();
var sellerBalances = exchange.getBalances(seller.id()).get();
assertEquals(100.0, buyerBalances.get(CNY));
assertEquals(180.0, buyerBalances.get(RUB));
assertEquals(120.0, sellerBalances.get(RUB));
assertEquals(100.0, sellerBalances.get(CNY));
}
@Test
void testSequentialMultipleClientsWithPartialMatches() throws ExecutionException, InterruptedException {
var buyer1 = exchange.createClient("Buyer1").get();
var buyer2 = exchange.createClient("Buyer2").get();
var seller = exchange.createClient("Seller").get();
exchange.deposit(buyer1.id(), RUB, 60.0).get();
exchange.deposit(buyer2.id(), RUB, 60.0).get();
exchange.deposit(seller.id(), CNY, 100.0).get();
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
exchange.placeOrder(buyer1.id(), RUB_CNY, OrderType.BUY, 36.0, 30.0).get();
exchange.placeOrder(buyer2.id(), RUB_CNY, OrderType.BUY, 36.0, 30.0).get();
var buyer1Balances = exchange.getBalances(buyer1.id()).get();
var buyer2Balances = exchange.getBalances(buyer2.id()).get();
var sellerBalances = exchange.getBalances(seller.id()).get();
assertEquals(30.0, buyer1Balances.get(CNY));
assertEquals(24.0, buyer1Balances.get(RUB));
assertEquals(30.0, buyer2Balances.get(CNY));
assertEquals(24.0, buyer2Balances.get(RUB));
var sellerOrders = exchange.getActiveOrders(seller.id()).get();
var sellerOrder = sellerOrders.iterator().next();
assertEquals(40.0, sellerOrder.quantity());
assertEquals(48.0, sellerOrder.price());
assertEquals(72.0, sellerBalances.get(RUB));
}
@Test
void testSequentialScenarioWithInsufficientFunds() throws ExecutionException, InterruptedException {
var buyer = exchange.createClient("Buyer").get();
var seller = exchange.createClient("Seller").get();
exchange.deposit(buyer.id(), RUB, 100.0).get();
exchange.deposit(seller.id(), CNY, 50.0).get();
assertThrows(ExecutionException.class, () ->
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get()
);
assertThrows(ExecutionException.class, () ->
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 240.0, 200.0).get()
);
var buyerOrders = exchange.getActiveOrders(buyer.id()).get();
var sellerOrders = exchange.getActiveOrders(seller.id()).get();
assertFalse(buyerOrders.iterator().hasNext());
assertFalse(sellerOrders.iterator().hasNext());
}
@Test
void testPricePriorityInOrderMatching() throws ExecutionException, InterruptedException {
@@ -309,27 +146,21 @@ class CurrencyExchangeTest {
var seller1 = exchange.createClient("Seller1").get();
var seller2 = exchange.createClient("Seller2").get();
exchange.deposit(buyer.id(), RUB, 200.0).get();
exchange.deposit(seller1.id(), CNY, 50.0).get();
exchange.deposit(seller2.id(), CNY, 50.0).get();
var sellOrderId1 = exchange.placeOrder(seller1.id(), RUB_CNY, OrderType.SELL, 75, 50.0).get();
var sellOrderId2 = exchange.placeOrder(seller2.id(), RUB_CNY, OrderType.SELL, 60, 50.0).get();
exchange.placeOrder(seller1.id(), RUB_CNY, OrderType.SELL, 75, 50.0).get();
exchange.placeOrder(seller2.id(), RUB_CNY, OrderType.SELL, 60, 50.0).get();
var buyOrderId = exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 75, 50.0).get();
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 75, 50.0).get();
var buyOrderMessages = exchange.getOrderMessages(buyer.id(), buyOrderId).get();
var sellOrder1Messages = exchange.getOrderMessages(seller1.id(), sellOrderId1).get();
var sellOrder2Messages = exchange.getOrderMessages(seller2.id(), sellOrderId2).get();
var buyerBalances = exchange.getBalances(buyer.id()).get();
var seller1Balances = exchange.getBalances(seller1.id()).get();
var seller2Balances = exchange.getBalances(seller2.id()).get();
assertEquals(50.0, buyerBalances.get(CNY));
assertEquals(140.0, buyerBalances.get(RUB));
assertEquals(60.0, seller2Balances.get(RUB));
assertEquals(0.0, seller2Balances.get(CNY));
assertNull(seller1Balances.get(RUB));
assertEquals(0.0, seller1Balances.get(CNY));
assertEquals(1, sellOrder1Messages.size());
assertSame(sellOrder1Messages.getLast().type(), OrderMessageType.CREATED);
assertEquals(3, sellOrder2Messages.size());
assertSame(sellOrder2Messages.getLast().type(), OrderMessageType.CLOSED);
assertEquals(3, buyOrderMessages.size());
assertSame(buyOrderMessages.getLast().type(), OrderMessageType.CLOSED);
}
@Test
@@ -339,173 +170,59 @@ class CurrencyExchangeTest {
var seller2 = exchange.createClient("Seller2").get();
var seller3 = exchange.createClient("Seller3").get();
exchange.deposit(buyer.id(), RUB, 300.0).get();
exchange.deposit(seller1.id(), CNY, 50.0).get();
exchange.deposit(seller2.id(), CNY, 50.0).get();
exchange.deposit(seller3.id(), CNY, 50.0).get();
var sellOrderId1 = exchange.placeOrder(seller1.id(), RUB_CNY, OrderType.SELL, 75.0, 50.0).get();
var sellOrderId2 = exchange.placeOrder(seller2.id(), RUB_CNY, OrderType.SELL, 65.0, 50.0).get();
var sellOrderId3 = exchange.placeOrder(seller3.id(), RUB_CNY, OrderType.SELL, 60.0, 50.0).get();
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();
exchange.placeOrder(seller3.id(), RUB_CNY, OrderType.SELL, 60.0, 50.0).get();
var buyOrderId = 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();
var buyOrderMessages = exchange.getOrderMessages(buyer.id(), buyOrderId).get();
var sellOrder1Messages = exchange.getOrderMessages(seller1.id(), sellOrderId1).get();
var sellOrder2Messages = exchange.getOrderMessages(seller2.id(), sellOrderId2).get();
var sellOrder3Messages = exchange.getOrderMessages(seller3.id(), sellOrderId3).get();
var buyerBalances = exchange.getBalances(buyer.id()).get();
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(CNY));
assertEquals(175.0, buyerBalances.get(RUB));
assertEquals(60.0, seller3Balances.get(RUB));
assertEquals(0.0, seller3Balances.get(CNY));
assertEquals(65.0, seller2Balances.get(RUB));
assertEquals(0.0, seller2Balances.get(CNY));
assertNull(seller1Balances.get(RUB));
assertEquals(0.0, seller1Balances.get(CNY));
}
@Test
void testTimePriorityWithEqualPrices() throws ExecutionException, InterruptedException {
var buyer = exchange.createClient("Buyer").get();
var seller1 = exchange.createClient("Seller1").get();
var seller2 = exchange.createClient("Seller2").get();
exchange.deposit(buyer.id(), RUB, 200.0).get();
exchange.deposit(seller1.id(), CNY, 50.0).get();
exchange.deposit(seller2.id(), CNY, 50.0).get();
exchange.placeOrder(seller1.id(), RUB_CNY, OrderType.SELL, 60, 50.0).get();
exchange.placeOrder(seller2.id(), RUB_CNY, OrderType.SELL, 60, 50.0).get();
exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 60, 50.0).get();
var buyerBalances = exchange.getBalances(buyer.id()).get();
var seller1Balances = exchange.getBalances(seller1.id()).get();
var seller2Balances = exchange.getBalances(seller2.id()).get();
assertEquals(50.0, buyerBalances.get(CNY));
assertEquals(140.0, buyerBalances.get(RUB));
assertEquals(60.0, seller1Balances.get(RUB));
assertEquals(0.0, seller1Balances.get(CNY));
assertNull(seller2Balances.get(RUB));
assertEquals(1, sellOrder1Messages.size());
assertSame(sellOrder1Messages.getLast().type(), OrderMessageType.CREATED);
assertEquals(3, sellOrder2Messages.size());
assertSame(sellOrder2Messages.getLast().type(), OrderMessageType.CLOSED);
assertEquals(3, sellOrder3Messages.size());
assertSame(sellOrder3Messages.getLast().type(), OrderMessageType.CLOSED);
assertEquals(4, buyOrderMessages.size());
assertSame(buyOrderMessages.getLast().type(), OrderMessageType.CLOSED);
}
@Test
void testPriceOrderNotMatching() throws ExecutionException, InterruptedException {
var buyer1 = exchange.createClient("Buyer1").get();
var buyer2 = exchange.createClient("Buyer2").get();
var seller = exchange.createClient("Seller2").get();
var seller = exchange.createClient("Seller").get();
exchange.deposit(buyer1.id(), RUB, 200.0).get();
exchange.deposit(buyer2.id(), RUB, 200.0).get();
exchange.deposit(seller.id(), CNY, 50.0).get();
var buyOrderId1 = exchange.placeOrder(buyer1.id(), RUB_CNY, OrderType.BUY, 80.0, 50.0).get();
var buyOrderId2 = exchange.placeOrder(buyer2.id(), RUB_CNY, OrderType.BUY, 75.0, 50.0).get();
exchange.placeOrder(buyer1.id(), RUB_CNY, OrderType.BUY, 80.0, 50.0).get();
exchange.placeOrder(buyer2.id(), RUB_CNY, OrderType.BUY, 75.0, 50.0).get();
var sellOrderId = exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 85, 50.0).get();
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 85, 50.0).get();
var buyOrderMessages = exchange.getOrderMessages(buyer1.id(), buyOrderId1).get();
var buyOrderMessages2 = exchange.getOrderMessages(buyer2.id(), buyOrderId2).get();
var sellOrderMessages = exchange.getOrderMessages(seller.id(), sellOrderId).get();
var buyer1Balances = exchange.getBalances(buyer1.id()).get();
var buyer2Balances = exchange.getBalances(buyer2.id()).get();
var sellerBalances = exchange.getBalances(seller.id()).get();
assertNull(sellerBalances.get(RUB));
assertEquals(0.0, sellerBalances.get(CNY));
assertNull(buyer1Balances.get(CNY));
assertEquals(120.0, buyer1Balances.get(RUB));
assertNull(buyer2Balances.get(CNY));
assertEquals(125.0, buyer2Balances.get(RUB));
assertEquals(1, sellOrderMessages.size());
assertEquals(1, buyOrderMessages.size());
assertEquals(1, buyOrderMessages2.size());
}
@Test
public void testCancelBuyOrder() throws ExecutionException, InterruptedException {
var client = exchange.createClient("Client1").get();
exchange.deposit(client.id(), RUB, 100.0);
var order = exchange.placeOrder(client.id(), RUB_CNY, OrderType.BUY, 100.0, 200.0).get().orElseThrow();
var balances = exchange.getBalances(client.id()).get();
assertEquals(0.0, balances.get(RUB));
exchange.cancelOrder(client.id(), order.id()).get();
balances = exchange.getBalances(client.id()).get();
assertEquals(100.0, balances.get(RUB));
var orders = exchange.getActiveOrders(client.id()).get();
assertFalse(orders.iterator().hasNext());
}
@Test
public void testCancelSellOrder() throws ExecutionException, InterruptedException {
var client = exchange.createClient("Client1").get();
exchange.deposit(client.id(), CNY, 200.0);
var order = exchange.placeOrder(client.id(), RUB_CNY, OrderType.SELL, 100.0, 200.0).get().orElseThrow();
var balances = exchange.getBalances(client.id()).get();
assertEquals(0.0, balances.get(CNY));
exchange.cancelOrder(client.id(), order.id()).get();
balances = exchange.getBalances(client.id()).get();
assertEquals(200.0, balances.get(CNY));
var orders = exchange.getActiveOrders(client.id()).get();
assertFalse(orders.iterator().hasNext());
}
@Test
public void testCancelPartialOrder() throws ExecutionException, InterruptedException {
var buyer = exchange.createClient("Buyer").get();
var seller = exchange.createClient("Seller").get();
exchange.deposit(buyer.id(), RUB, 1000.0).get();
exchange.deposit(seller.id(), CNY, 500.0).get();
exchange.placeOrder(seller.id(), RUB_CNY, OrderType.SELL, 120.0, 100.0).get();
var order = exchange.placeOrder(buyer.id(), RUB_CNY, OrderType.BUY, 200.0, 120.0).get().orElseThrow();
var buyerOrders = exchange.getActiveOrders(buyer.id()).get();
var sellerOrders = exchange.getActiveOrders(seller.id()).get();
assertTrue(buyerOrders.iterator().hasNext());
assertFalse(sellerOrders.iterator().hasNext());
exchange.cancelOrder(buyer.id(), order.id());
var buyerBalances = exchange.getBalances(buyer.id()).get();
assertEquals(100.0, buyerBalances.get(CNY), 0.001);
assertEquals(880.0, buyerBalances.get(RUB), 0.001);
}
@Test
public void testSameClientSellBuy() throws ExecutionException, InterruptedException {
var client = exchange.createClient("Trader").get();
exchange.deposit(client.id(), RUB, 1000.0).get();
exchange.deposit(client.id(), CNY, 1000.0).get();
var orderId = exchange.placeOrder(client.id(), RUB_CNY, OrderType.BUY, 100.0, 200.0).get();
exchange.placeOrder(client.id(), RUB_CNY, OrderType.BUY, 100.0, 100.0).get();
exchange.placeOrder(client.id(), RUB_CNY, OrderType.SELL, 90.0, 100.0).get();
exchange.cancelOrder(client.id(), orderId).get();
var balances = exchange.getBalances(client.id()).get();
assertEquals(900.0, balances.get(RUB), 0.001);
assertEquals(900.0, balances.get(CNY), 0.001);
var orderMessages = exchange.getOrderMessages(client.id(), orderId).get();
var orders = exchange.getActiveOrders(client.id()).get();
assertNotNull(orders.iterator().next());
assertNotNull(orders.iterator().next());
assertEquals(2, orderMessages.size());
assertSame(orderMessages.getLast().type(), OrderMessageType.CLOSED);
assertSame(((OrderClosedMessage) orderMessages.getLast()).reason(), OrderClosedMessage.Reason.CANCELLED);
}
}