aboutsummaryrefslogtreecommitdiffstats
path: root/service/src/main/java
diff options
context:
space:
mode:
authorKostya <mail@sartin.in>2026-08-18 06:52:47 +0000
committerKostya <mail@sartin.in>2026-08-18 06:52:47 +0000
commitd2688a197bd680580699f05e8194f9180e4832fa (patch)
tree0531ef505b0637c9c4bcc17ab6183a6089e26889 /service/src/main/java
parent13a221ffaefc169d028c7eab6f21ac88052d1d40 (diff)
downloadguess-market-d2688a197bd680580699f05e8194f9180e4832fa.tar.gz
guess-market-d2688a197bd680580699f05e8194f9180e4832fa.tar.xz
guess-market-d2688a197bd680580699f05e8194f9180e4832fa.zip
Add XML schema validation, split build scripts by module, rename BuyMenuCommand to PlaceOrderMenuCommand
Introduces XMLValidatorV1 and deserializer/adapter classes for events, ledgers, markets, users, and trading mechanisms. Replaces the monolithic build.bat/run.sh scripts with per-module tools (_util.sh, build.sh, package.sh, test.sh) that compile in dependency order.
Diffstat (limited to 'service/src/main/java')
-rw-r--r--service/src/main/java/market/guess/service/catalog/LocalCatalogContext.java4
-rw-r--r--service/src/main/java/market/guess/service/catalog/infrastructure/MarketContext.java38
-rw-r--r--service/src/main/java/market/guess/service/catalog/infrastructure/adapter/EventDeserializer.java51
-rw-r--r--service/src/main/java/market/guess/service/catalog/infrastructure/adapter/LedgerDeserializer.java28
-rw-r--r--service/src/main/java/market/guess/service/catalog/infrastructure/adapter/LmsrTradingMechanismAdapter.java42
-rw-r--r--service/src/main/java/market/guess/service/catalog/infrastructure/adapter/MarketDeserializer.java20
-rw-r--r--service/src/main/java/market/guess/service/catalog/infrastructure/adapter/TradingMechanismAdapter.java43
-rw-r--r--service/src/main/java/market/guess/service/catalog/infrastructure/adapter/UserDeserializer.java20
-rw-r--r--service/src/main/java/market/guess/service/catalog/infrastructure/mapper/v1/EventMapperV1.java3
-rw-r--r--service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLLoaderV1.java22
-rw-r--r--service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLValidatorV1.java35
-rw-r--r--service/src/main/java/market/guess/service/domain/Event.java34
-rw-r--r--service/src/main/java/market/guess/service/domain/User.java6
-rw-r--r--service/src/main/java/market/guess/service/matching/LocalMatchingEngine.java9
-rw-r--r--service/src/main/java/market/guess/service/mechanism/LmsrTradingMechanism.java23
-rw-r--r--service/src/main/java/market/guess/service/mechanism/OrderBookTradingMechanism.java24
-rw-r--r--service/src/main/java/market/guess/service/mechanism/TradingMechanism.java3
17 files changed, 371 insertions, 34 deletions
diff --git a/service/src/main/java/market/guess/service/catalog/LocalCatalogContext.java b/service/src/main/java/market/guess/service/catalog/LocalCatalogContext.java
index 7a32e62..270aadd 100644
--- a/service/src/main/java/market/guess/service/catalog/LocalCatalogContext.java
+++ b/service/src/main/java/market/guess/service/catalog/LocalCatalogContext.java
@@ -48,7 +48,7 @@ public final class LocalCatalogContext implements CatalogContext {
48 @Override 48 @Override
49 public Result<Void> saveState(Path path) { 49 public Result<Void> saveState(Path path) {
50 try { 50 try {
51 context.save(); 51 context.save(path);
52 } catch (Exception e) { 52 } catch (Exception e) {
53 return Result.error("Failed to save.", e.getMessage()); 53 return Result.error("Failed to save.", e.getMessage());
54 } 54 }
@@ -58,7 +58,7 @@ public final class LocalCatalogContext implements CatalogContext {
58 @Override 58 @Override
59 public Result<LoadResult> restoreState(Path path) { 59 public Result<LoadResult> restoreState(Path path) {
60 try { 60 try {
61 context.load(); 61 context.load(path);
62 } catch (Exception e) { 62 } catch (Exception e) {
63 return Result.error("Failed to load.", e.getMessage()); 63 return Result.error("Failed to load.", e.getMessage());
64 } 64 }
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/MarketContext.java b/service/src/main/java/market/guess/service/catalog/infrastructure/MarketContext.java
index 24521ef..cb2b467 100644
--- a/service/src/main/java/market/guess/service/catalog/infrastructure/MarketContext.java
+++ b/service/src/main/java/market/guess/service/catalog/infrastructure/MarketContext.java
@@ -7,15 +7,24 @@ import java.io.IOException;
7import java.nio.charset.StandardCharsets; 7import java.nio.charset.StandardCharsets;
8import java.nio.file.Files; 8import java.nio.file.Files;
9import java.nio.file.Path; 9import java.nio.file.Path;
10import java.nio.file.Paths;
11import java.time.Instant; 10import java.time.Instant;
12import java.util.List; 11import java.util.List;
13import market.guess.exception.GuessMarketException; 12import market.guess.exception.GuessMarketException;
13import market.guess.service.catalog.infrastructure.adapter.EventDeserializer;
14import market.guess.service.catalog.infrastructure.adapter.InstantTypeAdapter; 14import market.guess.service.catalog.infrastructure.adapter.InstantTypeAdapter;
15import market.guess.service.catalog.infrastructure.adapter.LedgerDeserializer;
16import market.guess.service.catalog.infrastructure.adapter.LmsrTradingMechanismAdapter;
17import market.guess.service.catalog.infrastructure.adapter.MarketDeserializer;
18import market.guess.service.catalog.infrastructure.adapter.TradingMechanismAdapter;
19import market.guess.service.catalog.infrastructure.adapter.UserDeserializer;
15import market.guess.service.catalog.infrastructure.repository.EventRepository; 20import market.guess.service.catalog.infrastructure.repository.EventRepository;
16import market.guess.service.catalog.infrastructure.repository.UserRepository; 21import market.guess.service.catalog.infrastructure.repository.UserRepository;
17import market.guess.service.domain.Event; 22import market.guess.service.domain.Event;
23import market.guess.service.domain.Market;
18import market.guess.service.domain.User; 24import market.guess.service.domain.User;
25import market.guess.service.ledger.Ledger;
26import market.guess.service.mechanism.LmsrTradingMechanism;
27import market.guess.service.mechanism.TradingMechanism;
19 28
20public final class MarketContext { 29public final class MarketContext {
21 record Wrapper(List<Event> events, List<User> users) {} 30 record Wrapper(List<Event> events, List<User> users) {}
@@ -26,6 +35,12 @@ public final class MarketContext {
26 private static final Gson GSON = 35 private static final Gson GSON =
27 new GsonBuilder() 36 new GsonBuilder()
28 .registerTypeAdapter(Instant.class, new InstantTypeAdapter()) 37 .registerTypeAdapter(Instant.class, new InstantTypeAdapter())
38 .registerTypeAdapter(Event.class, new EventDeserializer())
39 .registerTypeAdapter(Market.class, new MarketDeserializer())
40 .registerTypeAdapter(Ledger.class, new LedgerDeserializer())
41 .registerTypeAdapter(TradingMechanism.class, new TradingMechanismAdapter())
42 .registerTypeAdapter(LmsrTradingMechanism.class, new LmsrTradingMechanismAdapter())
43 .registerTypeAdapter(User.class, new UserDeserializer())
29 .setPrettyPrinting() 44 .setPrettyPrinting()
30 .create(); 45 .create();
31 46
@@ -42,23 +57,25 @@ public final class MarketContext {
42 return users; 57 return users;
43 } 58 }
44 59
45 public void save() throws GuessMarketException { 60 public void save(Path path) throws GuessMarketException {
46 var path = Paths.get("test-file.json"); 61 var target = withJsonExtension(path);
47 62
48 try { 63 try {
49 if (path.getParent() != null) Files.createDirectories(path.getParent()); 64 if (target.getParent() != null) Files.createDirectories(target.getParent());
50 Files.writeString( 65 Files.writeString(
51 path, GSON.toJson(new Wrapper(events.getAll(), users.getAll())), StandardCharsets.UTF_8); 66 target,
52 } catch (IOException e) { 67 GSON.toJson(new Wrapper(events.getAll(), users.getAll())),
68 StandardCharsets.UTF_8);
69 } catch (Exception e) {
53 throw new GuessMarketException("Failed to sove the file."); 70 throw new GuessMarketException("Failed to sove the file.");
54 } 71 }
55 } 72 }
56 73
57 public void load() throws GuessMarketException { 74 public void load(Path path) throws GuessMarketException {
58 try { 75 try {
59 var wrapper = 76 var wrapper =
60 GSON.fromJson( 77 GSON.fromJson(
61 Files.readString(Path.of("test-file.json"), StandardCharsets.UTF_8), Wrapper.class); 78 Files.readString(withJsonExtension(path), StandardCharsets.UTF_8), Wrapper.class);
62 events.clear(); 79 events.clear();
63 users.clear(); 80 users.clear();
64 81
@@ -76,4 +93,9 @@ public final class MarketContext {
76 throw new GuessMarketException("Unable the file."); 93 throw new GuessMarketException("Unable the file.");
77 } 94 }
78 } 95 }
96
97 private static Path withJsonExtension(Path path) {
98 var name = path.getFileName().toString();
99 return name.endsWith(".json") ? path : path.resolveSibling(name + ".json");
100 }
79} 101}
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/adapter/EventDeserializer.java b/service/src/main/java/market/guess/service/catalog/infrastructure/adapter/EventDeserializer.java
new file mode 100644
index 0000000..69a8447
--- /dev/null
+++ b/service/src/main/java/market/guess/service/catalog/infrastructure/adapter/EventDeserializer.java
@@ -0,0 +1,51 @@
1package market.guess.service.catalog.infrastructure.adapter;
2
3import com.google.gson.JsonDeserializationContext;
4import com.google.gson.JsonDeserializer;
5import com.google.gson.JsonElement;
6import com.google.gson.JsonParseException;
7import com.google.gson.reflect.TypeToken;
8import java.lang.reflect.Type;
9import java.util.List;
10import market.guess.api.CommissionTiming;
11import market.guess.model.event.EventStatus;
12import market.guess.service.domain.Event;
13import market.guess.service.domain.Market;
14import market.guess.service.domain.Trade;
15import market.guess.service.ledger.Ledger;
16import market.guess.service.mechanism.TradingMechanism;
17
18/**
19 * Rebuilds an Event through its constructor instead of Gson's default reflective field-setting,
20 * since Event's fields are final and reflective mutation of final fields is being phased out.
21 */
22public final class EventDeserializer implements JsonDeserializer<Event> {
23 @Override
24 public Event deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
25 throws JsonParseException {
26 var obj = json.getAsJsonObject();
27 Ledger ledger = context.deserialize(obj.get("ledger"), Ledger.class);
28 List<Market> markets =
29 context.deserialize(obj.get("markets"), new TypeToken<List<Market>>() {}.getType());
30 List<Trade> trades =
31 context.deserialize(obj.get("trades"), new TypeToken<List<Trade>>() {}.getType());
32 var winningOptionKey = obj.get("winningOptionKey");
33
34 return new Event(
35 obj.get("eventKey").getAsString(),
36 obj.get("displayId").getAsInt(),
37 obj.get("name").getAsString(),
38 obj.get("description").getAsString(),
39 obj.get("commissionPercent").getAsInt(),
40 context.deserialize(obj.get("commissionTiming"), CommissionTiming.class),
41 context.deserialize(obj.get("mechanism"), TradingMechanism.class),
42 markets,
43 obj.get("marketMaker").getAsString(),
44 ledger,
45 obj.get("runningId").getAsInt(),
46 trades,
47 context.deserialize(obj.get("state"), EventStatus.class),
48 winningOptionKey == null || winningOptionKey.isJsonNull() ? null : winningOptionKey.getAsString(),
49 obj.get("settlementCommission").getAsBigDecimal());
50 }
51}
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/adapter/LedgerDeserializer.java b/service/src/main/java/market/guess/service/catalog/infrastructure/adapter/LedgerDeserializer.java
new file mode 100644
index 0000000..4b3a892
--- /dev/null
+++ b/service/src/main/java/market/guess/service/catalog/infrastructure/adapter/LedgerDeserializer.java
@@ -0,0 +1,28 @@
1package market.guess.service.catalog.infrastructure.adapter;
2
3import com.google.gson.JsonDeserializationContext;
4import com.google.gson.JsonDeserializer;
5import com.google.gson.JsonElement;
6import com.google.gson.JsonParseException;
7import com.google.gson.reflect.TypeToken;
8import java.lang.reflect.Type;
9import java.util.List;
10import market.guess.service.helpers.BigDecimalOptions;
11import market.guess.service.ledger.Ledger;
12import market.guess.service.ledger.LedgerEntry;
13
14/** Rebuilds a Ledger through its constructor and public record() method, avoiding reflection on its final fields. */
15public final class LedgerDeserializer implements JsonDeserializer<Ledger> {
16 @Override
17 public Ledger deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
18 throws JsonParseException {
19 var obj = json.getAsJsonObject();
20 var ledger = new Ledger(obj.get("owner").getAsString(), BigDecimalOptions.ZERO_MONEY);
21
22 List<LedgerEntry> entries =
23 context.deserialize(obj.get("entries"), new TypeToken<List<LedgerEntry>>() {}.getType());
24 entries.forEach(ledger::record);
25
26 return ledger;
27 }
28}
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/adapter/LmsrTradingMechanismAdapter.java b/service/src/main/java/market/guess/service/catalog/infrastructure/adapter/LmsrTradingMechanismAdapter.java
new file mode 100644
index 0000000..c090cec
--- /dev/null
+++ b/service/src/main/java/market/guess/service/catalog/infrastructure/adapter/LmsrTradingMechanismAdapter.java
@@ -0,0 +1,42 @@
1package market.guess.service.catalog.infrastructure.adapter;
2
3import com.google.gson.JsonDeserializationContext;
4import com.google.gson.JsonDeserializer;
5import com.google.gson.JsonElement;
6import com.google.gson.JsonObject;
7import com.google.gson.JsonParseException;
8import com.google.gson.JsonSerializationContext;
9import com.google.gson.JsonSerializer;
10import java.lang.reflect.Type;
11import java.time.Clock;
12import market.guess.service.mechanism.LmsrTradingMechanism;
13
14/**
15 * Rebuilds an LmsrTradingMechanism through its constructor, avoiding reflection on its final
16 * fields. The clock is a placeholder here; MarketContext reattaches the live one after load.
17 *
18 * <p>Also writes the type discriminator {@link TradingMechanismAdapter} reads, since Gson prefers
19 * a type adapter registered for an object's runtime type over one registered for its declared
20 * (interface) type, so the interface-level adapter never gets a chance to add it here.
21 */
22public final class LmsrTradingMechanismAdapter
23 implements JsonSerializer<LmsrTradingMechanism>, JsonDeserializer<LmsrTradingMechanism> {
24 @Override
25 public JsonElement serialize(
26 LmsrTradingMechanism src, Type typeOfSrc, JsonSerializationContext context) {
27 var json = new JsonObject();
28 json.addProperty(TradingMechanismAdapter.TYPE_FIELD, src.getType().name());
29 json.addProperty("liquidity", src.getLiquidity());
30 json.add("q", context.serialize(src.getQuantities()));
31 return json;
32 }
33
34 @Override
35 public LmsrTradingMechanism deserialize(
36 JsonElement json, Type typeOfT, JsonDeserializationContext context)
37 throws JsonParseException {
38 var obj = json.getAsJsonObject();
39 var q = context.<int[]>deserialize(obj.get("q"), int[].class);
40 return new LmsrTradingMechanism(obj.get("liquidity").getAsInt(), q, Clock.systemUTC());
41 }
42}
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/adapter/MarketDeserializer.java b/service/src/main/java/market/guess/service/catalog/infrastructure/adapter/MarketDeserializer.java
new file mode 100644
index 0000000..6757292
--- /dev/null
+++ b/service/src/main/java/market/guess/service/catalog/infrastructure/adapter/MarketDeserializer.java
@@ -0,0 +1,20 @@
1package market.guess.service.catalog.infrastructure.adapter;
2
3import com.google.gson.JsonDeserializationContext;
4import com.google.gson.JsonDeserializer;
5import com.google.gson.JsonElement;
6import com.google.gson.JsonParseException;
7import java.lang.reflect.Type;
8import market.guess.service.domain.Market;
9
10/** Rebuilds a Market through its constructor and public mutator, avoiding reflection on its final fields. */
11public final class MarketDeserializer implements JsonDeserializer<Market> {
12 @Override
13 public Market deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
14 throws JsonParseException {
15 var obj = json.getAsJsonObject();
16 var market = new Market(obj.get("key").getAsString(), obj.get("name").getAsString());
17 market.addVolume(obj.get("volume").getAsLong());
18 return market;
19 }
20}
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/adapter/TradingMechanismAdapter.java b/service/src/main/java/market/guess/service/catalog/infrastructure/adapter/TradingMechanismAdapter.java
new file mode 100644
index 0000000..ac3da8b
--- /dev/null
+++ b/service/src/main/java/market/guess/service/catalog/infrastructure/adapter/TradingMechanismAdapter.java
@@ -0,0 +1,43 @@
1package market.guess.service.catalog.infrastructure.adapter;
2
3import com.google.gson.JsonDeserializationContext;
4import com.google.gson.JsonDeserializer;
5import com.google.gson.JsonElement;
6import com.google.gson.JsonParseException;
7import com.google.gson.JsonSerializationContext;
8import com.google.gson.JsonSerializer;
9import java.lang.reflect.Type;
10import market.guess.api.MechanismType;
11import market.guess.service.mechanism.LmsrTradingMechanism;
12import market.guess.service.mechanism.OrderBookTradingMechanism;
13import market.guess.service.mechanism.TradingMechanism;
14
15/**
16 * TradingMechanism is a sealed interface, so Gson can't tell which implementation to build from
17 * the JSON alone; this tags the JSON with a type discriminator and dispatches on it.
18 */
19public final class TradingMechanismAdapter
20 implements JsonSerializer<TradingMechanism>, JsonDeserializer<TradingMechanism> {
21 static final String TYPE_FIELD = "mechanismType";
22
23 @Override
24 public JsonElement serialize(
25 TradingMechanism src, Type typeOfSrc, JsonSerializationContext context) {
26 var json = context.serialize(src, src.getClass()).getAsJsonObject();
27 json.addProperty(TYPE_FIELD, src.getType().name());
28 return json;
29 }
30
31 @Override
32 public TradingMechanism deserialize(
33 JsonElement json, Type typeOfT, JsonDeserializationContext context)
34 throws JsonParseException {
35 var obj = json.getAsJsonObject();
36 var type = MechanismType.valueOf(obj.get(TYPE_FIELD).getAsString());
37
38 return switch (type) {
39 case LMSR -> context.deserialize(obj, LmsrTradingMechanism.class);
40 case ORDER_BOOK -> context.deserialize(obj, OrderBookTradingMechanism.class);
41 };
42 }
43}
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/adapter/UserDeserializer.java b/service/src/main/java/market/guess/service/catalog/infrastructure/adapter/UserDeserializer.java
new file mode 100644
index 0000000..e91620b
--- /dev/null
+++ b/service/src/main/java/market/guess/service/catalog/infrastructure/adapter/UserDeserializer.java
@@ -0,0 +1,20 @@
1package market.guess.service.catalog.infrastructure.adapter;
2
3import com.google.gson.JsonDeserializationContext;
4import com.google.gson.JsonDeserializer;
5import com.google.gson.JsonElement;
6import com.google.gson.JsonParseException;
7import java.lang.reflect.Type;
8import market.guess.service.domain.User;
9import market.guess.service.ledger.Ledger;
10
11/** Rebuilds a User through its constructor instead of Gson's default reflective field-setting, since its fields are final. */
12public final class UserDeserializer implements JsonDeserializer<User> {
13 @Override
14 public User deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
15 throws JsonParseException {
16 var obj = json.getAsJsonObject();
17 Ledger account = context.deserialize(obj.get("account"), Ledger.class);
18 return new User(obj.get("name").getAsString(), account);
19 }
20}
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/mapper/v1/EventMapperV1.java b/service/src/main/java/market/guess/service/catalog/infrastructure/mapper/v1/EventMapperV1.java
index 78fd550..f45eb46 100644
--- a/service/src/main/java/market/guess/service/catalog/infrastructure/mapper/v1/EventMapperV1.java
+++ b/service/src/main/java/market/guess/service/catalog/infrastructure/mapper/v1/EventMapperV1.java
@@ -26,8 +26,7 @@ public final class EventMapperV1 implements Mapper<GMEvent, Event> {
26 new LmsrTradingMechanism(source.getGMMethod().getGMLMSR().getB(), options.size()), 26 new LmsrTradingMechanism(source.getGMMethod().getGMLMSR().getB(), options.size()),
27 EventStatus.ACTIVE, 27 EventStatus.ACTIVE,
28 options, 28 options,
29 "Tester"); // ponytail: only user in this console app; swap for a real market-maker 29 "Tester");
30 // concept if multi-user support lands
31 } 30 }
32 31
33 private static String getEventKey(GMEvent event) { 32 private static String getEventKey(GMEvent event) {
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLLoaderV1.java b/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLLoaderV1.java
index 67d0b67..b97114d 100644
--- a/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLLoaderV1.java
+++ b/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLLoaderV1.java
@@ -20,13 +20,18 @@ public final class XMLLoaderV1 implements Loader {
20 private final EventRepository eventRepository; 20 private final EventRepository eventRepository;
21 private final UserRepository userRepository; 21 private final UserRepository userRepository;
22 private final EventMapperV1 mapper; 22 private final EventMapperV1 mapper;
23 private final XMLValidatorV1 validator;
23 24
24 public XMLLoaderV1( 25 public XMLLoaderV1(
25 EventRepository eventRepository, UserRepository userRepository, EventMapperV1 mapper) { 26 EventRepository eventRepository,
27 UserRepository userRepository,
28 EventMapperV1 mapper,
29 XMLValidatorV1 validator) {
26 super(); 30 super();
27 this.eventRepository = eventRepository; 31 this.eventRepository = eventRepository;
28 this.userRepository = userRepository; 32 this.userRepository = userRepository;
29 this.mapper = mapper; 33 this.mapper = mapper;
34 this.validator = validator;
30 try { 35 try {
31 context = JAXBContext.newInstance(GuessMarket.class); 36 context = JAXBContext.newInstance(GuessMarket.class);
32 } catch (JAXBException e) { 37 } catch (JAXBException e) {
@@ -36,14 +41,25 @@ public final class XMLLoaderV1 implements Loader {
36 41
37 @Override 42 @Override
38 public Result<LoadResult> load(Path path) { 43 public Result<LoadResult> load(Path path) {
44 if (!Files.isRegularFile(path)) {
45 return Result.error("FILE_NOT_FOUND", "No such file.");
46 }
47
39 if (path.getFileName() == null 48 if (path.getFileName() == null
40 || !path.getFileName().toString().toLowerCase().endsWith(".xml")) { 49 || !path.getFileName().toString().toLowerCase().endsWith(".xml")) {
41 return Result.error("NOT_XML", "The file must be an XML."); 50 return Result.error("NOT_XML", "The file must be an XML.");
42 } 51 }
43 52
44 try (var stream = Files.newInputStream(path)) { 53 try (var stream = Files.newInputStream(path)) {
45 var doc = (GuessMarket) context.createUnmarshaller().unmarshal(stream); 54 var seed = (GuessMarket) context.createUnmarshaller().unmarshal(stream);
46 for (var event : doc.getGMEvents().getGMEvent()) { 55
56 var validation = validator.validate(seed);
57
58 if (!validation.isSuccess()) {
59 return Result.error(validation.getMessage(), validation.getDetails());
60 }
61
62 for (var event : seed.getGMEvents().getGMEvent()) {
47 eventRepository.add(mapper.toDomain(event)); 63 eventRepository.add(mapper.toDomain(event));
48 } 64 }
49 65
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLValidatorV1.java b/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLValidatorV1.java
new file mode 100644
index 0000000..567558a
--- /dev/null
+++ b/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLValidatorV1.java
@@ -0,0 +1,35 @@
1package market.guess.service.catalog.infrastructure.provider.v1;
2
3import java.util.HashSet;
4import market.guess.api.Result;
5import market.guess.service.catalog.model.v1.GuessMarket;
6
7public final class XMLValidatorV1 {
8 private static final int MIN_COMMISSION = 0;
9 private static final int MAX_COMMISSION = 90;
10
11 public Result<Void> validate(GuessMarket seed) {
12
13 var seenIds = new HashSet<Integer>();
14 var duplicated = new HashSet<Integer>();
15
16 for (var e : seed.getGMEvents().getGMEvent()) {
17
18 if (!seenIds.add(e.getId()) && duplicated.add(e.getId())) {
19 return Result.error(
20 "DUPLICATE_ID",
21 "Event id " + e.getId() + " appears more than once, every event id must be unique.");
22 }
23
24 var pct = e.getComision().getValue();
25 if (pct < MIN_COMMISSION || pct > MAX_COMMISSION) {
26 return Result.error(
27 "COMMISSION_OUT_OF_RANGE",
28 "Commission is %d%%, it must be between %d%% and %d%%."
29 .formatted(pct, MIN_COMMISSION, MAX_COMMISSION));
30 }
31 }
32
33 return Result.ok();
34 }
35}
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 6089aa3..4c78486 100644
--- a/service/src/main/java/market/guess/service/domain/Event.java
+++ b/service/src/main/java/market/guess/service/domain/Event.java
@@ -57,6 +57,40 @@ public final class Event {
57 this.state = status; 57 this.state = status;
58 } 58 }
59 59
60 /** Reconstructs an event with its full accumulated state, e.g. from a saved snapshot. */
61 public Event(
62 String eventKey,
63 int displayId,
64 String name,
65 String description,
66 int commissionPercent,
67 CommissionTiming commissionTiming,
68 TradingMechanism mechanism,
69 List<Market> markets,
70 String marketMaker,
71 Ledger ledger,
72 int runningId,
73 List<Trade> trades,
74 EventStatus state,
75 String winningOptionKey,
76 BigDecimal settlementCommission) {
77 this.eventKey = eventKey;
78 this.displayId = displayId;
79 this.name = name;
80 this.description = description;
81 this.commissionPercent = commissionPercent;
82 this.commissionTiming = commissionTiming;
83 this.mechanism = mechanism;
84 this.markets = markets;
85 this.marketMaker = marketMaker;
86 this.ledger = ledger;
87 this.runningId = runningId;
88 this.trades.addAll(trades);
89 this.state = state;
90 this.winningOptionKey = winningOptionKey;
91 this.settlementCommission = settlementCommission;
92 }
93
60 public TradingMechanism getMechanism() { 94 public TradingMechanism getMechanism() {
61 return mechanism; 95 return mechanism;
62 } 96 }
diff --git a/service/src/main/java/market/guess/service/domain/User.java b/service/src/main/java/market/guess/service/domain/User.java
index a00b1a0..86ba5af 100644
--- a/service/src/main/java/market/guess/service/domain/User.java
+++ b/service/src/main/java/market/guess/service/domain/User.java
@@ -17,6 +17,12 @@ public final class User {
17 this.account = new Ledger(name, initialBalance); 17 this.account = new Ledger(name, initialBalance);
18 } 18 }
19 19
20 /** Reconstructs a user with its full accumulated ledger, e.g. from a saved snapshot. */
21 public User(String name, Ledger account) {
22 this.name = name;
23 this.account = account;
24 }
25
20 public String getName() { 26 public String getName() {
21 return name; 27 return name;
22 } 28 }
diff --git a/service/src/main/java/market/guess/service/matching/LocalMatchingEngine.java b/service/src/main/java/market/guess/service/matching/LocalMatchingEngine.java
index 6a815c8..22e5332 100644
--- a/service/src/main/java/market/guess/service/matching/LocalMatchingEngine.java
+++ b/service/src/main/java/market/guess/service/matching/LocalMatchingEngine.java
@@ -1,15 +1,22 @@
1package market.guess.service.matching; 1package market.guess.service.matching;
2 2
3import java.time.Clock;
3import java.util.List; 4import java.util.List;
4import market.guess.service.domain.Order; 5import market.guess.service.domain.Order;
5import market.guess.service.domain.Trade; 6import market.guess.service.domain.Trade;
6 7
7public final class LocalMatchingEngine implements MatchingEngine { 8public final class LocalMatchingEngine implements MatchingEngine {
9 private final Clock clock;
10
11 public LocalMatchingEngine(Clock clock) {
12 super();
13 this.clock = clock;
14 }
8 15
9 @Override 16 @Override
10 public List<Trade> match(Order order) { 17 public List<Trade> match(Order order) {
11 var mechanism = order.event().getMechanism(); 18 var mechanism = order.event().getMechanism();
12 return mechanism.buy(order.event(), order.market(), order.quantity()); 19 return mechanism.buy(order.event(), order.market(), clock.instant(), order.quantity());
13 } 20 }
14 21
15 @Override 22 @Override
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 87c5661..062a095 100644
--- a/service/src/main/java/market/guess/service/mechanism/LmsrTradingMechanism.java
+++ b/service/src/main/java/market/guess/service/mechanism/LmsrTradingMechanism.java
@@ -2,6 +2,7 @@ package market.guess.service.mechanism;
2 2
3import java.math.BigDecimal; 3import java.math.BigDecimal;
4import java.time.Clock; 4import java.time.Clock;
5import java.time.Instant;
5import java.util.Arrays; 6import java.util.Arrays;
6import java.util.List; 7import java.util.List;
7import market.guess.api.MechanismType; 8import market.guess.api.MechanismType;
@@ -14,17 +15,23 @@ public final class LmsrTradingMechanism implements TradingMechanism {
14 private final int liquidity; 15 private final int liquidity;
15 private final int[] q; 16 private final int[] q;
16 17
17 private final Clock clock;
18
19 public LmsrTradingMechanism(int liquidity, int optionCount) { 18 public LmsrTradingMechanism(int liquidity, int optionCount) {
20 this(liquidity, optionCount, Clock.systemUTC()); 19 this.q = new int[optionCount];
20 this.liquidity = liquidity;
21 } 21 }
22 22
23 public LmsrTradingMechanism(int liquidity, int optionCount, Clock clock) { 23 public LmsrTradingMechanism(int liquidity, int[] q, Clock clock) {
24 super(); 24 super();
25 this.liquidity = liquidity; 25 this.liquidity = liquidity;
26 this.q = new int[optionCount]; 26 this.q = q;
27 this.clock = clock; 27 }
28
29 public int getLiquidity() {
30 return liquidity;
31 }
32
33 public int[] getQuantities() {
34 return q;
28 } 35 }
29 36
30 @Override 37 @Override
@@ -33,7 +40,7 @@ public final class LmsrTradingMechanism implements TradingMechanism {
33 } 40 }
34 41
35 @Override 42 @Override
36 public List<Trade> buy(Event event, Market market, long quantity) { 43 public List<Trade> buy(Event event, Market market, Instant at, long quantity) {
37 var optionIndex = event.getMarkets().indexOf(market); 44 var optionIndex = event.getMarkets().indexOf(market);
38 var before = cost(q); 45 var before = cost(q);
39 46
@@ -46,7 +53,7 @@ public final class LmsrTradingMechanism implements TradingMechanism {
46 return List.of( 53 return List.of(
47 new Trade( 54 new Trade(
48 0, 55 0,
49 clock.instant(), 56 at,
50 null, 57 null,
51 market.getKey(), 58 market.getKey(),
52 market.getName(), 59 market.getName(),
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 4e0dd0a..fd03750 100644
--- a/service/src/main/java/market/guess/service/mechanism/OrderBookTradingMechanism.java
+++ b/service/src/main/java/market/guess/service/mechanism/OrderBookTradingMechanism.java
@@ -2,6 +2,7 @@ package market.guess.service.mechanism;
2 2
3import java.math.BigDecimal; 3import java.math.BigDecimal;
4import java.math.RoundingMode; 4import java.math.RoundingMode;
5import java.time.Instant;
5import java.util.ArrayList; 6import java.util.ArrayList;
6import java.util.List; 7import java.util.List;
7import market.guess.api.MechanismType; 8import market.guess.api.MechanismType;
@@ -18,15 +19,15 @@ public final class OrderBookTradingMechanism implements TradingMechanism {
18 19
19 private static final class RestingOrder { 20 private static final class RestingOrder {
20 private final BigDecimal price; 21 private final BigDecimal price;
21 private int quantity; 22 private long quantity;
22 23
23 private RestingOrder(BigDecimal price, int quantity) { 24 private RestingOrder(BigDecimal price, long quantity) {
24 this.price = price; 25 this.price = price;
25 this.quantity = quantity; 26 this.quantity = quantity;
26 } 27 }
27 } 28 }
28 29
29 private final boolean allowMint; // ponytail: mint deferred — cross-option matching, its own pass 30 private final boolean allowMint;
30 private final int initial; 31 private final int initial;
31 private final int d; 32 private final int d;
32 private final List<List<RestingOrder>> 33 private final List<List<RestingOrder>>
@@ -56,17 +57,17 @@ public final class OrderBookTradingMechanism implements TradingMechanism {
56 } 57 }
57 58
58 @Override 59 @Override
59 public List<Trade> buy(Event event, Market market, long quantity) { 60 public List<Trade> buy(Event event, Market market, Instant at, long quantity) {
60 var optionIndex = event.getMarkets().indexOf(market); 61 var optionIndex = event.getMarkets().indexOf(market);
61 return List.of(match(optionIndex, Side.BUY, (int) quantity, null, false)); 62 return List.of(match(optionIndex, Side.BUY, at, quantity, null, false));
62 } 63 }
63 64
64 /** Priced limit order — not on the shared interface, no engine call site yet. */ 65 /** Priced limit order — not on the shared interface, no engine call site yet. */
65 public Trade placeOrder(int optionIndex, Side side, int quantity, BigDecimal price) { 66 public Trade placeOrder(int optionIndex, Side side, Instant at, long quantity, BigDecimal price) {
66 if (price.signum() < 0 || price.compareTo(BigDecimal.valueOf(d)) > 0) { 67 if (price.signum() < 0 || price.compareTo(BigDecimal.valueOf(d)) > 0) {
67 throw new IllegalArgumentException("Price must be between 0 and " + d); 68 throw new IllegalArgumentException("Price must be between 0 and " + d);
68 } 69 }
69 return match(optionIndex, side, quantity, price, true); 70 return match(optionIndex, side, at, quantity, price, true);
70 } 71 }
71 72
72 @Override 73 @Override
@@ -97,7 +98,12 @@ public final class OrderBookTradingMechanism implements TradingMechanism {
97 } 98 }
98 99
99 private Trade match( 100 private Trade match(
100 int optionIndex, Side side, int quantity, BigDecimal limitPrice, boolean restRemainder) { 101 int optionIndex,
102 Side side,
103 Instant at,
104 long quantity,
105 BigDecimal limitPrice,
106 boolean restRemainder) {
101 var opposing = side == Side.BUY ? asks.get(optionIndex) : bids.get(optionIndex); 107 var opposing = side == Side.BUY ? asks.get(optionIndex) : bids.get(optionIndex);
102 108
103 var filled = 0; 109 var filled = 0;
@@ -127,7 +133,7 @@ public final class OrderBookTradingMechanism implements TradingMechanism {
127 } 133 }
128 134
129 return new Trade( 135 return new Trade(
130 filled, null, null, null, null, filled, BigDecimalOptions.toMoney(proceeds), proceeds); 136 filled, at, null, null, null, filled, BigDecimalOptions.toMoney(proceeds), proceeds);
131 } 137 }
132 138
133 private static boolean crosses(Side side, BigDecimal restingPrice, BigDecimal limitPrice) { 139 private static boolean crosses(Side side, BigDecimal restingPrice, BigDecimal limitPrice) {
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 16a5c6f..6fc5a9a 100644
--- a/service/src/main/java/market/guess/service/mechanism/TradingMechanism.java
+++ b/service/src/main/java/market/guess/service/mechanism/TradingMechanism.java
@@ -1,6 +1,7 @@
1package market.guess.service.mechanism; 1package market.guess.service.mechanism;
2 2
3import java.math.BigDecimal; 3import java.math.BigDecimal;
4import java.time.Instant;
4import java.util.List; 5import java.util.List;
5import market.guess.api.MechanismType; 6import market.guess.api.MechanismType;
6import market.guess.service.domain.Event; 7import market.guess.service.domain.Event;
@@ -11,7 +12,7 @@ public sealed interface TradingMechanism permits LmsrTradingMechanism, OrderBook
11 12
12 MechanismType getType(); 13 MechanismType getType();
13 14
14 List<Trade> buy(Event event, Market market, long quantity); 15 List<Trade> buy(Event event, Market market, Instant at, long quantity);
15 16
16 BigDecimal[] prices(); 17 BigDecimal[] prices();
17} 18}