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 | |
| 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.
29 files changed, 600 insertions, 114 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 | } |
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 @@ | |||
| 1 | package market.guess.service.mechanism; | ||
| 2 | |||
| 3 | import static org.junit.Assert.assertArrayEquals; | ||
| 4 | import static org.junit.Assert.assertEquals; | ||
| 5 | |||
| 6 | import java.math.BigDecimal; | ||
| 7 | import org.junit.Test; | ||
| 8 | |||
| 9 | public class LmsrTradingMechanismTest { | ||
| 10 | |||
| 11 | @Test | ||
| 12 | public void openingCostMatchesLmsrFormula() { | ||
| 13 | var mechanism = new LmsrTradingMechanism(100, 2); | ||
| 14 | |||
| 15 | assertEquals(new BigDecimal("69.31"), mechanism.openingCost()); | ||
| 16 | } | ||
| 17 | |||
| 18 | @Test | ||
| 19 | public void pricesStartEven() { | ||
| 20 | var mechanism = new LmsrTradingMechanism(100, 2); | ||
| 21 | |||
| 22 | assertArrayEquals( | ||
| 23 | new BigDecimal[] {new BigDecimal("0.50"), new BigDecimal("0.50")}, mechanism.prices()); | ||
| 24 | } | ||
| 25 | |||
| 26 | @Test | ||
| 27 | public void buyingSharesShiftsCostAndPrice() { | ||
| 28 | var mechanism = new LmsrTradingMechanism(100, 2); | ||
| 29 | |||
| 30 | var execution = mechanism.buy(0, 100); | ||
| 31 | |||
| 32 | assertEquals(100, execution.filledQuantity()); | ||
| 33 | assertEquals(new BigDecimal("62.01"), execution.cost()); | ||
| 34 | assertArrayEquals( | ||
| 35 | new BigDecimal[] {new BigDecimal("0.73"), new BigDecimal("0.27")}, mechanism.prices()); | ||
| 36 | } | ||
| 37 | } | ||
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 @@ | |||
| 1 | package market.guess.service.mechanism; | ||
| 2 | |||
| 3 | import static org.junit.Assert.assertEquals; | ||
| 4 | |||
| 5 | import java.math.BigDecimal; | ||
| 6 | import market.guess.service.mechanism.OrderBookTradingMechanism.Side; | ||
| 7 | import org.junit.Test; | ||
| 8 | |||
| 9 | public class OrderBookTradingMechanismTest { | ||
| 10 | |||
| 11 | @Test | ||
| 12 | public void limitBuyConsumesRestingAsksInFifoOrderAndLeavesRemainder() { | ||
| 13 | var mechanism = new OrderBookTradingMechanism(false, 100, 1, 2); | ||
| 14 | var price = new BigDecimal("0.60"); | ||
| 15 | |||
| 16 | mechanism.placeOrder(0, Side.SELL, 20, price); | ||
| 17 | mechanism.placeOrder(0, Side.SELL, 40, price); | ||
| 18 | |||
| 19 | var execution = mechanism.placeOrder(0, Side.BUY, 50, price); | ||
| 20 | |||
| 21 | assertEquals(50, execution.filledQuantity()); | ||
| 22 | assertEquals(new BigDecimal("30.00"), execution.cost()); | ||
| 23 | |||
| 24 | var followUp = mechanism.buy(0, 100); | ||
| 25 | assertEquals(10, followUp.filledQuantity()); | ||
| 26 | } | ||
| 27 | |||
| 28 | @Test | ||
| 29 | public void marketBuySweepsMultiplePriceLevelsAndPartiallyFillsWhenBookRunsOut() { | ||
| 30 | var mechanism = new OrderBookTradingMechanism(false, 100, 1, 2); | ||
| 31 | mechanism.placeOrder(0, Side.SELL, 10, new BigDecimal("0.50")); | ||
| 32 | mechanism.placeOrder(0, Side.SELL, 10, new BigDecimal("0.70")); | ||
| 33 | |||
| 34 | var execution = mechanism.buy(0, 15); | ||
| 35 | |||
| 36 | assertEquals(15, execution.filledQuantity()); | ||
| 37 | assertEquals(new BigDecimal("8.50"), execution.cost()); | ||
| 38 | |||
| 39 | var exhausted = mechanism.buy(0, 100); | ||
| 40 | assertEquals(5, exhausted.filledQuantity()); | ||
| 41 | } | ||
| 42 | |||
| 43 | @Test | ||
| 44 | public void pricesReflectMidpointOfBestBidAndAsk() { | ||
| 45 | var mechanism = new OrderBookTradingMechanism(false, 100, 1, 2); | ||
| 46 | mechanism.placeOrder(0, Side.BUY, 10, new BigDecimal("0.40")); | ||
| 47 | mechanism.placeOrder(0, Side.SELL, 10, new BigDecimal("0.60")); | ||
| 48 | |||
| 49 | assertEquals(new BigDecimal("0.50"), mechanism.prices()[0]); | ||
| 50 | } | ||
| 51 | } | ||
diff --git a/ui-console/src/main/java/market/guess/ui/console/App.java b/ui-console/src/main/java/market/guess/ui/console/App.java index 3d42e9b..330498c 100644 --- a/ui-console/src/main/java/market/guess/ui/console/App.java +++ b/ui-console/src/main/java/market/guess/ui/console/App.java | |||
| @@ -4,16 +4,22 @@ import market.guess.api.GuessMarketContext; | |||
| 4 | import market.guess.service.LocalGuessMarketContext; | 4 | import market.guess.service.LocalGuessMarketContext; |
| 5 | import market.guess.service.TradingOperation; | 5 | import market.guess.service.TradingOperation; |
| 6 | import market.guess.service.TradingOperationImpl; | 6 | import market.guess.service.TradingOperationImpl; |
| 7 | import market.guess.service.infrastructure.MarketContext; | ||
| 7 | import market.guess.service.infrastructure.mapper.v1.EventMapperV1; | 8 | import market.guess.service.infrastructure.mapper.v1.EventMapperV1; |
| 8 | import market.guess.service.infrastructure.provider.Loader; | 9 | import market.guess.service.infrastructure.provider.Loader; |
| 9 | import market.guess.service.infrastructure.provider.v1.XMLLoaderV1; | 10 | import market.guess.service.infrastructure.provider.v1.XMLLoaderV1; |
| 10 | import market.guess.service.repository.EventRepository; | 11 | import market.guess.service.infrastructure.repository.EventRepository; |
| 11 | import market.guess.service.repository.InMemoryEventRepository; | 12 | import market.guess.service.infrastructure.repository.InMemoryEventRepository; |
| 12 | import market.guess.service.repository.InMemoryUserRepository; | 13 | import market.guess.service.infrastructure.repository.InMemoryUserRepository; |
| 13 | import market.guess.service.repository.UserRepository; | 14 | import market.guess.service.infrastructure.repository.UserRepository; |
| 15 | import market.guess.ui.console.command.BuyMenuCommand; | ||
| 16 | import market.guess.ui.console.command.EventDetailsMenuCommand; | ||
| 14 | import market.guess.ui.console.command.ExitMenuCommand; | 17 | import market.guess.ui.console.command.ExitMenuCommand; |
| 15 | import market.guess.ui.console.command.ListEventsMenuCommand; | 18 | import market.guess.ui.console.command.ListEventsMenuCommand; |
| 16 | import market.guess.ui.console.command.LoadFileMenuCommand; | 19 | import market.guess.ui.console.command.LoadFileMenuCommand; |
| 20 | import market.guess.ui.console.command.LoadMarketStateMenuCommand; | ||
| 21 | import market.guess.ui.console.command.SaveMarketStateMenuCommand; | ||
| 22 | import market.guess.ui.console.command.SettleEventMenuCommand; | ||
| 17 | import market.guess.ui.console.io.InputProcessor; | 23 | import market.guess.ui.console.io.InputProcessor; |
| 18 | import org.picocontainer.DefaultPicoContainer; | 24 | import org.picocontainer.DefaultPicoContainer; |
| 19 | import org.picocontainer.behaviors.Caching; | 25 | import org.picocontainer.behaviors.Caching; |
| @@ -24,7 +30,7 @@ public class App { | |||
| 24 | 30 | ||
| 25 | pico.addComponent(EventRepository.class, InMemoryEventRepository.class); | 31 | pico.addComponent(EventRepository.class, InMemoryEventRepository.class); |
| 26 | pico.addComponent(UserRepository.class, InMemoryUserRepository.class); | 32 | pico.addComponent(UserRepository.class, InMemoryUserRepository.class); |
| 27 | 33 | pico.addComponent(MarketContext.class); | |
| 28 | pico.addComponent(TradingOperation.class, TradingOperationImpl.class); | 34 | pico.addComponent(TradingOperation.class, TradingOperationImpl.class); |
| 29 | pico.addComponent(EventMapperV1.class); | 35 | pico.addComponent(EventMapperV1.class); |
| 30 | pico.addComponent(Loader.class, XMLLoaderV1.class); | 36 | pico.addComponent(Loader.class, XMLLoaderV1.class); |
| @@ -33,6 +39,11 @@ public class App { | |||
| 33 | pico.addComponent(LoadFileMenuCommand.class); | 39 | pico.addComponent(LoadFileMenuCommand.class); |
| 34 | pico.addComponent(ListEventsMenuCommand.class); | 40 | pico.addComponent(ListEventsMenuCommand.class); |
| 35 | pico.addComponent(ExitMenuCommand.class); | 41 | pico.addComponent(ExitMenuCommand.class); |
| 42 | pico.addComponent(BuyMenuCommand.class); | ||
| 43 | pico.addComponent(SettleEventMenuCommand.class); | ||
| 44 | pico.addComponent(EventDetailsMenuCommand.class); | ||
| 45 | pico.addComponent(LoadMarketStateMenuCommand.class); | ||
| 46 | pico.addComponent(SaveMarketStateMenuCommand.class); | ||
| 36 | 47 | ||
| 37 | pico.addComponent(InputProcessor.class); | 48 | pico.addComponent(InputProcessor.class); |
| 38 | pico.addComponent(Menu.class); | 49 | pico.addComponent(Menu.class); |
diff --git a/ui-console/src/main/java/market/guess/ui/console/Menu.java b/ui-console/src/main/java/market/guess/ui/console/Menu.java index 18b72b7..90ddd38 100644 --- a/ui-console/src/main/java/market/guess/ui/console/Menu.java +++ b/ui-console/src/main/java/market/guess/ui/console/Menu.java | |||
| @@ -1,5 +1,6 @@ | |||
| 1 | package market.guess.ui.console; | 1 | package market.guess.ui.console; |
| 2 | 2 | ||
| 3 | import java.util.Comparator; | ||
| 3 | import java.util.List; | 4 | import java.util.List; |
| 4 | import market.guess.ui.console.command.MenuCommand; | 5 | import market.guess.ui.console.command.MenuCommand; |
| 5 | import market.guess.ui.console.io.InputProcessor; | 6 | import market.guess.ui.console.io.InputProcessor; |
| @@ -10,7 +11,8 @@ public final class Menu implements Startable { | |||
| 10 | private final List<MenuCommand> commands; | 11 | private final List<MenuCommand> commands; |
| 11 | 12 | ||
| 12 | public Menu(List<MenuCommand> commands, InputProcessor io) { | 13 | public Menu(List<MenuCommand> commands, InputProcessor io) { |
| 13 | this.commands = commands; | 14 | this.commands = |
| 15 | commands.stream().sorted(Comparator.comparingInt(MenuCommand::getIndex)).toList(); | ||
| 14 | this.io = io; | 16 | this.io = io; |
| 15 | } | 17 | } |
| 16 | 18 | ||
| @@ -19,8 +21,11 @@ public final class Menu implements Startable { | |||
| 19 | var max = commands.stream().mapToInt(MenuCommand::getIndex).max().orElse(0); | 21 | var max = commands.stream().mapToInt(MenuCommand::getIndex).max().orElse(0); |
| 20 | io.splashScreen(); | 22 | io.splashScreen(); |
| 21 | while (true) { | 23 | while (true) { |
| 22 | printMenu(); | ||
| 23 | 24 | ||
| 25 | io.newLine(); | ||
| 26 | for (var command : commands) { | ||
| 27 | io.println(" [%d]->> %s".formatted(command.getIndex(), command.getName())); | ||
| 28 | } | ||
| 24 | var selection = io.readInt("Choose a command: ", 1, max); | 29 | var selection = io.readInt("Choose a command: ", 1, max); |
| 25 | var commandOptional = commands.stream().filter(c -> c.getIndex() == selection).findFirst(); | 30 | var commandOptional = commands.stream().filter(c -> c.getIndex() == selection).findFirst(); |
| 26 | 31 | ||
| @@ -32,7 +37,9 @@ public final class Menu implements Startable { | |||
| 32 | 37 | ||
| 33 | command.execute(); | 38 | command.execute(); |
| 34 | 39 | ||
| 35 | if (command.isExit()) return; | 40 | if (command.isExit()) { |
| 41 | break; | ||
| 42 | } | ||
| 36 | } | 43 | } |
| 37 | } | 44 | } |
| 38 | 45 | ||
| @@ -40,11 +47,4 @@ public final class Menu implements Startable { | |||
| 40 | public void stop() { | 47 | public void stop() { |
| 41 | // Do nothing. | 48 | // Do nothing. |
| 42 | } | 49 | } |
| 43 | |||
| 44 | private void printMenu() { | ||
| 45 | io.newLine(); | ||
| 46 | for (var command : commands) { | ||
| 47 | io.println(" [%d]->> %s".formatted(command.getIndex(), command.getName())); | ||
| 48 | } | ||
| 49 | } | ||
| 50 | } | 50 | } |
diff --git a/ui-console/src/main/java/market/guess/ui/console/command/BuyMenuCommand.java b/ui-console/src/main/java/market/guess/ui/console/command/BuyMenuCommand.java new file mode 100644 index 0000000..b8e2523 --- /dev/null +++ b/ui-console/src/main/java/market/guess/ui/console/command/BuyMenuCommand.java | |||
| @@ -0,0 +1,74 @@ | |||
| 1 | package market.guess.ui.console.command; | ||
| 2 | |||
| 3 | import market.guess.api.EventStatus; | ||
| 4 | import market.guess.api.GuessMarketContext; | ||
| 5 | import market.guess.exception.GuessMarketException; | ||
| 6 | import market.guess.ui.console.io.InputProcessor; | ||
| 7 | |||
| 8 | public final class BuyMenuCommand implements MenuCommand { | ||
| 9 | private final GuessMarketContext context; | ||
| 10 | private final InputProcessor io; | ||
| 11 | |||
| 12 | public BuyMenuCommand(GuessMarketContext context, InputProcessor io) { | ||
| 13 | super(); | ||
| 14 | this.context = context; | ||
| 15 | this.io = io; | ||
| 16 | } | ||
| 17 | |||
| 18 | @Override | ||
| 19 | public int getIndex() { | ||
| 20 | return 4; | ||
| 21 | } | ||
| 22 | |||
| 23 | @Override | ||
| 24 | public String getName() { | ||
| 25 | return "Buy share(s) in an event"; | ||
| 26 | } | ||
| 27 | |||
| 28 | @Override | ||
| 29 | public boolean allowed() { | ||
| 30 | // TODO Auto-generated method stub | ||
| 31 | throw new UnsupportedOperationException("Unimplemented method 'allowed'"); | ||
| 32 | } | ||
| 33 | |||
| 34 | @Override | ||
| 35 | public void execute() { | ||
| 36 | var active = | ||
| 37 | context.listEvents().stream() | ||
| 38 | .filter(event -> event.status() == EventStatus.ACTIVE) | ||
| 39 | .toList(); | ||
| 40 | |||
| 41 | if (active.isEmpty()) { | ||
| 42 | io.println("There are currently no active events to trade on."); | ||
| 43 | return; | ||
| 44 | } | ||
| 45 | |||
| 46 | var pick = | ||
| 47 | io.readSelect( | ||
| 48 | "Pick an event: ", | ||
| 49 | active, | ||
| 50 | event -> "[%d] %s".formatted(event.displayId(), event.name())); | ||
| 51 | |||
| 52 | var event = context.getEvent(pick.eventKey()); | ||
| 53 | |||
| 54 | if (event == null) { | ||
| 55 | io.println("That event is no longer available."); | ||
| 56 | return; | ||
| 57 | } | ||
| 58 | |||
| 59 | // TODO: print state | ||
| 60 | |||
| 61 | var option = | ||
| 62 | io.readSelect( | ||
| 63 | "Choose an option: ", event.state().options(), o -> "%s [value %d, %d shares helds]"); | ||
| 64 | |||
| 65 | var amount = io.readInt("How many shares? ", 1, Integer.MAX_VALUE); | ||
| 66 | |||
| 67 | try { | ||
| 68 | var receipt = context.buyShares("", option.optionKey(), option.optionKey(), amount); | ||
| 69 | } catch (GuessMarketException e) { | ||
| 70 | io.newLine(); | ||
| 71 | io.println("Purchase declined: %s".formatted(e.getMessage())); | ||
| 72 | } | ||
| 73 | } | ||
| 74 | } | ||
diff --git a/ui-console/src/main/java/market/guess/ui/console/command/EventDetailsMenuCommand.java b/ui-console/src/main/java/market/guess/ui/console/command/EventDetailsMenuCommand.java new file mode 100644 index 0000000..e12caeb --- /dev/null +++ b/ui-console/src/main/java/market/guess/ui/console/command/EventDetailsMenuCommand.java | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | package market.guess.ui.console.command; | ||
| 2 | |||
| 3 | public final class EventDetailsMenuCommand implements MenuCommand { | ||
| 4 | |||
| 5 | @Override | ||
| 6 | public int getIndex() { | ||
| 7 | return 3; | ||
| 8 | } | ||
| 9 | |||
| 10 | @Override | ||
| 11 | public String getName() { | ||
| 12 | return "Show event details"; | ||
| 13 | } | ||
| 14 | |||
| 15 | @Override | ||
| 16 | public boolean allowed() { | ||
| 17 | // TODO Auto-generated method stub | ||
| 18 | throw new UnsupportedOperationException("Unimplemented method 'allowed'"); | ||
| 19 | } | ||
| 20 | |||
| 21 | @Override | ||
| 22 | public void execute() { | ||
| 23 | // TODO Auto-generated method stub | ||
| 24 | throw new UnsupportedOperationException("Unimplemented method 'execute'"); | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/ui-console/src/main/java/market/guess/ui/console/command/LoadMarketStateMenuCommand.java b/ui-console/src/main/java/market/guess/ui/console/command/LoadMarketStateMenuCommand.java new file mode 100644 index 0000000..3111044 --- /dev/null +++ b/ui-console/src/main/java/market/guess/ui/console/command/LoadMarketStateMenuCommand.java | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | package market.guess.ui.console.command; | ||
| 2 | |||
| 3 | public final class LoadMarketStateMenuCommand implements MenuCommand { | ||
| 4 | |||
| 5 | @Override | ||
| 6 | public int getIndex() { | ||
| 7 | return 8; | ||
| 8 | } | ||
| 9 | |||
| 10 | @Override | ||
| 11 | public String getName() { | ||
| 12 | return "Load market state"; | ||
| 13 | } | ||
| 14 | |||
| 15 | @Override | ||
| 16 | public boolean allowed() { | ||
| 17 | // TODO Auto-generated method stub | ||
| 18 | throw new UnsupportedOperationException("Unimplemented method 'allowed'"); | ||
| 19 | } | ||
| 20 | |||
| 21 | @Override | ||
| 22 | public void execute() { | ||
| 23 | // TODO Auto-generated method stub | ||
| 24 | throw new UnsupportedOperationException("Unimplemented method 'execute'"); | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/ui-console/src/main/java/market/guess/ui/console/command/SaveMarketStateMenuCommand.java b/ui-console/src/main/java/market/guess/ui/console/command/SaveMarketStateMenuCommand.java new file mode 100644 index 0000000..2f950f0 --- /dev/null +++ b/ui-console/src/main/java/market/guess/ui/console/command/SaveMarketStateMenuCommand.java | |||
| @@ -0,0 +1,44 @@ | |||
| 1 | package market.guess.ui.console.command; | ||
| 2 | |||
| 3 | import java.nio.file.Path; | ||
| 4 | import market.guess.api.GuessMarketContext; | ||
| 5 | import market.guess.ui.console.io.InputProcessor; | ||
| 6 | |||
| 7 | public final class SaveMarketStateMenuCommand implements MenuCommand { | ||
| 8 | private final GuessMarketContext context; | ||
| 9 | private final InputProcessor io; | ||
| 10 | |||
| 11 | public SaveMarketStateMenuCommand(GuessMarketContext context, InputProcessor io) { | ||
| 12 | super(); | ||
| 13 | this.context = context; | ||
| 14 | this.io = io; | ||
| 15 | } | ||
| 16 | |||
| 17 | @Override | ||
| 18 | public int getIndex() { | ||
| 19 | return 7; | ||
| 20 | } | ||
| 21 | |||
| 22 | @Override | ||
| 23 | public String getName() { | ||
| 24 | return "Save current state"; | ||
| 25 | } | ||
| 26 | |||
| 27 | @Override | ||
| 28 | public boolean allowed() { | ||
| 29 | // TODO Auto-generated method stub | ||
| 30 | throw new UnsupportedOperationException("Unimplemented method 'allowed'"); | ||
| 31 | } | ||
| 32 | |||
| 33 | @Override | ||
| 34 | public void execute() { | ||
| 35 | var path = io.readPath("Save to: "); | ||
| 36 | |||
| 37 | try { | ||
| 38 | context.saveState(Path.of(path)); | ||
| 39 | io.println("Saved current state to %s.".formatted(path)); | ||
| 40 | } catch (Exception e) { | ||
| 41 | io.println("Failed to save: %s".formatted(e.getMessage())); | ||
| 42 | } | ||
| 43 | } | ||
| 44 | } | ||
diff --git a/ui-console/src/main/java/market/guess/ui/console/command/SettleEventMenuCommand.java b/ui-console/src/main/java/market/guess/ui/console/command/SettleEventMenuCommand.java new file mode 100644 index 0000000..9b62be5 --- /dev/null +++ b/ui-console/src/main/java/market/guess/ui/console/command/SettleEventMenuCommand.java | |||
| @@ -0,0 +1,36 @@ | |||
| 1 | package market.guess.ui.console.command; | ||
| 2 | |||
| 3 | import market.guess.api.GuessMarketContext; | ||
| 4 | import market.guess.ui.console.io.InputProcessor; | ||
| 5 | |||
| 6 | public final class SettleEventMenuCommand implements MenuCommand { | ||
| 7 | private final GuessMarketContext context; | ||
| 8 | private final InputProcessor io; | ||
| 9 | |||
| 10 | public SettleEventMenuCommand(GuessMarketContext context, InputProcessor io) { | ||
| 11 | this.context = context; | ||
| 12 | this.io = io; | ||
| 13 | } | ||
| 14 | |||
| 15 | @Override | ||
| 16 | public int getIndex() { | ||
| 17 | return 5; | ||
| 18 | } | ||
| 19 | |||
| 20 | @Override | ||
| 21 | public String getName() { | ||
| 22 | return "Settle an event"; | ||
| 23 | } | ||
| 24 | |||
| 25 | @Override | ||
| 26 | public boolean allowed() { | ||
| 27 | // TODO Auto-generated method stub | ||
| 28 | throw new UnsupportedOperationException("Unimplemented method 'allowed'"); | ||
| 29 | } | ||
| 30 | |||
| 31 | @Override | ||
| 32 | public void execute() { | ||
| 33 | // TODO Auto-generated method stub | ||
| 34 | throw new UnsupportedOperationException("Unimplemented method 'execute'"); | ||
| 35 | } | ||
| 36 | } | ||
diff --git a/ui-console/src/main/java/market/guess/ui/console/io/InputProcessor.java b/ui-console/src/main/java/market/guess/ui/console/io/InputProcessor.java index fc5422f..b6d5405 100644 --- a/ui-console/src/main/java/market/guess/ui/console/io/InputProcessor.java +++ b/ui-console/src/main/java/market/guess/ui/console/io/InputProcessor.java | |||
| @@ -20,14 +20,20 @@ public final class InputProcessor { | |||
| 20 | } | 20 | } |
| 21 | 21 | ||
| 22 | public void splashScreen() { | 22 | public void splashScreen() { |
| 23 | IO.println("<<-- GUESS MARKET -->>"); | 23 | newLine(); |
| 24 | IO.println("<<- GUESS MARKET ->>"); | ||
| 25 | } | ||
| 26 | |||
| 27 | public void printState() { | ||
| 28 | nextLine(); | ||
| 29 | println(" Option | Value | Shares "); | ||
| 30 | |||
| 31 | newLine(); | ||
| 24 | } | 32 | } |
| 25 | 33 | ||
| 26 | public String readLine(String prompt) { | 34 | public String readLine(String prompt) { |
| 27 | print(prompt); | 35 | print(prompt); |
| 28 | var line = nextLine(); | 36 | return nextLine().trim(); |
| 29 | |||
| 30 | return line.trim(); | ||
| 31 | } | 37 | } |
| 32 | 38 | ||
| 33 | public int readInt(String prompt, int min, int max) { | 39 | public int readInt(String prompt, int min, int max) { |