diff options
| author | Kostya <mail@sartin.in> | 2026-08-15 16:25:40 +0000 |
|---|---|---|
| committer | Kostya <mail@sartin.in> | 2026-08-15 16:25:40 +0000 |
| commit | 0247f3f5068b96df52a4f9424578d4c66ca6975c (patch) | |
| tree | 0cad643715e5f77920781318ecbb720fa1dec6e6 /service/src/main/java | |
| parent | d77a70c48865592be9029aeb0c83b36c162ea31a (diff) | |
| download | guess-market-0247f3f5068b96df52a4f9424578d4c66ca6975c.tar.gz guess-market-0247f3f5068b96df52a4f9424578d4c66ca6975c.tar.xz guess-market-0247f3f5068b96df52a4f9424578d4c66ca6975c.zip | |
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.
Diffstat (limited to 'service/src/main/java')
19 files changed, 270 insertions, 95 deletions
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; | |||
| 10 | import market.guess.api.PurchaseReceiptDTO; | 10 | import market.guess.api.PurchaseReceiptDTO; |
| 11 | import market.guess.exception.GuessMarketException; | 11 | import market.guess.exception.GuessMarketException; |
| 12 | import market.guess.service.domain.Event; | 12 | import market.guess.service.domain.Event; |
| 13 | import market.guess.service.infrastructure.MarketContext; | ||
| 13 | import market.guess.service.infrastructure.provider.Loader; | 14 | import market.guess.service.infrastructure.provider.Loader; |
| 14 | import market.guess.service.repository.EventRepository; | ||
| 15 | import market.guess.service.repository.UserRepository; | ||
| 16 | 15 | ||
| 17 | public final class LocalGuessMarketContext implements GuessMarketContext { | 16 | public final class LocalGuessMarketContext implements GuessMarketContext { |
| 18 | private final EventRepository eventRepository; | ||
| 19 | private final UserRepository userRepository; | ||
| 20 | 17 | ||
| 21 | private final TradingOperation tradingOperation; | 18 | private final TradingOperation tradingOperation; |
| 22 | 19 | private final MarketContext context; | |
| 23 | private final Loader provider; | 20 | private final Loader provider; |
| 24 | 21 | ||
| 25 | public LocalGuessMarketContext( | 22 | public LocalGuessMarketContext( |
| 26 | Loader provider, | 23 | Loader provider, MarketContext context, TradingOperation tradingOperation) { |
| 27 | EventRepository eventRepository, | ||
| 28 | UserRepository userRepository, | ||
| 29 | TradingOperation tradingOperation) { | ||
| 30 | super(); | 24 | super(); |
| 31 | this.provider = provider; | 25 | this.provider = provider; |
| 32 | this.eventRepository = eventRepository; | 26 | this.context = context; |
| 33 | this.userRepository = userRepository; | ||
| 34 | this.tradingOperation = tradingOperation; | 27 | this.tradingOperation = tradingOperation; |
| 35 | } | 28 | } |
| 36 | 29 | ||
| @@ -41,7 +34,7 @@ public final class LocalGuessMarketContext implements GuessMarketContext { | |||
| 41 | 34 | ||
| 42 | @Override | 35 | @Override |
| 43 | public List<EventSummaryDTO> listEvents() { | 36 | public List<EventSummaryDTO> listEvents() { |
| 44 | return eventRepository.getAll().stream().map(Event::toEventSummary).toList(); | 37 | return context.getEvents().getAll().stream().map(Event::toEventSummary).toList(); |
| 45 | } | 38 | } |
| 46 | 39 | ||
| 47 | @Override | 40 | @Override |
| @@ -58,9 +51,9 @@ public final class LocalGuessMarketContext implements GuessMarketContext { | |||
| 58 | public PurchaseReceiptDTO buyShares( | 51 | public PurchaseReceiptDTO buyShares( |
| 59 | String userName, String eventKey, String optionKey, int quantity) | 52 | String userName, String eventKey, String optionKey, int quantity) |
| 60 | throws GuessMarketException { | 53 | throws GuessMarketException { |
| 61 | var optional = eventRepository.get(eventKey); | 54 | var optional = context.getEvents().get(eventKey); |
| 62 | 55 | ||
| 63 | var userOptional = userRepository.get(userName); | 56 | var userOptional = context.getUsers().get(userName); |
| 64 | if (optional.isEmpty() || userOptional.isEmpty()) throw new GuessMarketException(); | 57 | if (optional.isEmpty() || userOptional.isEmpty()) throw new GuessMarketException(); |
| 65 | 58 | ||
| 66 | var event = optional.get(); | 59 | var event = optional.get(); |
| @@ -83,7 +76,7 @@ public final class LocalGuessMarketContext implements GuessMarketContext { | |||
| 83 | 76 | ||
| 84 | @Override | 77 | @Override |
| 85 | public void saveState(Path path) throws GuessMarketException { | 78 | public void saveState(Path path) throws GuessMarketException { |
| 86 | throw new UnsupportedOperationException("Unimplemented method 'saveState'"); | 79 | context.save(); |
| 87 | } | 80 | } |
| 88 | 81 | ||
| 89 | @Override | 82 | @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; | |||
| 8 | 8 | ||
| 9 | public final class Account { | 9 | public final class Account { |
| 10 | private final String owner; | 10 | private final String owner; |
| 11 | private final Clock clock; | ||
| 12 | private final List<LedgerEntry> entries = new ArrayList<>(); | 11 | private final List<LedgerEntry> entries = new ArrayList<>(); |
| 13 | private BigDecimal balance; | 12 | private BigDecimal balance; |
| 14 | private int runningId; | 13 | private int runningId; |
| 15 | 14 | ||
| 15 | private final Clock clock; | ||
| 16 | |||
| 16 | public Account(String owner, BigDecimal initialBalance) { | 17 | public Account(String owner, BigDecimal initialBalance) { |
| 17 | this(owner, initialBalance, Clock.systemUTC()); | 18 | this(owner, initialBalance, Clock.systemUTC()); |
| 18 | } | 19 | } |
| @@ -43,6 +44,6 @@ public final class Account { | |||
| 43 | } | 44 | } |
| 44 | 45 | ||
| 45 | private void record(LedgerType type, BigDecimal amount, String note) { | 46 | private void record(LedgerType type, BigDecimal amount, String note) { |
| 46 | entries.add(new LedgerEntry(++runningId, clock.instant(), type, amount, balance, note)); | 47 | entries.add(new LedgerEntry(runningId++, clock.instant(), type, amount, balance, note)); |
| 47 | } | 48 | } |
| 48 | } | 49 | } |
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) { | |||
| 12 | return value.setScale(DEFAULT.scale(), DEFAULT.roundingMode()); | 12 | return value.setScale(DEFAULT.scale(), DEFAULT.roundingMode()); |
| 13 | } | 13 | } |
| 14 | 14 | ||
| 15 | public static BigDecimal toMoney(double v) { | 15 | public static BigDecimal toMoney(int value) { |
| 16 | return BigDecimal.valueOf(v).setScale(DEFAULT.scale(), DEFAULT.roundingMode()); | 16 | return BigDecimal.valueOf(value).setScale(DEFAULT.scale(), DEFAULT.roundingMode()); |
| 17 | } | ||
| 18 | |||
| 19 | public static BigDecimal toMoney(double value) { | ||
| 20 | return BigDecimal.valueOf(value).setScale(DEFAULT.scale(), DEFAULT.roundingMode()); | ||
| 17 | } | 21 | } |
| 18 | } | 22 | } |
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 { | |||
| 84 | } | 84 | } |
| 85 | 85 | ||
| 86 | public List<String> getOptionsNames() { | 86 | public List<String> getOptionsNames() { |
| 87 | return options.stream().map(Option::getName).toList(); | 87 | return options.stream().map(Option::name).toList(); |
| 88 | } | 88 | } |
| 89 | 89 | ||
| 90 | public Account getAccount() { | 90 | public Account getAccount() { |
| @@ -123,15 +123,7 @@ public final class Event { | |||
| 123 | BigDecimal cost, | 123 | BigDecimal cost, |
| 124 | BigDecimal commission) { | 124 | BigDecimal commission) { |
| 125 | var trade = | 125 | var trade = |
| 126 | new Trade( | 126 | new Trade(runningId++, time, user, option.key(), option.name(), quantity, cost, commission); |
| 127 | ++runningId, | ||
| 128 | time, | ||
| 129 | user, | ||
| 130 | option.getKey(), | ||
| 131 | option.getName(), | ||
| 132 | quantity, | ||
| 133 | cost, | ||
| 134 | commission); | ||
| 135 | trades.add(trade); | 127 | trades.add(trade); |
| 136 | 128 | ||
| 137 | return trade; | 129 | return trade; |
| @@ -159,7 +151,7 @@ public final class Event { | |||
| 159 | description, | 151 | description, |
| 160 | commissionPercent, | 152 | commissionPercent, |
| 161 | commissionTiming, | 153 | commissionTiming, |
| 162 | mechanism.type(), | 154 | mechanism.getType(), |
| 163 | status, | 155 | status, |
| 164 | getOptionsNames(), | 156 | getOptionsNames(), |
| 165 | account.getBalance().toPlainString()); | 157 | account.getBalance().toPlainString()); |
| @@ -167,7 +159,7 @@ public final class Event { | |||
| 167 | 159 | ||
| 168 | public Option getOption(String optionKey) { | 160 | public Option getOption(String optionKey) { |
| 169 | return options.stream() | 161 | return options.stream() |
| 170 | .filter(option -> option.getKey().equalsIgnoreCase(optionKey)) | 162 | .filter(option -> option.key().equalsIgnoreCase(optionKey)) |
| 171 | .findFirst() | 163 | .findFirst() |
| 172 | .orElseThrow(() -> new IllegalArgumentException("No such option.")); | 164 | .orElseThrow(() -> new IllegalArgumentException("No such option.")); |
| 173 | } | 165 | } |
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; | |||
| 4 | import java.time.Instant; | 4 | import java.time.Instant; |
| 5 | import market.guess.api.LedgerType; | 5 | import market.guess.api.LedgerType; |
| 6 | 6 | ||
| 7 | public record LedgerEntry( | 7 | public final record LedgerEntry( |
| 8 | int id, | 8 | int id, |
| 9 | Instant time, | 9 | Instant time, |
| 10 | LedgerType type, | 10 | 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 @@ | |||
| 1 | package market.guess.service.domain; | 1 | package market.guess.service.domain; |
| 2 | 2 | ||
| 3 | public final class Option { | 3 | public final record Option(String key, String name) {} |
| 4 | |||
| 5 | private String key; | ||
| 6 | private String name; | ||
| 7 | |||
| 8 | public Option(String key, String name) { | ||
| 9 | super(); | ||
| 10 | this.key = key; | ||
| 11 | this.name = name; | ||
| 12 | } | ||
| 13 | |||
| 14 | public String getName() { | ||
| 15 | return name; | ||
| 16 | } | ||
| 17 | |||
| 18 | public String getKey() { | ||
| 19 | return key; | ||
| 20 | } | ||
| 21 | } | ||
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 @@ | |||
| 1 | package market.guess.service.infrastructure; | ||
| 2 | |||
| 3 | import com.google.gson.Gson; | ||
| 4 | import com.google.gson.GsonBuilder; | ||
| 5 | import java.io.IOException; | ||
| 6 | import java.nio.charset.StandardCharsets; | ||
| 7 | import java.nio.file.Files; | ||
| 8 | import java.nio.file.Paths; | ||
| 9 | import java.time.Instant; | ||
| 10 | import market.guess.service.infrastructure.adapter.InstantTypeAdapter; | ||
| 11 | import market.guess.service.infrastructure.repository.EventRepository; | ||
| 12 | import market.guess.service.infrastructure.repository.UserRepository; | ||
| 13 | |||
| 14 | public final class MarketContext { | ||
| 15 | private final EventRepository events; | ||
| 16 | private final UserRepository users; | ||
| 17 | |||
| 18 | private static final Gson GSON = | ||
| 19 | new GsonBuilder() | ||
| 20 | .registerTypeAdapter(Instant.class, new InstantTypeAdapter()) | ||
| 21 | .setPrettyPrinting() | ||
| 22 | .create(); | ||
| 23 | |||
| 24 | public MarketContext(EventRepository events, UserRepository users) { | ||
| 25 | this.events = events; | ||
| 26 | this.users = users; | ||
| 27 | } | ||
| 28 | |||
| 29 | public EventRepository getEvents() { | ||
| 30 | return events; | ||
| 31 | } | ||
| 32 | |||
| 33 | public UserRepository getUsers() { | ||
| 34 | return users; | ||
| 35 | } | ||
| 36 | |||
| 37 | public void save() { | ||
| 38 | var path = Paths.get("test-file.json"); | ||
| 39 | |||
| 40 | try { | ||
| 41 | if (path.getParent() != null) Files.createDirectories(path.getParent()); | ||
| 42 | Files.writeString(path, GSON.toJson(events), StandardCharsets.UTF_8); | ||
| 43 | } catch (IOException e) { | ||
| 44 | |||
| 45 | } | ||
| 46 | } | ||
| 47 | |||
| 48 | public void load() {} | ||
| 49 | } | ||
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<GMEvent, Event> { | |||
| 14 | 14 | ||
| 15 | @Override | 15 | @Override |
| 16 | public Event toDomain(GMEvent source) { | 16 | public Event toDomain(GMEvent source) { |
| 17 | var options = getOptions(source); | ||
| 18 | |||
| 17 | return new Event( | 19 | return new Event( |
| 18 | getEventKey(source), | 20 | getEventKey(source), |
| 19 | source.getId(), | 21 | source.getId(), |
| @@ -21,9 +23,9 @@ public final class EventMapperV1 implements Mapper<GMEvent, Event> { | |||
| 21 | source.getDescription(), | 23 | source.getDescription(), |
| 22 | source.getComision().getValue(), | 24 | source.getComision().getValue(), |
| 23 | getTiming(source.getComision().getType()), | 25 | getTiming(source.getComision().getType()), |
| 24 | new LmsrTradingMechanism(source.getGMMethod().getGMLMSR().getB()), | 26 | new LmsrTradingMechanism(source.getGMMethod().getGMLMSR().getB(), options.size()), |
| 25 | EventStatus.ACTIVE, | 27 | EventStatus.ACTIVE, |
| 26 | getOptions(source), | 28 | options, |
| 27 | ""); | 29 | ""); |
| 28 | } | 30 | } |
| 29 | 31 | ||
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<GMEvent, Event> { | |||
| 18 | 18 | ||
| 19 | @Override | 19 | @Override |
| 20 | public Event toDomain(GMEvent source) { | 20 | public Event toDomain(GMEvent source) { |
| 21 | var options = getOptions(source); | ||
| 22 | |||
| 21 | return new Event( | 23 | return new Event( |
| 22 | getEventKey(source), | 24 | getEventKey(source), |
| 23 | source.getId(), | 25 | source.getId(), |
| @@ -25,9 +27,9 @@ public final class EventMapperV2 implements Mapper<GMEvent, Event> { | |||
| 25 | source.getDescription(), | 27 | source.getDescription(), |
| 26 | source.getCommission().getValue(), | 28 | source.getCommission().getValue(), |
| 27 | getTiming(source.getCommission().getType()), | 29 | getTiming(source.getCommission().getType()), |
| 28 | getMechanism(source), | 30 | getMechanism(source, options.size()), |
| 29 | EventStatus.NOT_STARTED, | 31 | EventStatus.NOT_STARTED, |
| 30 | getOptions(source), | 32 | options, |
| 31 | ""); | 33 | ""); |
| 32 | } | 34 | } |
| 33 | 35 | ||
| @@ -43,15 +45,16 @@ public final class EventMapperV2 implements Mapper<GMEvent, Event> { | |||
| 43 | }; | 45 | }; |
| 44 | } | 46 | } |
| 45 | 47 | ||
| 46 | private static TradingMechanism getMechanism(GMEvent source) { | 48 | private static TradingMechanism getMechanism(GMEvent source, int optionCount) { |
| 47 | if (source.getGMMethod().getGMLMSR() instanceof GMLMSR lmsr) { | 49 | if (source.getGMMethod().getGMLMSR() instanceof GMLMSR lmsr) { |
| 48 | return new LmsrTradingMechanism(lmsr.getB()); | 50 | return new LmsrTradingMechanism(lmsr.getB(), optionCount); |
| 49 | } | 51 | } |
| 50 | if (source.getGMMethod().getGMOrderBook() instanceof GMOrderBook orderBook) { | 52 | if (source.getGMMethod().getGMOrderBook() instanceof GMOrderBook orderBook) { |
| 51 | return new OrderBookTradingMechanism( | 53 | return new OrderBookTradingMechanism( |
| 52 | orderBook.getAllowMint().equalsIgnoreCase("true"), | 54 | orderBook.getAllowMint().equalsIgnoreCase("true"), |
| 53 | orderBook.getInitial(), | 55 | orderBook.getInitial(), |
| 54 | orderBook.getD()); | 56 | orderBook.getD(), |
| 57 | optionCount); | ||
| 55 | } | 58 | } |
| 56 | throw new IllegalArgumentException(); | 59 | throw new IllegalArgumentException(); |
| 57 | } | 60 | } |
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; | |||
| 9 | import market.guess.service.infrastructure.mapper.v1.EventMapperV1; | 9 | import market.guess.service.infrastructure.mapper.v1.EventMapperV1; |
| 10 | import market.guess.service.infrastructure.provider.LoadValidator; | 10 | import market.guess.service.infrastructure.provider.LoadValidator; |
| 11 | import market.guess.service.infrastructure.provider.Loader; | 11 | import market.guess.service.infrastructure.provider.Loader; |
| 12 | import market.guess.service.infrastructure.repository.EventRepository; | ||
| 13 | import market.guess.service.infrastructure.repository.UserRepository; | ||
| 12 | import market.guess.service.model.v1.GuessMarket; | 14 | import market.guess.service.model.v1.GuessMarket; |
| 13 | import market.guess.service.repository.EventRepository; | ||
| 14 | import market.guess.service.repository.UserRepository; | ||
| 15 | 15 | ||
| 16 | public final class XMLLoaderV1 implements Loader { | 16 | public final class XMLLoaderV1 implements Loader { |
| 17 | private final JAXBContext context; | 17 | private final JAXBContext context; |
diff --git a/service/src/main/java/market/guess/service/repository/EventRepository.java b/service/src/main/java/market/guess/service/infrastructure/repository/EventRepository.java index afc5371..186695b 100644 --- a/service/src/main/java/market/guess/service/repository/EventRepository.java +++ b/service/src/main/java/market/guess/service/infrastructure/repository/EventRepository.java | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | package market.guess.service.repository; | 1 | package market.guess.service.infrastructure.repository; |
| 2 | 2 | ||
| 3 | import market.guess.service.domain.Event; | 3 | import market.guess.service.domain.Event; |
| 4 | 4 | ||
diff --git a/service/src/main/java/market/guess/service/repository/InMemoryEventRepository.java b/service/src/main/java/market/guess/service/infrastructure/repository/InMemoryEventRepository.java index eb7763e..0ed5b20 100644 --- a/service/src/main/java/market/guess/service/repository/InMemoryEventRepository.java +++ b/service/src/main/java/market/guess/service/infrastructure/repository/InMemoryEventRepository.java | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | package market.guess.service.repository; | 1 | package market.guess.service.infrastructure.repository; |
| 2 | 2 | ||
| 3 | import java.util.ArrayList; | 3 | import java.util.ArrayList; |
| 4 | import java.util.Collections; | 4 | import java.util.Collections; |
diff --git a/service/src/main/java/market/guess/service/repository/InMemoryUserRepository.java b/service/src/main/java/market/guess/service/infrastructure/repository/InMemoryUserRepository.java index 0b5cfe7..b4014e5 100644 --- a/service/src/main/java/market/guess/service/repository/InMemoryUserRepository.java +++ b/service/src/main/java/market/guess/service/infrastructure/repository/InMemoryUserRepository.java | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | package market.guess.service.repository; | 1 | package market.guess.service.infrastructure.repository; |
| 2 | 2 | ||
| 3 | import java.util.ArrayList; | 3 | import java.util.ArrayList; |
| 4 | import java.util.Collections; | 4 | import java.util.Collections; |
diff --git a/service/src/main/java/market/guess/service/repository/Repository.java b/service/src/main/java/market/guess/service/infrastructure/repository/Repository.java index 8ea1817..13f3901 100644 --- a/service/src/main/java/market/guess/service/repository/Repository.java +++ b/service/src/main/java/market/guess/service/infrastructure/repository/Repository.java | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | package market.guess.service.repository; | 1 | package market.guess.service.infrastructure.repository; |
| 2 | 2 | ||
| 3 | import java.util.List; | 3 | import java.util.List; |
| 4 | import java.util.Optional; | 4 | import java.util.Optional; |
diff --git a/service/src/main/java/market/guess/service/repository/UserRepository.java b/service/src/main/java/market/guess/service/infrastructure/repository/UserRepository.java index 86cc9a6..4f0b0a6 100644 --- a/service/src/main/java/market/guess/service/repository/UserRepository.java +++ b/service/src/main/java/market/guess/service/infrastructure/repository/UserRepository.java | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | package market.guess.service.repository; | 1 | package market.guess.service.infrastructure.repository; |
| 2 | 2 | ||
| 3 | import market.guess.service.domain.User; | 3 | import market.guess.service.domain.User; |
| 4 | 4 | ||
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 @@ | |||
| 1 | package market.guess.service.mechanism; | 1 | package market.guess.service.mechanism; |
| 2 | 2 | ||
| 3 | import java.math.BigDecimal; | 3 | import java.math.BigDecimal; |
| 4 | import java.util.Arrays; | ||
| 4 | import market.guess.api.MechanismType; | 5 | import market.guess.api.MechanismType; |
| 6 | import market.guess.service.domain.BigDecimalOptions; | ||
| 5 | 7 | ||
| 6 | public final class LmsrTradingMechanism implements TradingMechanism { | 8 | public final class LmsrTradingMechanism implements TradingMechanism { |
| 7 | private final int liquidity; | 9 | private final int liquidity; |
| 10 | private final int[] q; | ||
| 8 | 11 | ||
| 9 | public LmsrTradingMechanism(int liquidity) { | 12 | public LmsrTradingMechanism(int liquidity, int optionCount) { |
| 10 | super(); | ||
| 11 | this.liquidity = liquidity; | 13 | this.liquidity = liquidity; |
| 14 | this.q = new int[optionCount]; | ||
| 12 | } | 15 | } |
| 13 | 16 | ||
| 14 | @Override | 17 | @Override |
| 15 | public MechanismType type() { | 18 | public MechanismType getType() { |
| 16 | return MechanismType.LMSR; | 19 | return MechanismType.LMSR; |
| 17 | } | 20 | } |
| 18 | 21 | ||
| 19 | @Override | 22 | @Override |
| 20 | public BigDecimal openingCost(int optionCount) { | 23 | public BigDecimal openingCost() { |
| 21 | // TODO Auto-generated method stub | 24 | return BigDecimalOptions.toMoney(cost(new int[q.length])); |
| 22 | throw new UnsupportedOperationException("Unimplemented method 'openingCost'"); | ||
| 23 | } | 25 | } |
| 24 | 26 | ||
| 25 | @Override | 27 | @Override |
| 26 | public BigDecimal costOfBuying(int[] q, int optionIndex, int quantity) { | 28 | public TradeExecution buy(int optionIndex, int quantity) { |
| 27 | // TODO Auto-generated method stub | 29 | var before = cost(q); |
| 28 | throw new UnsupportedOperationException("Unimplemented method 'costOfBuying'"); | 30 | |
| 31 | var after = q.clone(); | ||
| 32 | after[optionIndex] += quantity; | ||
| 33 | var delta = cost(after) - before; | ||
| 34 | |||
| 35 | q[optionIndex] += quantity; | ||
| 36 | |||
| 37 | return new TradeExecution(quantity, BigDecimalOptions.toMoney(delta)); | ||
| 29 | } | 38 | } |
| 30 | 39 | ||
| 31 | @Override | 40 | @Override |
| 32 | public double[] prices(int[] q) { | 41 | public BigDecimal[] prices() { |
| 33 | // TODO Auto-generated method stub | 42 | var max = Arrays.stream(q).mapToDouble(qi -> (double) qi / liquidity).max().orElse(0); |
| 34 | throw new UnsupportedOperationException("Unimplemented method 'prices'"); | 43 | |
| 44 | var exp = new double[q.length]; | ||
| 45 | var sumExp = 0.0; | ||
| 46 | for (var i = 0; i < q.length; i++) { | ||
| 47 | exp[i] = Math.exp((double) q[i] / liquidity - max); | ||
| 48 | sumExp += exp[i]; | ||
| 49 | } | ||
| 50 | |||
| 51 | var result = new BigDecimal[q.length]; | ||
| 52 | for (var i = 0; i < q.length; i++) { | ||
| 53 | result[i] = BigDecimalOptions.toMoney(exp[i] / sumExp); | ||
| 54 | } | ||
| 55 | return result; | ||
| 56 | } | ||
| 57 | |||
| 58 | private double cost(int[] qs) { | ||
| 59 | var max = Arrays.stream(qs).mapToDouble(qi -> (double) qi / liquidity).max().orElse(0); | ||
| 60 | |||
| 61 | var sumExp = 0.0; | ||
| 62 | for (var qi : qs) { | ||
| 63 | sumExp += Math.exp((double) qi / liquidity - max); | ||
| 64 | } | ||
| 65 | return liquidity * (max + Math.log(sumExp)); | ||
| 35 | } | 66 | } |
| 36 | } | 67 | } |
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 @@ | |||
| 1 | package market.guess.service.mechanism; | 1 | package market.guess.service.mechanism; |
| 2 | 2 | ||
| 3 | import java.math.BigDecimal; | 3 | import java.math.BigDecimal; |
| 4 | import java.math.RoundingMode; | ||
| 5 | import java.util.ArrayList; | ||
| 6 | import java.util.List; | ||
| 4 | import market.guess.api.MechanismType; | 7 | import market.guess.api.MechanismType; |
| 8 | import market.guess.service.domain.BigDecimalOptions; | ||
| 5 | 9 | ||
| 6 | public final class OrderBookTradingMechanism implements TradingMechanism { | 10 | public final class OrderBookTradingMechanism implements TradingMechanism { |
| 7 | private final boolean allowMint; | 11 | public enum Side { |
| 12 | BUY, | ||
| 13 | SELL | ||
| 14 | } | ||
| 15 | |||
| 16 | private static final class RestingOrder { | ||
| 17 | private final BigDecimal price; | ||
| 18 | private int quantity; | ||
| 19 | |||
| 20 | private RestingOrder(BigDecimal price, int quantity) { | ||
| 21 | this.price = price; | ||
| 22 | this.quantity = quantity; | ||
| 23 | } | ||
| 24 | } | ||
| 25 | |||
| 26 | private final boolean allowMint; // ponytail: mint deferred — cross-option matching, its own pass | ||
| 8 | private final int initial; | 27 | private final int initial; |
| 9 | private final int d; | 28 | private final int d; |
| 29 | private final List<List<RestingOrder>> | ||
| 30 | asks; // per option, ascending price (best = lowest, index 0) | ||
| 31 | private final List<List<RestingOrder>> | ||
| 32 | bids; // per option, descending price (best = highest, index 0) | ||
| 33 | private final BigDecimal[] lastTradePrice; | ||
| 10 | 34 | ||
| 11 | public OrderBookTradingMechanism(boolean allowMint, int initial, int d) { | 35 | public OrderBookTradingMechanism(boolean allowMint, int initial, int d, int optionCount) { |
| 12 | super(); | ||
| 13 | this.allowMint = allowMint; | 36 | this.allowMint = allowMint; |
| 14 | this.initial = initial; | 37 | this.initial = initial; |
| 15 | this.d = d; | 38 | this.d = d; |
| 39 | this.asks = newBooks(optionCount); | ||
| 40 | this.bids = newBooks(optionCount); | ||
| 41 | this.lastTradePrice = new BigDecimal[optionCount]; | ||
| 42 | } | ||
| 43 | |||
| 44 | private static List<List<RestingOrder>> newBooks(int optionCount) { | ||
| 45 | var books = new ArrayList<List<RestingOrder>>(optionCount); | ||
| 46 | for (var i = 0; i < optionCount; i++) books.add(new ArrayList<>()); | ||
| 47 | return books; | ||
| 16 | } | 48 | } |
| 17 | 49 | ||
| 18 | @Override | 50 | @Override |
| 19 | public MechanismType type() { | 51 | public MechanismType getType() { |
| 20 | return MechanismType.ORDER_BOOK; | 52 | return MechanismType.ORDER_BOOK; |
| 21 | } | 53 | } |
| 22 | 54 | ||
| 23 | @Override | 55 | @Override |
| 24 | public BigDecimal openingCost(int optionCount) { | 56 | public BigDecimal openingCost() { |
| 25 | // TODO Auto-generated method stub | 57 | return BigDecimalOptions.toMoney(initial); |
| 26 | throw new UnsupportedOperationException("Unimplemented method 'openingCost'"); | ||
| 27 | } | 58 | } |
| 28 | 59 | ||
| 29 | @Override | 60 | @Override |
| 30 | public BigDecimal costOfBuying(int[] q, int optionIndex, int quantity) { | 61 | public TradeExecution buy(int optionIndex, int quantity) { |
| 31 | // TODO Auto-generated method stub | 62 | return match(optionIndex, Side.BUY, quantity, null, false); |
| 32 | throw new UnsupportedOperationException("Unimplemented method 'costOfBuying'"); | 63 | } |
| 64 | |||
| 65 | /** Priced limit order — not on the shared interface, no engine call site yet. */ | ||
| 66 | public TradeExecution placeOrder(int optionIndex, Side side, int quantity, BigDecimal price) { | ||
| 67 | if (price.signum() < 0 || price.compareTo(BigDecimal.valueOf(d)) > 0) { | ||
| 68 | throw new IllegalArgumentException("Price must be between 0 and " + d); | ||
| 69 | } | ||
| 70 | return match(optionIndex, side, quantity, price, true); | ||
| 33 | } | 71 | } |
| 34 | 72 | ||
| 35 | @Override | 73 | @Override |
| 36 | public double[] prices(int[] q) { | 74 | public BigDecimal[] prices() { |
| 37 | // TODO Auto-generated method stub | 75 | var result = new BigDecimal[asks.size()]; |
| 38 | throw new UnsupportedOperationException("Unimplemented method 'prices'"); | 76 | for (var i = 0; i < result.length; i++) result[i] = priceFor(i); |
| 77 | return result; | ||
| 78 | } | ||
| 79 | |||
| 80 | private BigDecimal priceFor(int optionIndex) { | ||
| 81 | var bestBid = bestPrice(bids.get(optionIndex)); | ||
| 82 | var bestAsk = bestPrice(asks.get(optionIndex)); | ||
| 83 | |||
| 84 | if (bestBid != null && bestAsk != null) { | ||
| 85 | return BigDecimalOptions.toMoney( | ||
| 86 | bestBid.add(bestAsk).divide(BigDecimal.valueOf(2), 10, RoundingMode.HALF_EVEN)); | ||
| 87 | } | ||
| 88 | if (bestBid != null) return BigDecimalOptions.toMoney(bestBid); | ||
| 89 | if (bestAsk != null) return BigDecimalOptions.toMoney(bestAsk); | ||
| 90 | if (lastTradePrice[optionIndex] != null) return lastTradePrice[optionIndex]; | ||
| 91 | |||
| 92 | // No information yet: neutral 50/50 prior, same as LMSR's day-one price. | ||
| 93 | return BigDecimalOptions.toMoney(d / 2.0); | ||
| 94 | } | ||
| 95 | |||
| 96 | private static BigDecimal bestPrice(List<RestingOrder> book) { | ||
| 97 | return book.isEmpty() ? null : book.get(0).price; | ||
| 98 | } | ||
| 99 | |||
| 100 | private TradeExecution match( | ||
| 101 | int optionIndex, Side side, int quantity, BigDecimal limitPrice, boolean restRemainder) { | ||
| 102 | var opposing = side == Side.BUY ? asks.get(optionIndex) : bids.get(optionIndex); | ||
| 103 | |||
| 104 | var filled = 0; | ||
| 105 | var proceeds = BigDecimal.ZERO; | ||
| 106 | while (filled < quantity | ||
| 107 | && !opposing.isEmpty() | ||
| 108 | && crosses(side, opposing.get(0).price, limitPrice)) { | ||
| 109 | var top = opposing.get(0); | ||
| 110 | var take = Math.min(quantity - filled, top.quantity); | ||
| 111 | |||
| 112 | filled += take; | ||
| 113 | proceeds = proceeds.add(top.price.multiply(BigDecimal.valueOf(take))); | ||
| 114 | top.quantity -= take; | ||
| 115 | if (top.quantity == 0) opposing.remove(0); | ||
| 116 | } | ||
| 117 | |||
| 118 | if (filled > 0) { | ||
| 119 | lastTradePrice[optionIndex] = | ||
| 120 | BigDecimalOptions.toMoney( | ||
| 121 | proceeds.divide(BigDecimal.valueOf(filled), 10, RoundingMode.HALF_EVEN)); | ||
| 122 | } | ||
| 123 | if (restRemainder && filled < quantity) { | ||
| 124 | var restingBook = side == Side.BUY ? bids.get(optionIndex) : asks.get(optionIndex); | ||
| 125 | restingBook.add( | ||
| 126 | insertionIndex(restingBook, side, limitPrice), | ||
| 127 | new RestingOrder(limitPrice, quantity - filled)); | ||
| 128 | } | ||
| 129 | |||
| 130 | return new TradeExecution(filled, BigDecimalOptions.toMoney(proceeds)); | ||
| 131 | } | ||
| 132 | |||
| 133 | private static boolean crosses(Side side, BigDecimal restingPrice, BigDecimal limitPrice) { | ||
| 134 | if (limitPrice == null) return true; // market order: any price crosses | ||
| 135 | return side == Side.BUY | ||
| 136 | ? restingPrice.compareTo(limitPrice) <= 0 | ||
| 137 | : restingPrice.compareTo(limitPrice) >= 0; | ||
| 138 | } | ||
| 139 | |||
| 140 | private static int insertionIndex(List<RestingOrder> book, Side side, BigDecimal price) { | ||
| 141 | var i = 0; | ||
| 142 | while (i < book.size() && betterOrEqual(side, book.get(i).price, price)) i++; | ||
| 143 | return i; | ||
| 144 | } | ||
| 145 | |||
| 146 | private static boolean betterOrEqual(Side side, BigDecimal existing, BigDecimal incoming) { | ||
| 147 | return side == Side.BUY | ||
| 148 | ? existing.compareTo(incoming) | ||
| 149 | >= 0 // bids: descending, ties keep earlier order first (FIFO) | ||
| 150 | : existing.compareTo(incoming) | ||
| 151 | <= 0; // asks: ascending, ties keep earlier order first (FIFO) | ||
| 39 | } | 152 | } |
| 40 | } | 153 | } |
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 @@ | |||
| 1 | package market.guess.service.mechanism; | ||
| 2 | |||
| 3 | import java.math.BigDecimal; | ||
| 4 | |||
| 5 | 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; | |||
| 3 | import java.math.BigDecimal; | 3 | import java.math.BigDecimal; |
| 4 | import market.guess.api.MechanismType; | 4 | import market.guess.api.MechanismType; |
| 5 | 5 | ||
| 6 | public interface TradingMechanism { | 6 | public sealed interface TradingMechanism permits LmsrTradingMechanism, OrderBookTradingMechanism { |
| 7 | 7 | ||
| 8 | MechanismType type(); | 8 | MechanismType getType(); |
| 9 | 9 | ||
| 10 | BigDecimal openingCost(int optionCount); | 10 | BigDecimal openingCost(); |
| 11 | 11 | ||
| 12 | BigDecimal costOfBuying(int[] q, int optionIndex, int quantity); | 12 | TradeExecution buy(int optionIndex, int quantity); |
| 13 | 13 | ||
| 14 | double[] prices(int[] q); | 14 | BigDecimal[] prices(); |
| 15 | } | 15 | } |