aboutsummaryrefslogtreecommitdiffstats
path: root/service/src
diff options
context:
space:
mode:
Diffstat (limited to 'service/src')
-rw-r--r--service/src/main/java/market/guess/service/LocalGuessMarketContext.java23
-rw-r--r--service/src/main/java/market/guess/service/domain/Account.java5
-rw-r--r--service/src/main/java/market/guess/service/domain/BigDecimalOptions.java8
-rw-r--r--service/src/main/java/market/guess/service/domain/Event.java16
-rw-r--r--service/src/main/java/market/guess/service/domain/LedgerEntry.java2
-rw-r--r--service/src/main/java/market/guess/service/domain/Option.java20
-rw-r--r--service/src/main/java/market/guess/service/infrastructure/MarketContext.java49
-rw-r--r--service/src/main/java/market/guess/service/infrastructure/mapper/v1/EventMapperV1.java6
-rw-r--r--service/src/main/java/market/guess/service/infrastructure/mapper/v2/EventMapperV2.java13
-rw-r--r--service/src/main/java/market/guess/service/infrastructure/provider/v1/XMLLoaderV1.java4
-rw-r--r--service/src/main/java/market/guess/service/infrastructure/repository/EventRepository.java (renamed from service/src/main/java/market/guess/service/repository/EventRepository.java)2
-rw-r--r--service/src/main/java/market/guess/service/infrastructure/repository/InMemoryEventRepository.java (renamed from service/src/main/java/market/guess/service/repository/InMemoryEventRepository.java)2
-rw-r--r--service/src/main/java/market/guess/service/infrastructure/repository/InMemoryUserRepository.java (renamed from service/src/main/java/market/guess/service/repository/InMemoryUserRepository.java)2
-rw-r--r--service/src/main/java/market/guess/service/infrastructure/repository/Repository.java (renamed from service/src/main/java/market/guess/service/repository/Repository.java)2
-rw-r--r--service/src/main/java/market/guess/service/infrastructure/repository/UserRepository.java (renamed from service/src/main/java/market/guess/service/repository/UserRepository.java)2
-rw-r--r--service/src/main/java/market/guess/service/mechanism/LmsrTradingMechanism.java55
-rw-r--r--service/src/main/java/market/guess/service/mechanism/OrderBookTradingMechanism.java139
-rw-r--r--service/src/main/java/market/guess/service/mechanism/TradeExecution.java5
-rw-r--r--service/src/main/java/market/guess/service/mechanism/TradingMechanism.java10
-rw-r--r--service/src/test/java/market/guess/service/mechanism/LmsrTradingMechanismTest.java37
-rw-r--r--service/src/test/java/market/guess/service/mechanism/OrderBookTradingMechanismTest.java51
21 files changed, 358 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;
10import market.guess.api.PurchaseReceiptDTO; 10import market.guess.api.PurchaseReceiptDTO;
11import market.guess.exception.GuessMarketException; 11import market.guess.exception.GuessMarketException;
12import market.guess.service.domain.Event; 12import market.guess.service.domain.Event;
13import market.guess.service.infrastructure.MarketContext;
13import market.guess.service.infrastructure.provider.Loader; 14import market.guess.service.infrastructure.provider.Loader;
14import market.guess.service.repository.EventRepository;
15import market.guess.service.repository.UserRepository;
16 15
17public final class LocalGuessMarketContext implements GuessMarketContext { 16public 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
9public final class Account { 9public 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;
4import java.time.Instant; 4import java.time.Instant;
5import market.guess.api.LedgerType; 5import market.guess.api.LedgerType;
6 6
7public record LedgerEntry( 7public 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 @@
1package market.guess.service.domain; 1package market.guess.service.domain;
2 2
3public final class Option { 3public 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 @@
1package market.guess.service.infrastructure;
2
3import com.google.gson.Gson;
4import com.google.gson.GsonBuilder;
5import java.io.IOException;
6import java.nio.charset.StandardCharsets;
7import java.nio.file.Files;
8import java.nio.file.Paths;
9import java.time.Instant;
10import market.guess.service.infrastructure.adapter.InstantTypeAdapter;
11import market.guess.service.infrastructure.repository.EventRepository;
12import market.guess.service.infrastructure.repository.UserRepository;
13
14public 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;
9import market.guess.service.infrastructure.mapper.v1.EventMapperV1; 9import market.guess.service.infrastructure.mapper.v1.EventMapperV1;
10import market.guess.service.infrastructure.provider.LoadValidator; 10import market.guess.service.infrastructure.provider.LoadValidator;
11import market.guess.service.infrastructure.provider.Loader; 11import market.guess.service.infrastructure.provider.Loader;
12import market.guess.service.infrastructure.repository.EventRepository;
13import market.guess.service.infrastructure.repository.UserRepository;
12import market.guess.service.model.v1.GuessMarket; 14import market.guess.service.model.v1.GuessMarket;
13import market.guess.service.repository.EventRepository;
14import market.guess.service.repository.UserRepository;
15 15
16public final class XMLLoaderV1 implements Loader { 16public 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 @@
1package market.guess.service.repository; 1package market.guess.service.infrastructure.repository;
2 2
3import market.guess.service.domain.Event; 3import 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 @@
1package market.guess.service.repository; 1package market.guess.service.infrastructure.repository;
2 2
3import java.util.ArrayList; 3import java.util.ArrayList;
4import java.util.Collections; 4import 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 @@
1package market.guess.service.repository; 1package market.guess.service.infrastructure.repository;
2 2
3import java.util.ArrayList; 3import java.util.ArrayList;
4import java.util.Collections; 4import 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 @@
1package market.guess.service.repository; 1package market.guess.service.infrastructure.repository;
2 2
3import java.util.List; 3import java.util.List;
4import java.util.Optional; 4import 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 @@
1package market.guess.service.repository; 1package market.guess.service.infrastructure.repository;
2 2
3import market.guess.service.domain.User; 3import 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 @@
1package market.guess.service.mechanism; 1package market.guess.service.mechanism;
2 2
3import java.math.BigDecimal; 3import java.math.BigDecimal;
4import java.util.Arrays;
4import market.guess.api.MechanismType; 5import market.guess.api.MechanismType;
6import market.guess.service.domain.BigDecimalOptions;
5 7
6public final class LmsrTradingMechanism implements TradingMechanism { 8public 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 @@
1package market.guess.service.mechanism; 1package market.guess.service.mechanism;
2 2
3import java.math.BigDecimal; 3import java.math.BigDecimal;
4import java.math.RoundingMode;
5import java.util.ArrayList;
6import java.util.List;
4import market.guess.api.MechanismType; 7import market.guess.api.MechanismType;
8import market.guess.service.domain.BigDecimalOptions;
5 9
6public final class OrderBookTradingMechanism implements TradingMechanism { 10public 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 @@
1package market.guess.service.mechanism;
2
3import java.math.BigDecimal;
4
5public 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;
3import java.math.BigDecimal; 3import java.math.BigDecimal;
4import market.guess.api.MechanismType; 4import market.guess.api.MechanismType;
5 5
6public interface TradingMechanism { 6public 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 @@
1package market.guess.service.mechanism;
2
3import static org.junit.Assert.assertArrayEquals;
4import static org.junit.Assert.assertEquals;
5
6import java.math.BigDecimal;
7import org.junit.Test;
8
9public 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 @@
1package market.guess.service.mechanism;
2
3import static org.junit.Assert.assertEquals;
4
5import java.math.BigDecimal;
6import market.guess.service.mechanism.OrderBookTradingMechanism.Side;
7import org.junit.Test;
8
9public 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}