lab10
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
package ru.lionarius.api.command;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public record CancelOrderCommand(UUID clientId, UUID orderId, CompletableFuture<Void> result) implements Command<Void> {
|
||||||
|
|
||||||
|
public CancelOrderCommand(UUID clientId, UUID orderId) {
|
||||||
|
this(clientId, orderId, new CompletableFuture<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Void> result() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/main/java/ru/lionarius/api/command/Command.java
Normal file
7
src/main/java/ru/lionarius/api/command/Command.java
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package ru.lionarius.api.command;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public interface Command<T> {
|
||||||
|
CompletableFuture<T> result();
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package ru.lionarius.api.command;
|
||||||
|
|
||||||
|
import ru.lionarius.api.client.Client;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public record CreateClientCommand(String name, CompletableFuture<Client> result) implements Command<Client> {
|
||||||
|
|
||||||
|
public CreateClientCommand(String name) {
|
||||||
|
this(name, new CompletableFuture<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Client> result() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package ru.lionarius.api.command;
|
||||||
|
|
||||||
|
import ru.lionarius.api.order.message.OrderMessage;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public record GetOrderMessagesCommand(UUID clientId, UUID orderId,
|
||||||
|
CompletableFuture<List<OrderMessage>> result) implements Command<List<OrderMessage>> {
|
||||||
|
|
||||||
|
public GetOrderMessagesCommand(UUID clientId, UUID orderId) {
|
||||||
|
this(clientId, orderId, new CompletableFuture<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<List<OrderMessage>> result() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/main/java/ru/lionarius/api/command/GetOrdersCommand.java
Normal file
20
src/main/java/ru/lionarius/api/command/GetOrdersCommand.java
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package ru.lionarius.api.command;
|
||||||
|
|
||||||
|
import ru.lionarius.api.order.OrderView;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public record GetOrdersCommand(UUID clientId,
|
||||||
|
CompletableFuture<List<OrderView>> result) implements Command<List<OrderView>> {
|
||||||
|
|
||||||
|
public GetOrdersCommand(UUID clientId) {
|
||||||
|
this(clientId, new CompletableFuture<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<List<OrderView>> result() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package ru.lionarius.api.command;
|
||||||
|
|
||||||
|
import ru.lionarius.api.currency.CurrencyPair;
|
||||||
|
import ru.lionarius.api.order.OrderType;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public record PlaceOrderCommand(UUID clientId, CurrencyPair pair, OrderType type, double price, double quantity,
|
||||||
|
CompletableFuture<UUID> result) implements Command<UUID> {
|
||||||
|
|
||||||
|
public PlaceOrderCommand(UUID clientId, CurrencyPair pair, OrderType type, double price, double quantity) {
|
||||||
|
this(clientId, pair, type, price, quantity, new CompletableFuture<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<UUID> result() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ public record OrderClosedMessage(Reason reason) implements OrderMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum Reason {
|
public enum Reason {
|
||||||
CANCELLED,
|
FULFILLED,
|
||||||
FULFILLED
|
CANCELLED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ package ru.lionarius.api.order.message;
|
|||||||
|
|
||||||
public enum OrderMessageType {
|
public enum OrderMessageType {
|
||||||
CREATED,
|
CREATED,
|
||||||
CLOSED,
|
FILLED,
|
||||||
FILLED
|
CLOSED
|
||||||
}
|
}
|
||||||
|
|||||||
96
src/main/java/ru/lionarius/impl/OrderBook.java
Normal file
96
src/main/java/ru/lionarius/impl/OrderBook.java
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package ru.lionarius.impl;
|
||||||
|
|
||||||
|
import ru.lionarius.api.order.Order;
|
||||||
|
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 java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.concurrent.locks.ReadWriteLock;
|
||||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
|
||||||
|
public 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package ru.lionarius.impl;
|
package ru.lionarius.impl.plain;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
@@ -12,23 +12,20 @@ import ru.lionarius.api.order.OrderData;
|
|||||||
import ru.lionarius.api.order.OrderType;
|
import ru.lionarius.api.order.OrderType;
|
||||||
import ru.lionarius.api.order.OrderView;
|
import ru.lionarius.api.order.OrderView;
|
||||||
import ru.lionarius.api.order.message.OrderClosedMessage;
|
import ru.lionarius.api.order.message.OrderClosedMessage;
|
||||||
import ru.lionarius.api.order.message.OrderFilledMessage;
|
|
||||||
import ru.lionarius.api.order.message.OrderMessage;
|
import ru.lionarius.api.order.message.OrderMessage;
|
||||||
|
import ru.lionarius.impl.InMemoryClientRepository;
|
||||||
|
import ru.lionarius.impl.OrderBook;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.locks.ReadWriteLock;
|
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
|
||||||
|
|
||||||
public class PlainCurrencyExchange implements CurrencyExchange {
|
public class PlainCurrencyExchange implements CurrencyExchange {
|
||||||
private final ClientRepository clientRepository = new InMemoryClientRepository();
|
private final ClientRepository clientRepository = new InMemoryClientRepository();
|
||||||
private final Map<CurrencyPair, OrderBook> orderBooks;
|
private final Map<CurrencyPair, OrderBook> orderBooks;
|
||||||
|
|
||||||
private final Set<Currency> allowedCurrencies;
|
|
||||||
private final Set<CurrencyPair> allowedPairs;
|
private final Set<CurrencyPair> allowedPairs;
|
||||||
|
|
||||||
public PlainCurrencyExchange(Set<Currency> allowedCurrencies, Set<CurrencyPair> allowedPairs) {
|
public PlainCurrencyExchange(Set<CurrencyPair> allowedPairs) {
|
||||||
this.allowedCurrencies = ImmutableSet.copyOf(allowedCurrencies);
|
|
||||||
this.allowedPairs = ImmutableSet.copyOf(allowedPairs);
|
this.allowedPairs = ImmutableSet.copyOf(allowedPairs);
|
||||||
|
|
||||||
this.orderBooks = allowedPairs.stream()
|
this.orderBooks = allowedPairs.stream()
|
||||||
@@ -64,6 +61,9 @@ public class PlainCurrencyExchange implements CurrencyExchange {
|
|||||||
if (quantity <= 0.0)
|
if (quantity <= 0.0)
|
||||||
throw new IllegalArgumentException("Quantity must be positive");
|
throw new IllegalArgumentException("Quantity must be positive");
|
||||||
|
|
||||||
|
if (!allowedPairs.contains(pair))
|
||||||
|
throw new IllegalArgumentException("Pair is not allowed");
|
||||||
|
|
||||||
var orders = clientRepository.getClientOrders(clientId).orElseThrow();
|
var orders = clientRepository.getClientOrders(clientId).orElseThrow();
|
||||||
|
|
||||||
var order = new Order(clientId, type, pair, new OrderData(price, quantity));
|
var order = new Order(clientId, type, pair, new OrderData(price, quantity));
|
||||||
@@ -121,86 +121,4 @@ public class PlainCurrencyExchange implements CurrencyExchange {
|
|||||||
return orders.get(orderId).map(Order::getMessages).orElseThrow();
|
return orders.get(orderId).map(Order::getMessages).orElseThrow();
|
||||||
}, Runnable::run);
|
}, 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
154
src/main/java/ru/lionarius/impl/queue/CommandProcessor.java
Normal file
154
src/main/java/ru/lionarius/impl/queue/CommandProcessor.java
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package ru.lionarius.impl.queue;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import ru.lionarius.api.client.ClientRepository;
|
||||||
|
import ru.lionarius.api.command.*;
|
||||||
|
import ru.lionarius.api.currency.CurrencyPair;
|
||||||
|
import ru.lionarius.api.order.Order;
|
||||||
|
import ru.lionarius.api.order.OrderData;
|
||||||
|
import ru.lionarius.api.order.message.OrderClosedMessage;
|
||||||
|
import ru.lionarius.impl.InMemoryClientRepository;
|
||||||
|
import ru.lionarius.impl.OrderBook;
|
||||||
|
|
||||||
|
import java.util.AbstractMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class CommandProcessor implements Runnable {
|
||||||
|
private final BlockingQueue<Command<?>> commandQueue;
|
||||||
|
private final ExecutorService executorService;
|
||||||
|
|
||||||
|
private final ClientRepository clientRepository = new InMemoryClientRepository();
|
||||||
|
private final Map<CurrencyPair, OrderBook> orderBooks;
|
||||||
|
private final Set<CurrencyPair> allowedPairs;
|
||||||
|
|
||||||
|
public CommandProcessor(Set<CurrencyPair> allowedPairs, BlockingQueue<Command<?>> commandQueue) {
|
||||||
|
this.commandQueue = commandQueue;
|
||||||
|
this.executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
|
||||||
|
|
||||||
|
this.allowedPairs = ImmutableSet.copyOf(allowedPairs);
|
||||||
|
this.orderBooks = allowedPairs.stream()
|
||||||
|
.map(pair -> new AbstractMap.SimpleEntry<>(pair, new OrderBook()))
|
||||||
|
.collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
var command = commandQueue.take();
|
||||||
|
|
||||||
|
executorService.submit(() -> processCommand(command));
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() throws InterruptedException {
|
||||||
|
executorService.shutdown();
|
||||||
|
executorService.awaitTermination(5, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processCommand(Command<?> command) {
|
||||||
|
try {
|
||||||
|
if (command instanceof CreateClientCommand) {
|
||||||
|
processCreateClientCommand((CreateClientCommand) command);
|
||||||
|
} else if (command instanceof PlaceOrderCommand) {
|
||||||
|
processPlaceOrderCommand((PlaceOrderCommand) command);
|
||||||
|
} else if (command instanceof CancelOrderCommand) {
|
||||||
|
processCancelOrderCommand((CancelOrderCommand) command);
|
||||||
|
} else if (command instanceof GetOrdersCommand) {
|
||||||
|
processGetOrdersCommand((GetOrdersCommand) command);
|
||||||
|
} else if (command instanceof GetOrderMessagesCommand) {
|
||||||
|
processGetOrderMessagesCommand((GetOrderMessagesCommand) command);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
command.result().completeExceptionally(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processCreateClientCommand(CreateClientCommand command) {
|
||||||
|
if (command.name() == null)
|
||||||
|
throw new IllegalArgumentException("Name cannot be null");
|
||||||
|
|
||||||
|
command.result().complete(clientRepository.createClient(command.name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processPlaceOrderCommand(PlaceOrderCommand command) {
|
||||||
|
if (command.clientId() == null)
|
||||||
|
throw new IllegalArgumentException("Client ID cannot be null");
|
||||||
|
|
||||||
|
if (command.pair() == null)
|
||||||
|
throw new IllegalArgumentException("Currency pair cannot be null");
|
||||||
|
|
||||||
|
if (command.type() == null)
|
||||||
|
throw new IllegalArgumentException("Order type cannot be null");
|
||||||
|
|
||||||
|
if (command.price() <= 0.0)
|
||||||
|
throw new IllegalArgumentException("Price must be positive");
|
||||||
|
|
||||||
|
if (command.quantity() <= 0.0)
|
||||||
|
throw new IllegalArgumentException("Quantity must be positive");
|
||||||
|
|
||||||
|
if (!allowedPairs.contains(command.pair()))
|
||||||
|
throw new IllegalArgumentException("Pair is not allowed");
|
||||||
|
|
||||||
|
var orders = clientRepository.getClientOrders(command.clientId()).orElseThrow();
|
||||||
|
|
||||||
|
var order = new Order(command.clientId(), command.type(), command.pair(), new OrderData(command.price(), command.quantity()));
|
||||||
|
orders.add(order);
|
||||||
|
|
||||||
|
var orderBook = orderBooks.get(command.pair());
|
||||||
|
|
||||||
|
orderBook.addOrder(order);
|
||||||
|
orderBook.matchOrders();
|
||||||
|
|
||||||
|
command.result().complete(order.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processCancelOrderCommand(CancelOrderCommand command) {
|
||||||
|
if (command.clientId() == null)
|
||||||
|
throw new IllegalArgumentException("Client ID cannot be null");
|
||||||
|
|
||||||
|
if (command.orderId() == null)
|
||||||
|
throw new IllegalArgumentException("Order ID cannot be null");
|
||||||
|
|
||||||
|
var orders = clientRepository.getClientOrders(command.clientId()).orElseThrow();
|
||||||
|
|
||||||
|
var order = orders.get(command.orderId()).orElseThrow();
|
||||||
|
order.pushMessage(new OrderClosedMessage(OrderClosedMessage.Reason.CANCELLED));
|
||||||
|
|
||||||
|
command.result().complete(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processGetOrdersCommand(GetOrdersCommand command) {
|
||||||
|
if (command.clientId() == null)
|
||||||
|
throw new IllegalArgumentException("Client ID cannot be null");
|
||||||
|
|
||||||
|
var orders = clientRepository.getClientOrders(command.clientId()).orElseThrow();
|
||||||
|
|
||||||
|
var result = orders.get().stream().map(Order::getView).toList();
|
||||||
|
|
||||||
|
command.result().complete(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processGetOrderMessagesCommand(GetOrderMessagesCommand command) {
|
||||||
|
if (command.clientId() == null)
|
||||||
|
throw new IllegalArgumentException("Client ID cannot be null");
|
||||||
|
|
||||||
|
if (command.orderId() == null)
|
||||||
|
throw new IllegalArgumentException("Order ID cannot be null");
|
||||||
|
|
||||||
|
var orders = clientRepository.getClientOrders(command.clientId()).orElseThrow();
|
||||||
|
|
||||||
|
var result = orders.get(command.orderId()).map(Order::getMessages).orElseThrow();
|
||||||
|
|
||||||
|
command.result().complete(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package ru.lionarius.impl.queue;
|
||||||
|
|
||||||
|
import ru.lionarius.api.CurrencyExchange;
|
||||||
|
import ru.lionarius.api.client.Client;
|
||||||
|
import ru.lionarius.api.command.*;
|
||||||
|
import ru.lionarius.api.currency.CurrencyPair;
|
||||||
|
import ru.lionarius.api.order.OrderType;
|
||||||
|
import ru.lionarius.api.order.OrderView;
|
||||||
|
import ru.lionarius.api.order.message.OrderMessage;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
|
public class QueueCurrencyExchange implements CurrencyExchange {
|
||||||
|
private final BlockingQueue<Command<?>> commandQueue;
|
||||||
|
private final CommandProcessor commandProcessor;
|
||||||
|
private final Thread commandProcessorThread;
|
||||||
|
|
||||||
|
public QueueCurrencyExchange(Set<CurrencyPair> allowedPairs) {
|
||||||
|
this.commandQueue = new LinkedBlockingQueue<>();
|
||||||
|
this.commandProcessor = new CommandProcessor(allowedPairs, commandQueue);
|
||||||
|
|
||||||
|
this.commandProcessorThread = new Thread(commandProcessor);
|
||||||
|
this.commandProcessorThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Client> createClient(String name) {
|
||||||
|
var command = new CreateClientCommand(name);
|
||||||
|
commandQueue.add(command);
|
||||||
|
|
||||||
|
return command.result();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<UUID> placeOrder(UUID clientId, CurrencyPair pair, OrderType type, double price, double quantity) {
|
||||||
|
var command = new PlaceOrderCommand(clientId, pair, type, price, quantity);
|
||||||
|
commandQueue.add(command);
|
||||||
|
|
||||||
|
return command.result();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Void> cancelOrder(UUID clientId, UUID orderId) {
|
||||||
|
var command = new CancelOrderCommand(clientId, orderId);
|
||||||
|
commandQueue.add(command);
|
||||||
|
|
||||||
|
return command.result();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<List<OrderView>> getOrders(UUID clientId) {
|
||||||
|
var command = new GetOrdersCommand(clientId);
|
||||||
|
commandQueue.add(command);
|
||||||
|
|
||||||
|
return command.result();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<List<OrderMessage>> getOrderMessages(UUID clientId, UUID orderId) {
|
||||||
|
var command = new GetOrderMessagesCommand(clientId, orderId);
|
||||||
|
commandQueue.add(command);
|
||||||
|
|
||||||
|
return command.result();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() throws InterruptedException {
|
||||||
|
this.commandProcessorThread.interrupt();
|
||||||
|
this.commandProcessorThread.join();
|
||||||
|
this.commandProcessor.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import ru.lionarius.api.client.Client;
|
|||||||
import ru.lionarius.api.currency.Currency;
|
import ru.lionarius.api.currency.Currency;
|
||||||
import ru.lionarius.api.currency.CurrencyPair;
|
import ru.lionarius.api.currency.CurrencyPair;
|
||||||
import ru.lionarius.api.order.OrderType;
|
import ru.lionarius.api.order.OrderType;
|
||||||
import ru.lionarius.impl.PlainCurrencyExchange;
|
import ru.lionarius.impl.plain.PlainCurrencyExchange;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
@@ -16,19 +16,16 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
|
|
||||||
class ConcurrentCurrencyExchangeTest {
|
class ConcurrentCurrencyExchangeTest {
|
||||||
private CurrencyExchange exchange;
|
private CurrencyExchange exchange;
|
||||||
private Currency RUB;
|
|
||||||
private Currency CNY;
|
|
||||||
private CurrencyPair RUB_CNY;
|
private CurrencyPair RUB_CNY;
|
||||||
private ExecutorService executorService;
|
private ExecutorService executorService;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
RUB = new Currency("RUB");
|
Currency RUB = new Currency("RUB");
|
||||||
CNY = new Currency("CNY");
|
Currency CNY = new Currency("CNY");
|
||||||
RUB_CNY = new CurrencyPair(RUB, CNY);
|
RUB_CNY = new CurrencyPair(RUB, CNY);
|
||||||
|
|
||||||
exchange = new PlainCurrencyExchange(
|
exchange = new PlainCurrencyExchange(
|
||||||
Set.of(RUB, CNY),
|
|
||||||
Set.of(RUB_CNY)
|
Set.of(RUB_CNY)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
334
src/test/java/ConcurrentQueueCurrencyExchangeTest.java
Normal file
334
src/test/java/ConcurrentQueueCurrencyExchangeTest.java
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
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 ru.lionarius.impl.queue.QueueCurrencyExchange;
|
||||||
|
|
||||||
|
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.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class ConcurrentQueueCurrencyExchangeTest {
|
||||||
|
private QueueCurrencyExchange 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 = new QueueCurrencyExchange(
|
||||||
|
Set.of(RUB_CNY)
|
||||||
|
);
|
||||||
|
|
||||||
|
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 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(0.0, totalCnyMoney, 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
executorService.shutdown();
|
||||||
|
try {
|
||||||
|
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
|
executorService.shutdownNow();
|
||||||
|
}
|
||||||
|
exchange.shutdown();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
executorService.shutdownNow();
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,10 +8,8 @@ import ru.lionarius.api.order.OrderType;
|
|||||||
import ru.lionarius.api.order.message.OrderClosedMessage;
|
import ru.lionarius.api.order.message.OrderClosedMessage;
|
||||||
import ru.lionarius.api.order.message.OrderFilledMessage;
|
import ru.lionarius.api.order.message.OrderFilledMessage;
|
||||||
import ru.lionarius.api.order.message.OrderMessageType;
|
import ru.lionarius.api.order.message.OrderMessageType;
|
||||||
import ru.lionarius.impl.PlainCurrencyExchange;
|
import ru.lionarius.impl.plain.PlainCurrencyExchange;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
@@ -20,18 +18,15 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
|
|
||||||
class CurrencyExchangeTest {
|
class CurrencyExchangeTest {
|
||||||
private CurrencyExchange exchange;
|
private CurrencyExchange exchange;
|
||||||
private Currency RUB;
|
|
||||||
private Currency CNY;
|
|
||||||
private CurrencyPair RUB_CNY;
|
private CurrencyPair RUB_CNY;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
RUB = new Currency("RUB");
|
Currency RUB = new Currency("RUB");
|
||||||
CNY = new Currency("CNY");
|
Currency CNY = new Currency("CNY");
|
||||||
RUB_CNY = new CurrencyPair(RUB, CNY);
|
RUB_CNY = new CurrencyPair(RUB, CNY);
|
||||||
|
|
||||||
exchange = new PlainCurrencyExchange(
|
exchange = new PlainCurrencyExchange(
|
||||||
Set.of(RUB, CNY),
|
|
||||||
Set.of(RUB_CNY)
|
Set.of(RUB_CNY)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
232
src/test/java/CurrencyQueueExchangeTest.java
Normal file
232
src/test/java/CurrencyQueueExchangeTest.java
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
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.queue.QueueCurrencyExchange;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class CurrencyQueueExchangeTest {
|
||||||
|
private QueueCurrencyExchange exchange;
|
||||||
|
private CurrencyPair RUB_CNY;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
Currency RUB = new Currency("RUB");
|
||||||
|
Currency CNY = new Currency("CNY");
|
||||||
|
RUB_CNY = new CurrencyPair(RUB, CNY);
|
||||||
|
|
||||||
|
exchange = new QueueCurrencyExchange(
|
||||||
|
Set.of(RUB_CNY)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateClient() throws ExecutionException, InterruptedException {
|
||||||
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
|
assertNotNull(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateClientWithNullName() {
|
||||||
|
assertThrows(ExecutionException.class, () -> exchange.createClient(null).get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetBalancesNonexistentClient() {
|
||||||
|
var nonexistentId = UUID.randomUUID();
|
||||||
|
assertThrows(Exception.class, () -> exchange.getOrders(nonexistentId).get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPlaceOrder() throws ExecutionException, InterruptedException {
|
||||||
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
|
exchange.placeOrder(client.id(), RUB_CNY, OrderType.SELL, 120, 100.0).get();
|
||||||
|
|
||||||
|
exchange.getOrders(client.id()).get().getFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testOrderPriceValidation() throws ExecutionException, InterruptedException {
|
||||||
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
|
assertThrows(ExecutionException.class, () ->
|
||||||
|
exchange.placeOrder(client.id(), RUB_CNY, OrderType.SELL, -1.0, 100.0).get()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testOrderQuantityValidation() 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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMatchingOrders() throws ExecutionException, InterruptedException {
|
||||||
|
var buyer = exchange.createClient("Buyer").get();
|
||||||
|
var seller = exchange.createClient("Seller").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();
|
||||||
|
|
||||||
|
var sellOrderMessages = exchange.getOrderMessages(seller.id(), sellOrderId).get();
|
||||||
|
var buyOrderMessages = exchange.getOrderMessages(buyer.id(), buyOrderId).get();
|
||||||
|
|
||||||
|
assertEquals(3, sellOrderMessages.size());
|
||||||
|
assertSame(sellOrderMessages.getLast().type(), OrderMessageType.CLOSED);
|
||||||
|
assertEquals(3, buyOrderMessages.size());
|
||||||
|
assertSame(buyOrderMessages.getLast().type(), OrderMessageType.CLOSED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPartialOrderMatching() throws ExecutionException, InterruptedException {
|
||||||
|
var buyer = exchange.createClient("Buyer").get();
|
||||||
|
var seller = exchange.createClient("Seller").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();
|
||||||
|
|
||||||
|
var buyOrderMessages = exchange.getOrderMessages(buyer.id(), buyOrderId).get();
|
||||||
|
var sellOrderMessages = exchange.getOrderMessages(seller.id(), sellOrderId).get();
|
||||||
|
|
||||||
|
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
|
||||||
|
void testPartialOrderMatchingMultiple() throws ExecutionException, InterruptedException {
|
||||||
|
var buyer1 = exchange.createClient("Buyer1").get();
|
||||||
|
var buyer2 = exchange.createClient("Buyer2").get();
|
||||||
|
var seller = exchange.createClient("Seller").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();
|
||||||
|
|
||||||
|
var sellOrderMessages = exchange.getOrderMessages(seller.id(), sellOrderId).get();
|
||||||
|
var buyOrder1Messages = exchange.getOrderMessages(buyer1.id(), buyOrderId1).get();
|
||||||
|
var buyOrder2Messages = exchange.getOrderMessages(buyer2.id(), buyOrderId2).get();
|
||||||
|
|
||||||
|
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 testPricePriorityInOrderMatching() throws ExecutionException, InterruptedException {
|
||||||
|
var buyer = exchange.createClient("Buyer").get();
|
||||||
|
var seller1 = exchange.createClient("Seller1").get();
|
||||||
|
var seller2 = exchange.createClient("Seller2").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();
|
||||||
|
|
||||||
|
var buyOrderId = 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();
|
||||||
|
|
||||||
|
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
|
||||||
|
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();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
var buyOrderId = 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();
|
||||||
|
|
||||||
|
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("Seller").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();
|
||||||
|
|
||||||
|
var sellOrderId = 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();
|
||||||
|
|
||||||
|
assertEquals(1, sellOrderMessages.size());
|
||||||
|
assertEquals(1, buyOrderMessages.size());
|
||||||
|
assertEquals(1, buyOrderMessages2.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCancelBuyOrder() throws ExecutionException, InterruptedException {
|
||||||
|
var client = exchange.createClient("Trader").get();
|
||||||
|
|
||||||
|
var orderId = exchange.placeOrder(client.id(), RUB_CNY, OrderType.BUY, 100.0, 200.0).get();
|
||||||
|
|
||||||
|
exchange.cancelOrder(client.id(), orderId).get();
|
||||||
|
|
||||||
|
var orderMessages = exchange.getOrderMessages(client.id(), orderId).get();
|
||||||
|
|
||||||
|
assertEquals(2, orderMessages.size());
|
||||||
|
assertSame(orderMessages.getLast().type(), OrderMessageType.CLOSED);
|
||||||
|
assertSame(((OrderClosedMessage) orderMessages.getLast()).reason(), OrderClosedMessage.Reason.CANCELLED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
try {
|
||||||
|
exchange.shutdown();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
106
src/test/java/PerformanceTest.java
Normal file
106
src/test/java/PerformanceTest.java
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
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 ru.lionarius.impl.plain.PlainCurrencyExchange;
|
||||||
|
import ru.lionarius.impl.queue.QueueCurrencyExchange;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
|
public class PerformanceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void compareQueuePlainPerformance() throws Exception {
|
||||||
|
var BTC = new Currency("BTC");
|
||||||
|
var ETH = new Currency("ETH");
|
||||||
|
var USD = new Currency("USD");
|
||||||
|
var RUB = new Currency("RUB");
|
||||||
|
|
||||||
|
var pairs = Set.of(
|
||||||
|
new CurrencyPair(BTC, USD),
|
||||||
|
new CurrencyPair(ETH, USD),
|
||||||
|
new CurrencyPair(USD, BTC),
|
||||||
|
new CurrencyPair(USD, ETH),
|
||||||
|
new CurrencyPair(RUB, USD),
|
||||||
|
new CurrencyPair(USD, RUB),
|
||||||
|
new CurrencyPair(BTC, RUB),
|
||||||
|
new CurrencyPair(ETH, RUB),
|
||||||
|
new CurrencyPair(RUB, BTC),
|
||||||
|
new CurrencyPair(RUB, ETH)
|
||||||
|
);
|
||||||
|
|
||||||
|
var plainExchange = new PlainCurrencyExchange(pairs);
|
||||||
|
var queueExchange = new QueueCurrencyExchange(pairs);
|
||||||
|
|
||||||
|
var numTraders = 500;
|
||||||
|
var numOrders = 1000;
|
||||||
|
|
||||||
|
var plainTime = benchmark(numTraders, numOrders, plainExchange, pairs);
|
||||||
|
var queueTime = benchmark(numTraders, numOrders, queueExchange, pairs);
|
||||||
|
|
||||||
|
queueExchange.shutdown();
|
||||||
|
|
||||||
|
System.out.printf("Direct implementation: %.2f orders/sec%n", plainTime);
|
||||||
|
System.out.printf("Queue implementation: %.2f orders/sec%n", queueTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
double benchmark(int numTraders, int numOrders, CurrencyExchange exchange, Set<CurrencyPair> pairs) throws InterruptedException {
|
||||||
|
var buyers = new ArrayList<Client>();
|
||||||
|
var sellers = new ArrayList<Client>();
|
||||||
|
|
||||||
|
initTest(numTraders, buyers, sellers, exchange);
|
||||||
|
|
||||||
|
var start = System.nanoTime();
|
||||||
|
var orders = stressTest(buyers, sellers, numOrders, exchange, List.copyOf(pairs));
|
||||||
|
|
||||||
|
return orders * 1_000_000_000.0 / (System.nanoTime() - start);
|
||||||
|
}
|
||||||
|
|
||||||
|
void initTest(int numTraders, List<Client> buyers, List<Client> sellers, CurrencyExchange exchange) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
long stressTest(List<Client> buyers, List<Client> sellers, int numOrders, CurrencyExchange exchange, List<CurrencyPair> pairs) throws InterruptedException {
|
||||||
|
long orders = 0;
|
||||||
|
|
||||||
|
var futures = new ArrayList<CompletableFuture<?>>();
|
||||||
|
|
||||||
|
var random = new Random();
|
||||||
|
|
||||||
|
for (var seller : sellers) {
|
||||||
|
for (var i = 0; i < numOrders; i++) {
|
||||||
|
var randomPair = pairs.get(random.nextInt(pairs.size()));
|
||||||
|
var buyAmount = random.nextInt(100) + 1;
|
||||||
|
var sellAmount = random.nextInt(100) + 1;
|
||||||
|
futures.add(exchange.placeOrder(seller.id(), randomPair, OrderType.SELL, buyAmount, sellAmount));
|
||||||
|
orders += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var buyer : buyers) {
|
||||||
|
for (var i = 0; i < numOrders; i++) {
|
||||||
|
var randomPair = pairs.get(random.nextInt(pairs.size()));
|
||||||
|
var buyAmount = random.nextInt(100) + 1;
|
||||||
|
var sellAmount = random.nextInt(100) + 1;
|
||||||
|
futures.add(exchange.placeOrder(buyer.id(), randomPair, OrderType.BUY, sellAmount, buyAmount));
|
||||||
|
orders += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||||
|
|
||||||
|
return orders;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user