From 0247f3f5068b96df52a4f9424578d4c66ca6975c Mon Sep 17 00:00:00 2001 From: Kostya Date: Sat, 15 Aug 2026 16:25:40 +0000 Subject: Implement LMSR and order-book trading logic, wire MarketContext facade and new menu commands Fill in the previously stubbed cost/pricing math for both trading mechanisms, and give OrderBookTradingMechanism an actual matching engine with resting bid/ask books. Fold EventRepository/UserRepository access into a single MarketContext used by LocalGuessMarketContext, and move the repository package under infrastructure. Add Buy/EventDetails/LoadState/ SaveState/SettleEvent console commands and wire them into App/Menu. --- .../guess/service/LocalGuessMarketContext.java | 23 ++-- .../java/market/guess/service/domain/Account.java | 5 +- .../guess/service/domain/BigDecimalOptions.java | 8 +- .../java/market/guess/service/domain/Event.java | 16 +-- .../market/guess/service/domain/LedgerEntry.java | 2 +- .../java/market/guess/service/domain/Option.java | 20 +-- .../service/infrastructure/MarketContext.java | 49 ++++++++ .../infrastructure/mapper/v1/EventMapperV1.java | 6 +- .../infrastructure/mapper/v2/EventMapperV2.java | 13 +- .../infrastructure/provider/v1/XMLLoaderV1.java | 4 +- .../infrastructure/repository/EventRepository.java | 5 + .../repository/InMemoryEventRepository.java | 27 ++++ .../repository/InMemoryUserRepository.java | 27 ++++ .../infrastructure/repository/Repository.java | 12 ++ .../infrastructure/repository/UserRepository.java | 5 + .../service/mechanism/LmsrTradingMechanism.java | 55 ++++++-- .../mechanism/OrderBookTradingMechanism.java | 139 +++++++++++++++++++-- .../guess/service/mechanism/TradeExecution.java | 5 + .../guess/service/mechanism/TradingMechanism.java | 10 +- .../guess/service/repository/EventRepository.java | 5 - .../repository/InMemoryEventRepository.java | 27 ---- .../service/repository/InMemoryUserRepository.java | 27 ---- .../guess/service/repository/Repository.java | 12 -- .../guess/service/repository/UserRepository.java | 5 - .../mechanism/LmsrTradingMechanismTest.java | 37 ++++++ .../mechanism/OrderBookTradingMechanismTest.java | 51 ++++++++ 26 files changed, 429 insertions(+), 166 deletions(-) create mode 100644 service/src/main/java/market/guess/service/infrastructure/MarketContext.java create mode 100644 service/src/main/java/market/guess/service/infrastructure/repository/EventRepository.java create mode 100644 service/src/main/java/market/guess/service/infrastructure/repository/InMemoryEventRepository.java create mode 100644 service/src/main/java/market/guess/service/infrastructure/repository/InMemoryUserRepository.java create mode 100644 service/src/main/java/market/guess/service/infrastructure/repository/Repository.java create mode 100644 service/src/main/java/market/guess/service/infrastructure/repository/UserRepository.java create mode 100644 service/src/main/java/market/guess/service/mechanism/TradeExecution.java delete mode 100644 service/src/main/java/market/guess/service/repository/EventRepository.java delete mode 100644 service/src/main/java/market/guess/service/repository/InMemoryEventRepository.java delete mode 100644 service/src/main/java/market/guess/service/repository/InMemoryUserRepository.java delete mode 100644 service/src/main/java/market/guess/service/repository/Repository.java delete mode 100644 service/src/main/java/market/guess/service/repository/UserRepository.java create mode 100644 service/src/test/java/market/guess/service/mechanism/LmsrTradingMechanismTest.java create mode 100644 service/src/test/java/market/guess/service/mechanism/OrderBookTradingMechanismTest.java (limited to 'service') diff --git a/service/src/main/java/market/guess/service/LocalGuessMarketContext.java b/service/src/main/java/market/guess/service/LocalGuessMarketContext.java index 19c7297..b405329 100644 --- a/service/src/main/java/market/guess/service/LocalGuessMarketContext.java +++ b/service/src/main/java/market/guess/service/LocalGuessMarketContext.java @@ -10,27 +10,20 @@ import market.guess.api.LoadResultDTO; import market.guess.api.PurchaseReceiptDTO; import market.guess.exception.GuessMarketException; import market.guess.service.domain.Event; +import market.guess.service.infrastructure.MarketContext; import market.guess.service.infrastructure.provider.Loader; -import market.guess.service.repository.EventRepository; -import market.guess.service.repository.UserRepository; public final class LocalGuessMarketContext implements GuessMarketContext { - private final EventRepository eventRepository; - private final UserRepository userRepository; private final TradingOperation tradingOperation; - + private final MarketContext context; private final Loader provider; public LocalGuessMarketContext( - Loader provider, - EventRepository eventRepository, - UserRepository userRepository, - TradingOperation tradingOperation) { + Loader provider, MarketContext context, TradingOperation tradingOperation) { super(); this.provider = provider; - this.eventRepository = eventRepository; - this.userRepository = userRepository; + this.context = context; this.tradingOperation = tradingOperation; } @@ -41,7 +34,7 @@ public final class LocalGuessMarketContext implements GuessMarketContext { @Override public List listEvents() { - return eventRepository.getAll().stream().map(Event::toEventSummary).toList(); + return context.getEvents().getAll().stream().map(Event::toEventSummary).toList(); } @Override @@ -58,9 +51,9 @@ public final class LocalGuessMarketContext implements GuessMarketContext { public PurchaseReceiptDTO buyShares( String userName, String eventKey, String optionKey, int quantity) throws GuessMarketException { - var optional = eventRepository.get(eventKey); + var optional = context.getEvents().get(eventKey); - var userOptional = userRepository.get(userName); + var userOptional = context.getUsers().get(userName); if (optional.isEmpty() || userOptional.isEmpty()) throw new GuessMarketException(); var event = optional.get(); @@ -83,7 +76,7 @@ public final class LocalGuessMarketContext implements GuessMarketContext { @Override public void saveState(Path path) throws GuessMarketException { - throw new UnsupportedOperationException("Unimplemented method 'saveState'"); + context.save(); } @Override diff --git a/service/src/main/java/market/guess/service/domain/Account.java b/service/src/main/java/market/guess/service/domain/Account.java index d1a1841..48d5054 100644 --- a/service/src/main/java/market/guess/service/domain/Account.java +++ b/service/src/main/java/market/guess/service/domain/Account.java @@ -8,11 +8,12 @@ import market.guess.api.LedgerType; public final class Account { private final String owner; - private final Clock clock; private final List entries = new ArrayList<>(); private BigDecimal balance; private int runningId; + private final Clock clock; + public Account(String owner, BigDecimal initialBalance) { this(owner, initialBalance, Clock.systemUTC()); } @@ -43,6 +44,6 @@ public final class Account { } private void record(LedgerType type, BigDecimal amount, String note) { - entries.add(new LedgerEntry(++runningId, clock.instant(), type, amount, balance, note)); + entries.add(new LedgerEntry(runningId++, clock.instant(), type, amount, balance, note)); } } diff --git a/service/src/main/java/market/guess/service/domain/BigDecimalOptions.java b/service/src/main/java/market/guess/service/domain/BigDecimalOptions.java index 25306c6..992e44f 100644 --- a/service/src/main/java/market/guess/service/domain/BigDecimalOptions.java +++ b/service/src/main/java/market/guess/service/domain/BigDecimalOptions.java @@ -12,7 +12,11 @@ public record BigDecimalOptions(int scale, RoundingMode roundingMode) { return value.setScale(DEFAULT.scale(), DEFAULT.roundingMode()); } - public static BigDecimal toMoney(double v) { - return BigDecimal.valueOf(v).setScale(DEFAULT.scale(), DEFAULT.roundingMode()); + public static BigDecimal toMoney(int value) { + return BigDecimal.valueOf(value).setScale(DEFAULT.scale(), DEFAULT.roundingMode()); + } + + public static BigDecimal toMoney(double value) { + return BigDecimal.valueOf(value).setScale(DEFAULT.scale(), DEFAULT.roundingMode()); } } diff --git a/service/src/main/java/market/guess/service/domain/Event.java b/service/src/main/java/market/guess/service/domain/Event.java index 8f7e11d..ce35e82 100644 --- a/service/src/main/java/market/guess/service/domain/Event.java +++ b/service/src/main/java/market/guess/service/domain/Event.java @@ -84,7 +84,7 @@ public final class Event { } public List getOptionsNames() { - return options.stream().map(Option::getName).toList(); + return options.stream().map(Option::name).toList(); } public Account getAccount() { @@ -123,15 +123,7 @@ public final class Event { BigDecimal cost, BigDecimal commission) { var trade = - new Trade( - ++runningId, - time, - user, - option.getKey(), - option.getName(), - quantity, - cost, - commission); + new Trade(runningId++, time, user, option.key(), option.name(), quantity, cost, commission); trades.add(trade); return trade; @@ -159,7 +151,7 @@ public final class Event { description, commissionPercent, commissionTiming, - mechanism.type(), + mechanism.getType(), status, getOptionsNames(), account.getBalance().toPlainString()); @@ -167,7 +159,7 @@ public final class Event { public Option getOption(String optionKey) { return options.stream() - .filter(option -> option.getKey().equalsIgnoreCase(optionKey)) + .filter(option -> option.key().equalsIgnoreCase(optionKey)) .findFirst() .orElseThrow(() -> new IllegalArgumentException("No such option.")); } diff --git a/service/src/main/java/market/guess/service/domain/LedgerEntry.java b/service/src/main/java/market/guess/service/domain/LedgerEntry.java index 4ca4b92..7e6ff83 100644 --- a/service/src/main/java/market/guess/service/domain/LedgerEntry.java +++ b/service/src/main/java/market/guess/service/domain/LedgerEntry.java @@ -4,7 +4,7 @@ import java.math.BigDecimal; import java.time.Instant; import market.guess.api.LedgerType; -public record LedgerEntry( +public final record LedgerEntry( int id, Instant time, LedgerType type, diff --git a/service/src/main/java/market/guess/service/domain/Option.java b/service/src/main/java/market/guess/service/domain/Option.java index c182850..7f2ff86 100644 --- a/service/src/main/java/market/guess/service/domain/Option.java +++ b/service/src/main/java/market/guess/service/domain/Option.java @@ -1,21 +1,3 @@ package market.guess.service.domain; -public final class Option { - - private String key; - private String name; - - public Option(String key, String name) { - super(); - this.key = key; - this.name = name; - } - - public String getName() { - return name; - } - - public String getKey() { - return key; - } -} +public final record Option(String key, String name) {} diff --git a/service/src/main/java/market/guess/service/infrastructure/MarketContext.java b/service/src/main/java/market/guess/service/infrastructure/MarketContext.java new file mode 100644 index 0000000..e769384 --- /dev/null +++ b/service/src/main/java/market/guess/service/infrastructure/MarketContext.java @@ -0,0 +1,49 @@ +package market.guess.service.infrastructure; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.Instant; +import market.guess.service.infrastructure.adapter.InstantTypeAdapter; +import market.guess.service.infrastructure.repository.EventRepository; +import market.guess.service.infrastructure.repository.UserRepository; + +public final class MarketContext { + private final EventRepository events; + private final UserRepository users; + + private static final Gson GSON = + new GsonBuilder() + .registerTypeAdapter(Instant.class, new InstantTypeAdapter()) + .setPrettyPrinting() + .create(); + + public MarketContext(EventRepository events, UserRepository users) { + this.events = events; + this.users = users; + } + + public EventRepository getEvents() { + return events; + } + + public UserRepository getUsers() { + return users; + } + + public void save() { + var path = Paths.get("test-file.json"); + + try { + if (path.getParent() != null) Files.createDirectories(path.getParent()); + Files.writeString(path, GSON.toJson(events), StandardCharsets.UTF_8); + } catch (IOException e) { + + } + } + + public void load() {} +} diff --git a/service/src/main/java/market/guess/service/infrastructure/mapper/v1/EventMapperV1.java b/service/src/main/java/market/guess/service/infrastructure/mapper/v1/EventMapperV1.java index f61820a..8754555 100644 --- a/service/src/main/java/market/guess/service/infrastructure/mapper/v1/EventMapperV1.java +++ b/service/src/main/java/market/guess/service/infrastructure/mapper/v1/EventMapperV1.java @@ -14,6 +14,8 @@ public final class EventMapperV1 implements Mapper { @Override public Event toDomain(GMEvent source) { + var options = getOptions(source); + return new Event( getEventKey(source), source.getId(), @@ -21,9 +23,9 @@ public final class EventMapperV1 implements Mapper { source.getDescription(), source.getComision().getValue(), getTiming(source.getComision().getType()), - new LmsrTradingMechanism(source.getGMMethod().getGMLMSR().getB()), + new LmsrTradingMechanism(source.getGMMethod().getGMLMSR().getB(), options.size()), EventStatus.ACTIVE, - getOptions(source), + options, ""); } diff --git a/service/src/main/java/market/guess/service/infrastructure/mapper/v2/EventMapperV2.java b/service/src/main/java/market/guess/service/infrastructure/mapper/v2/EventMapperV2.java index 687ec33..a4fc4e7 100644 --- a/service/src/main/java/market/guess/service/infrastructure/mapper/v2/EventMapperV2.java +++ b/service/src/main/java/market/guess/service/infrastructure/mapper/v2/EventMapperV2.java @@ -18,6 +18,8 @@ public final class EventMapperV2 implements Mapper { @Override public Event toDomain(GMEvent source) { + var options = getOptions(source); + return new Event( getEventKey(source), source.getId(), @@ -25,9 +27,9 @@ public final class EventMapperV2 implements Mapper { source.getDescription(), source.getCommission().getValue(), getTiming(source.getCommission().getType()), - getMechanism(source), + getMechanism(source, options.size()), EventStatus.NOT_STARTED, - getOptions(source), + options, ""); } @@ -43,15 +45,16 @@ public final class EventMapperV2 implements Mapper { }; } - private static TradingMechanism getMechanism(GMEvent source) { + private static TradingMechanism getMechanism(GMEvent source, int optionCount) { if (source.getGMMethod().getGMLMSR() instanceof GMLMSR lmsr) { - return new LmsrTradingMechanism(lmsr.getB()); + return new LmsrTradingMechanism(lmsr.getB(), optionCount); } if (source.getGMMethod().getGMOrderBook() instanceof GMOrderBook orderBook) { return new OrderBookTradingMechanism( orderBook.getAllowMint().equalsIgnoreCase("true"), orderBook.getInitial(), - orderBook.getD()); + orderBook.getD(), + optionCount); } throw new IllegalArgumentException(); } diff --git a/service/src/main/java/market/guess/service/infrastructure/provider/v1/XMLLoaderV1.java b/service/src/main/java/market/guess/service/infrastructure/provider/v1/XMLLoaderV1.java index 7af22ea..0d5bb9d 100644 --- a/service/src/main/java/market/guess/service/infrastructure/provider/v1/XMLLoaderV1.java +++ b/service/src/main/java/market/guess/service/infrastructure/provider/v1/XMLLoaderV1.java @@ -9,9 +9,9 @@ import market.guess.service.domain.User; import market.guess.service.infrastructure.mapper.v1.EventMapperV1; import market.guess.service.infrastructure.provider.LoadValidator; import market.guess.service.infrastructure.provider.Loader; +import market.guess.service.infrastructure.repository.EventRepository; +import market.guess.service.infrastructure.repository.UserRepository; import market.guess.service.model.v1.GuessMarket; -import market.guess.service.repository.EventRepository; -import market.guess.service.repository.UserRepository; public final class XMLLoaderV1 implements Loader { private final JAXBContext context; diff --git a/service/src/main/java/market/guess/service/infrastructure/repository/EventRepository.java b/service/src/main/java/market/guess/service/infrastructure/repository/EventRepository.java new file mode 100644 index 0000000..186695b --- /dev/null +++ b/service/src/main/java/market/guess/service/infrastructure/repository/EventRepository.java @@ -0,0 +1,5 @@ +package market.guess.service.infrastructure.repository; + +import market.guess.service.domain.Event; + +public interface EventRepository extends Repository {} diff --git a/service/src/main/java/market/guess/service/infrastructure/repository/InMemoryEventRepository.java b/service/src/main/java/market/guess/service/infrastructure/repository/InMemoryEventRepository.java new file mode 100644 index 0000000..0ed5b20 --- /dev/null +++ b/service/src/main/java/market/guess/service/infrastructure/repository/InMemoryEventRepository.java @@ -0,0 +1,27 @@ +package market.guess.service.infrastructure.repository; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import market.guess.service.domain.Event; + +public final class InMemoryEventRepository implements EventRepository { + private final List events = new ArrayList<>(); + + @Override + public Event add(Event element) { + events.add(element); + return element; + } + + @Override + public Optional get(String id) { + return events.stream().filter(event -> event.getEventKey().equalsIgnoreCase(id)).findFirst(); + } + + @Override + public List getAll() { + return Collections.unmodifiableList(events); + } +} diff --git a/service/src/main/java/market/guess/service/infrastructure/repository/InMemoryUserRepository.java b/service/src/main/java/market/guess/service/infrastructure/repository/InMemoryUserRepository.java new file mode 100644 index 0000000..b4014e5 --- /dev/null +++ b/service/src/main/java/market/guess/service/infrastructure/repository/InMemoryUserRepository.java @@ -0,0 +1,27 @@ +package market.guess.service.infrastructure.repository; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import market.guess.service.domain.User; + +public final class InMemoryUserRepository implements UserRepository { + private final List users = new ArrayList<>(); + + @Override + public User add(User element) { + users.add(element); + return element; + } + + @Override + public Optional get(String id) { + return users.stream().filter(u -> u.getName().equalsIgnoreCase(id)).findFirst(); + } + + @Override + public List getAll() { + return Collections.unmodifiableList(users); + } +} diff --git a/service/src/main/java/market/guess/service/infrastructure/repository/Repository.java b/service/src/main/java/market/guess/service/infrastructure/repository/Repository.java new file mode 100644 index 0000000..13f3901 --- /dev/null +++ b/service/src/main/java/market/guess/service/infrastructure/repository/Repository.java @@ -0,0 +1,12 @@ +package market.guess.service.infrastructure.repository; + +import java.util.List; +import java.util.Optional; + +public interface Repository { + T add(T element); + + Optional get(String id); + + List getAll(); +} diff --git a/service/src/main/java/market/guess/service/infrastructure/repository/UserRepository.java b/service/src/main/java/market/guess/service/infrastructure/repository/UserRepository.java new file mode 100644 index 0000000..4f0b0a6 --- /dev/null +++ b/service/src/main/java/market/guess/service/infrastructure/repository/UserRepository.java @@ -0,0 +1,5 @@ +package market.guess.service.infrastructure.repository; + +import market.guess.service.domain.User; + +public interface UserRepository extends Repository {} diff --git a/service/src/main/java/market/guess/service/mechanism/LmsrTradingMechanism.java b/service/src/main/java/market/guess/service/mechanism/LmsrTradingMechanism.java index 1904a04..cdba3f6 100644 --- a/service/src/main/java/market/guess/service/mechanism/LmsrTradingMechanism.java +++ b/service/src/main/java/market/guess/service/mechanism/LmsrTradingMechanism.java @@ -1,36 +1,67 @@ package market.guess.service.mechanism; import java.math.BigDecimal; +import java.util.Arrays; import market.guess.api.MechanismType; +import market.guess.service.domain.BigDecimalOptions; public final class LmsrTradingMechanism implements TradingMechanism { private final int liquidity; + private final int[] q; - public LmsrTradingMechanism(int liquidity) { - super(); + public LmsrTradingMechanism(int liquidity, int optionCount) { this.liquidity = liquidity; + this.q = new int[optionCount]; } @Override - public MechanismType type() { + public MechanismType getType() { return MechanismType.LMSR; } @Override - public BigDecimal openingCost(int optionCount) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'openingCost'"); + public BigDecimal openingCost() { + return BigDecimalOptions.toMoney(cost(new int[q.length])); } @Override - public BigDecimal costOfBuying(int[] q, int optionIndex, int quantity) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'costOfBuying'"); + public TradeExecution buy(int optionIndex, int quantity) { + var before = cost(q); + + var after = q.clone(); + after[optionIndex] += quantity; + var delta = cost(after) - before; + + q[optionIndex] += quantity; + + return new TradeExecution(quantity, BigDecimalOptions.toMoney(delta)); } @Override - public double[] prices(int[] q) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'prices'"); + public BigDecimal[] prices() { + var max = Arrays.stream(q).mapToDouble(qi -> (double) qi / liquidity).max().orElse(0); + + var exp = new double[q.length]; + var sumExp = 0.0; + for (var i = 0; i < q.length; i++) { + exp[i] = Math.exp((double) q[i] / liquidity - max); + sumExp += exp[i]; + } + + var result = new BigDecimal[q.length]; + for (var i = 0; i < q.length; i++) { + result[i] = BigDecimalOptions.toMoney(exp[i] / sumExp); + } + return result; + } + + private double cost(int[] qs) { + var max = Arrays.stream(qs).mapToDouble(qi -> (double) qi / liquidity).max().orElse(0); + + var sumExp = 0.0; + for (var qi : qs) { + sumExp += Math.exp((double) qi / liquidity - max); + } + return liquidity * (max + Math.log(sumExp)); } } diff --git a/service/src/main/java/market/guess/service/mechanism/OrderBookTradingMechanism.java b/service/src/main/java/market/guess/service/mechanism/OrderBookTradingMechanism.java index 5807e45..91d5110 100644 --- a/service/src/main/java/market/guess/service/mechanism/OrderBookTradingMechanism.java +++ b/service/src/main/java/market/guess/service/mechanism/OrderBookTradingMechanism.java @@ -1,40 +1,153 @@ package market.guess.service.mechanism; import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; import market.guess.api.MechanismType; +import market.guess.service.domain.BigDecimalOptions; public final class OrderBookTradingMechanism implements TradingMechanism { - private final boolean allowMint; + public enum Side { + BUY, + SELL + } + + private static final class RestingOrder { + private final BigDecimal price; + private int quantity; + + private RestingOrder(BigDecimal price, int quantity) { + this.price = price; + this.quantity = quantity; + } + } + + private final boolean allowMint; // ponytail: mint deferred — cross-option matching, its own pass private final int initial; private final int d; + private final List> + asks; // per option, ascending price (best = lowest, index 0) + private final List> + bids; // per option, descending price (best = highest, index 0) + private final BigDecimal[] lastTradePrice; - public OrderBookTradingMechanism(boolean allowMint, int initial, int d) { - super(); + public OrderBookTradingMechanism(boolean allowMint, int initial, int d, int optionCount) { this.allowMint = allowMint; this.initial = initial; this.d = d; + this.asks = newBooks(optionCount); + this.bids = newBooks(optionCount); + this.lastTradePrice = new BigDecimal[optionCount]; + } + + private static List> newBooks(int optionCount) { + var books = new ArrayList>(optionCount); + for (var i = 0; i < optionCount; i++) books.add(new ArrayList<>()); + return books; } @Override - public MechanismType type() { + public MechanismType getType() { return MechanismType.ORDER_BOOK; } @Override - public BigDecimal openingCost(int optionCount) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'openingCost'"); + public BigDecimal openingCost() { + return BigDecimalOptions.toMoney(initial); } @Override - public BigDecimal costOfBuying(int[] q, int optionIndex, int quantity) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'costOfBuying'"); + public TradeExecution buy(int optionIndex, int quantity) { + return match(optionIndex, Side.BUY, quantity, null, false); + } + + /** Priced limit order — not on the shared interface, no engine call site yet. */ + public TradeExecution placeOrder(int optionIndex, Side side, int quantity, BigDecimal price) { + if (price.signum() < 0 || price.compareTo(BigDecimal.valueOf(d)) > 0) { + throw new IllegalArgumentException("Price must be between 0 and " + d); + } + return match(optionIndex, side, quantity, price, true); } @Override - public double[] prices(int[] q) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'prices'"); + public BigDecimal[] prices() { + var result = new BigDecimal[asks.size()]; + for (var i = 0; i < result.length; i++) result[i] = priceFor(i); + return result; + } + + private BigDecimal priceFor(int optionIndex) { + var bestBid = bestPrice(bids.get(optionIndex)); + var bestAsk = bestPrice(asks.get(optionIndex)); + + if (bestBid != null && bestAsk != null) { + return BigDecimalOptions.toMoney( + bestBid.add(bestAsk).divide(BigDecimal.valueOf(2), 10, RoundingMode.HALF_EVEN)); + } + if (bestBid != null) return BigDecimalOptions.toMoney(bestBid); + if (bestAsk != null) return BigDecimalOptions.toMoney(bestAsk); + if (lastTradePrice[optionIndex] != null) return lastTradePrice[optionIndex]; + + // No information yet: neutral 50/50 prior, same as LMSR's day-one price. + return BigDecimalOptions.toMoney(d / 2.0); + } + + private static BigDecimal bestPrice(List book) { + return book.isEmpty() ? null : book.get(0).price; + } + + private TradeExecution match( + int optionIndex, Side side, int quantity, BigDecimal limitPrice, boolean restRemainder) { + var opposing = side == Side.BUY ? asks.get(optionIndex) : bids.get(optionIndex); + + var filled = 0; + var proceeds = BigDecimal.ZERO; + while (filled < quantity + && !opposing.isEmpty() + && crosses(side, opposing.get(0).price, limitPrice)) { + var top = opposing.get(0); + var take = Math.min(quantity - filled, top.quantity); + + filled += take; + proceeds = proceeds.add(top.price.multiply(BigDecimal.valueOf(take))); + top.quantity -= take; + if (top.quantity == 0) opposing.remove(0); + } + + if (filled > 0) { + lastTradePrice[optionIndex] = + BigDecimalOptions.toMoney( + proceeds.divide(BigDecimal.valueOf(filled), 10, RoundingMode.HALF_EVEN)); + } + if (restRemainder && filled < quantity) { + var restingBook = side == Side.BUY ? bids.get(optionIndex) : asks.get(optionIndex); + restingBook.add( + insertionIndex(restingBook, side, limitPrice), + new RestingOrder(limitPrice, quantity - filled)); + } + + return new TradeExecution(filled, BigDecimalOptions.toMoney(proceeds)); + } + + private static boolean crosses(Side side, BigDecimal restingPrice, BigDecimal limitPrice) { + if (limitPrice == null) return true; // market order: any price crosses + return side == Side.BUY + ? restingPrice.compareTo(limitPrice) <= 0 + : restingPrice.compareTo(limitPrice) >= 0; + } + + private static int insertionIndex(List book, Side side, BigDecimal price) { + var i = 0; + while (i < book.size() && betterOrEqual(side, book.get(i).price, price)) i++; + return i; + } + + private static boolean betterOrEqual(Side side, BigDecimal existing, BigDecimal incoming) { + return side == Side.BUY + ? existing.compareTo(incoming) + >= 0 // bids: descending, ties keep earlier order first (FIFO) + : existing.compareTo(incoming) + <= 0; // asks: ascending, ties keep earlier order first (FIFO) } } diff --git a/service/src/main/java/market/guess/service/mechanism/TradeExecution.java b/service/src/main/java/market/guess/service/mechanism/TradeExecution.java new file mode 100644 index 0000000..3d8df1d --- /dev/null +++ b/service/src/main/java/market/guess/service/mechanism/TradeExecution.java @@ -0,0 +1,5 @@ +package market.guess.service.mechanism; + +import java.math.BigDecimal; + +public record TradeExecution(int filledQuantity, BigDecimal cost) {} diff --git a/service/src/main/java/market/guess/service/mechanism/TradingMechanism.java b/service/src/main/java/market/guess/service/mechanism/TradingMechanism.java index c2fc865..fc915fb 100644 --- a/service/src/main/java/market/guess/service/mechanism/TradingMechanism.java +++ b/service/src/main/java/market/guess/service/mechanism/TradingMechanism.java @@ -3,13 +3,13 @@ package market.guess.service.mechanism; import java.math.BigDecimal; import market.guess.api.MechanismType; -public interface TradingMechanism { +public sealed interface TradingMechanism permits LmsrTradingMechanism, OrderBookTradingMechanism { - MechanismType type(); + MechanismType getType(); - BigDecimal openingCost(int optionCount); + BigDecimal openingCost(); - BigDecimal costOfBuying(int[] q, int optionIndex, int quantity); + TradeExecution buy(int optionIndex, int quantity); - double[] prices(int[] q); + BigDecimal[] prices(); } diff --git a/service/src/main/java/market/guess/service/repository/EventRepository.java b/service/src/main/java/market/guess/service/repository/EventRepository.java deleted file mode 100644 index afc5371..0000000 --- a/service/src/main/java/market/guess/service/repository/EventRepository.java +++ /dev/null @@ -1,5 +0,0 @@ -package market.guess.service.repository; - -import market.guess.service.domain.Event; - -public interface EventRepository extends Repository {} diff --git a/service/src/main/java/market/guess/service/repository/InMemoryEventRepository.java b/service/src/main/java/market/guess/service/repository/InMemoryEventRepository.java deleted file mode 100644 index eb7763e..0000000 --- a/service/src/main/java/market/guess/service/repository/InMemoryEventRepository.java +++ /dev/null @@ -1,27 +0,0 @@ -package market.guess.service.repository; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import market.guess.service.domain.Event; - -public final class InMemoryEventRepository implements EventRepository { - private final List events = new ArrayList<>(); - - @Override - public Event add(Event element) { - events.add(element); - return element; - } - - @Override - public Optional get(String id) { - return events.stream().filter(event -> event.getEventKey().equalsIgnoreCase(id)).findFirst(); - } - - @Override - public List getAll() { - return Collections.unmodifiableList(events); - } -} diff --git a/service/src/main/java/market/guess/service/repository/InMemoryUserRepository.java b/service/src/main/java/market/guess/service/repository/InMemoryUserRepository.java deleted file mode 100644 index 0b5cfe7..0000000 --- a/service/src/main/java/market/guess/service/repository/InMemoryUserRepository.java +++ /dev/null @@ -1,27 +0,0 @@ -package market.guess.service.repository; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import market.guess.service.domain.User; - -public final class InMemoryUserRepository implements UserRepository { - private final List users = new ArrayList<>(); - - @Override - public User add(User element) { - users.add(element); - return element; - } - - @Override - public Optional get(String id) { - return users.stream().filter(u -> u.getName().equalsIgnoreCase(id)).findFirst(); - } - - @Override - public List getAll() { - return Collections.unmodifiableList(users); - } -} diff --git a/service/src/main/java/market/guess/service/repository/Repository.java b/service/src/main/java/market/guess/service/repository/Repository.java deleted file mode 100644 index 8ea1817..0000000 --- a/service/src/main/java/market/guess/service/repository/Repository.java +++ /dev/null @@ -1,12 +0,0 @@ -package market.guess.service.repository; - -import java.util.List; -import java.util.Optional; - -public interface Repository { - T add(T element); - - Optional get(String id); - - List getAll(); -} diff --git a/service/src/main/java/market/guess/service/repository/UserRepository.java b/service/src/main/java/market/guess/service/repository/UserRepository.java deleted file mode 100644 index 86cc9a6..0000000 --- a/service/src/main/java/market/guess/service/repository/UserRepository.java +++ /dev/null @@ -1,5 +0,0 @@ -package market.guess.service.repository; - -import market.guess.service.domain.User; - -public interface UserRepository extends Repository {} diff --git a/service/src/test/java/market/guess/service/mechanism/LmsrTradingMechanismTest.java b/service/src/test/java/market/guess/service/mechanism/LmsrTradingMechanismTest.java new file mode 100644 index 0000000..f5c6af5 --- /dev/null +++ b/service/src/test/java/market/guess/service/mechanism/LmsrTradingMechanismTest.java @@ -0,0 +1,37 @@ +package market.guess.service.mechanism; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import java.math.BigDecimal; +import org.junit.Test; + +public class LmsrTradingMechanismTest { + + @Test + public void openingCostMatchesLmsrFormula() { + var mechanism = new LmsrTradingMechanism(100, 2); + + assertEquals(new BigDecimal("69.31"), mechanism.openingCost()); + } + + @Test + public void pricesStartEven() { + var mechanism = new LmsrTradingMechanism(100, 2); + + assertArrayEquals( + new BigDecimal[] {new BigDecimal("0.50"), new BigDecimal("0.50")}, mechanism.prices()); + } + + @Test + public void buyingSharesShiftsCostAndPrice() { + var mechanism = new LmsrTradingMechanism(100, 2); + + var execution = mechanism.buy(0, 100); + + assertEquals(100, execution.filledQuantity()); + assertEquals(new BigDecimal("62.01"), execution.cost()); + assertArrayEquals( + new BigDecimal[] {new BigDecimal("0.73"), new BigDecimal("0.27")}, mechanism.prices()); + } +} diff --git a/service/src/test/java/market/guess/service/mechanism/OrderBookTradingMechanismTest.java b/service/src/test/java/market/guess/service/mechanism/OrderBookTradingMechanismTest.java new file mode 100644 index 0000000..b4b2da3 --- /dev/null +++ b/service/src/test/java/market/guess/service/mechanism/OrderBookTradingMechanismTest.java @@ -0,0 +1,51 @@ +package market.guess.service.mechanism; + +import static org.junit.Assert.assertEquals; + +import java.math.BigDecimal; +import market.guess.service.mechanism.OrderBookTradingMechanism.Side; +import org.junit.Test; + +public class OrderBookTradingMechanismTest { + + @Test + public void limitBuyConsumesRestingAsksInFifoOrderAndLeavesRemainder() { + var mechanism = new OrderBookTradingMechanism(false, 100, 1, 2); + var price = new BigDecimal("0.60"); + + mechanism.placeOrder(0, Side.SELL, 20, price); + mechanism.placeOrder(0, Side.SELL, 40, price); + + var execution = mechanism.placeOrder(0, Side.BUY, 50, price); + + assertEquals(50, execution.filledQuantity()); + assertEquals(new BigDecimal("30.00"), execution.cost()); + + var followUp = mechanism.buy(0, 100); + assertEquals(10, followUp.filledQuantity()); + } + + @Test + public void marketBuySweepsMultiplePriceLevelsAndPartiallyFillsWhenBookRunsOut() { + var mechanism = new OrderBookTradingMechanism(false, 100, 1, 2); + mechanism.placeOrder(0, Side.SELL, 10, new BigDecimal("0.50")); + mechanism.placeOrder(0, Side.SELL, 10, new BigDecimal("0.70")); + + var execution = mechanism.buy(0, 15); + + assertEquals(15, execution.filledQuantity()); + assertEquals(new BigDecimal("8.50"), execution.cost()); + + var exhausted = mechanism.buy(0, 100); + assertEquals(5, exhausted.filledQuantity()); + } + + @Test + public void pricesReflectMidpointOfBestBidAndAsk() { + var mechanism = new OrderBookTradingMechanism(false, 100, 1, 2); + mechanism.placeOrder(0, Side.BUY, 10, new BigDecimal("0.40")); + mechanism.placeOrder(0, Side.SELL, 10, new BigDecimal("0.60")); + + assertEquals(new BigDecimal("0.50"), mechanism.prices()[0]); + } +} -- cgit v1.2.3