From d2688a197bd680580699f05e8194f9180e4832fa Mon Sep 17 00:00:00 2001 From: Kostya Date: Tue, 18 Aug 2026 06:52:47 +0000 Subject: 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. --- .../guess/service/catalog/LocalCatalogContext.java | 4 +- .../catalog/infrastructure/MarketContext.java | 38 ++++++++++++---- .../infrastructure/adapter/EventDeserializer.java | 51 ++++++++++++++++++++++ .../infrastructure/adapter/LedgerDeserializer.java | 28 ++++++++++++ .../adapter/LmsrTradingMechanismAdapter.java | 42 ++++++++++++++++++ .../infrastructure/adapter/MarketDeserializer.java | 20 +++++++++ .../adapter/TradingMechanismAdapter.java | 43 ++++++++++++++++++ .../infrastructure/adapter/UserDeserializer.java | 20 +++++++++ .../infrastructure/mapper/v1/EventMapperV1.java | 3 +- .../infrastructure/provider/v1/XMLLoaderV1.java | 22 ++++++++-- .../infrastructure/provider/v1/XMLValidatorV1.java | 35 +++++++++++++++ .../java/market/guess/service/domain/Event.java | 34 +++++++++++++++ .../java/market/guess/service/domain/User.java | 6 +++ .../service/matching/LocalMatchingEngine.java | 9 +++- .../service/mechanism/LmsrTradingMechanism.java | 23 ++++++---- .../mechanism/OrderBookTradingMechanism.java | 24 ++++++---- .../guess/service/mechanism/TradingMechanism.java | 3 +- 17 files changed, 371 insertions(+), 34 deletions(-) create mode 100644 service/src/main/java/market/guess/service/catalog/infrastructure/adapter/EventDeserializer.java create mode 100644 service/src/main/java/market/guess/service/catalog/infrastructure/adapter/LedgerDeserializer.java create mode 100644 service/src/main/java/market/guess/service/catalog/infrastructure/adapter/LmsrTradingMechanismAdapter.java create mode 100644 service/src/main/java/market/guess/service/catalog/infrastructure/adapter/MarketDeserializer.java create mode 100644 service/src/main/java/market/guess/service/catalog/infrastructure/adapter/TradingMechanismAdapter.java create mode 100644 service/src/main/java/market/guess/service/catalog/infrastructure/adapter/UserDeserializer.java create mode 100644 service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLValidatorV1.java (limited to 'service/src/main') 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 { @Override public Result saveState(Path path) { try { - context.save(); + context.save(path); } catch (Exception e) { return Result.error("Failed to save.", e.getMessage()); } @@ -58,7 +58,7 @@ public final class LocalCatalogContext implements CatalogContext { @Override public Result restoreState(Path path) { try { - context.load(); + context.load(path); } catch (Exception e) { return Result.error("Failed to load.", e.getMessage()); } 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; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.time.Instant; import java.util.List; import market.guess.exception.GuessMarketException; +import market.guess.service.catalog.infrastructure.adapter.EventDeserializer; import market.guess.service.catalog.infrastructure.adapter.InstantTypeAdapter; +import market.guess.service.catalog.infrastructure.adapter.LedgerDeserializer; +import market.guess.service.catalog.infrastructure.adapter.LmsrTradingMechanismAdapter; +import market.guess.service.catalog.infrastructure.adapter.MarketDeserializer; +import market.guess.service.catalog.infrastructure.adapter.TradingMechanismAdapter; +import market.guess.service.catalog.infrastructure.adapter.UserDeserializer; import market.guess.service.catalog.infrastructure.repository.EventRepository; import market.guess.service.catalog.infrastructure.repository.UserRepository; import market.guess.service.domain.Event; +import market.guess.service.domain.Market; import market.guess.service.domain.User; +import market.guess.service.ledger.Ledger; +import market.guess.service.mechanism.LmsrTradingMechanism; +import market.guess.service.mechanism.TradingMechanism; public final class MarketContext { record Wrapper(List events, List users) {} @@ -26,6 +35,12 @@ public final class MarketContext { private static final Gson GSON = new GsonBuilder() .registerTypeAdapter(Instant.class, new InstantTypeAdapter()) + .registerTypeAdapter(Event.class, new EventDeserializer()) + .registerTypeAdapter(Market.class, new MarketDeserializer()) + .registerTypeAdapter(Ledger.class, new LedgerDeserializer()) + .registerTypeAdapter(TradingMechanism.class, new TradingMechanismAdapter()) + .registerTypeAdapter(LmsrTradingMechanism.class, new LmsrTradingMechanismAdapter()) + .registerTypeAdapter(User.class, new UserDeserializer()) .setPrettyPrinting() .create(); @@ -42,23 +57,25 @@ public final class MarketContext { return users; } - public void save() throws GuessMarketException { - var path = Paths.get("test-file.json"); + public void save(Path path) throws GuessMarketException { + var target = withJsonExtension(path); try { - if (path.getParent() != null) Files.createDirectories(path.getParent()); + if (target.getParent() != null) Files.createDirectories(target.getParent()); Files.writeString( - path, GSON.toJson(new Wrapper(events.getAll(), users.getAll())), StandardCharsets.UTF_8); - } catch (IOException e) { + target, + GSON.toJson(new Wrapper(events.getAll(), users.getAll())), + StandardCharsets.UTF_8); + } catch (Exception e) { throw new GuessMarketException("Failed to sove the file."); } } - public void load() throws GuessMarketException { + public void load(Path path) throws GuessMarketException { try { var wrapper = GSON.fromJson( - Files.readString(Path.of("test-file.json"), StandardCharsets.UTF_8), Wrapper.class); + Files.readString(withJsonExtension(path), StandardCharsets.UTF_8), Wrapper.class); events.clear(); users.clear(); @@ -76,4 +93,9 @@ public final class MarketContext { throw new GuessMarketException("Unable the file."); } } + + private static Path withJsonExtension(Path path) { + var name = path.getFileName().toString(); + return name.endsWith(".json") ? path : path.resolveSibling(name + ".json"); + } } 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 @@ +package market.guess.service.catalog.infrastructure.adapter; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.List; +import market.guess.api.CommissionTiming; +import market.guess.model.event.EventStatus; +import market.guess.service.domain.Event; +import market.guess.service.domain.Market; +import market.guess.service.domain.Trade; +import market.guess.service.ledger.Ledger; +import market.guess.service.mechanism.TradingMechanism; + +/** + * Rebuilds an Event through its constructor instead of Gson's default reflective field-setting, + * since Event's fields are final and reflective mutation of final fields is being phased out. + */ +public final class EventDeserializer implements JsonDeserializer { + @Override + public Event deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + var obj = json.getAsJsonObject(); + Ledger ledger = context.deserialize(obj.get("ledger"), Ledger.class); + List markets = + context.deserialize(obj.get("markets"), new TypeToken>() {}.getType()); + List trades = + context.deserialize(obj.get("trades"), new TypeToken>() {}.getType()); + var winningOptionKey = obj.get("winningOptionKey"); + + return new Event( + obj.get("eventKey").getAsString(), + obj.get("displayId").getAsInt(), + obj.get("name").getAsString(), + obj.get("description").getAsString(), + obj.get("commissionPercent").getAsInt(), + context.deserialize(obj.get("commissionTiming"), CommissionTiming.class), + context.deserialize(obj.get("mechanism"), TradingMechanism.class), + markets, + obj.get("marketMaker").getAsString(), + ledger, + obj.get("runningId").getAsInt(), + trades, + context.deserialize(obj.get("state"), EventStatus.class), + winningOptionKey == null || winningOptionKey.isJsonNull() ? null : winningOptionKey.getAsString(), + obj.get("settlementCommission").getAsBigDecimal()); + } +} 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 @@ +package market.guess.service.catalog.infrastructure.adapter; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.List; +import market.guess.service.helpers.BigDecimalOptions; +import market.guess.service.ledger.Ledger; +import market.guess.service.ledger.LedgerEntry; + +/** Rebuilds a Ledger through its constructor and public record() method, avoiding reflection on its final fields. */ +public final class LedgerDeserializer implements JsonDeserializer { + @Override + public Ledger deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + var obj = json.getAsJsonObject(); + var ledger = new Ledger(obj.get("owner").getAsString(), BigDecimalOptions.ZERO_MONEY); + + List entries = + context.deserialize(obj.get("entries"), new TypeToken>() {}.getType()); + entries.forEach(ledger::record); + + return ledger; + } +} 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 @@ +package market.guess.service.catalog.infrastructure.adapter; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import java.lang.reflect.Type; +import java.time.Clock; +import market.guess.service.mechanism.LmsrTradingMechanism; + +/** + * Rebuilds an LmsrTradingMechanism through its constructor, avoiding reflection on its final + * fields. The clock is a placeholder here; MarketContext reattaches the live one after load. + * + *

Also writes the type discriminator {@link TradingMechanismAdapter} reads, since Gson prefers + * a type adapter registered for an object's runtime type over one registered for its declared + * (interface) type, so the interface-level adapter never gets a chance to add it here. + */ +public final class LmsrTradingMechanismAdapter + implements JsonSerializer, JsonDeserializer { + @Override + public JsonElement serialize( + LmsrTradingMechanism src, Type typeOfSrc, JsonSerializationContext context) { + var json = new JsonObject(); + json.addProperty(TradingMechanismAdapter.TYPE_FIELD, src.getType().name()); + json.addProperty("liquidity", src.getLiquidity()); + json.add("q", context.serialize(src.getQuantities())); + return json; + } + + @Override + public LmsrTradingMechanism deserialize( + JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + var obj = json.getAsJsonObject(); + var q = context.deserialize(obj.get("q"), int[].class); + return new LmsrTradingMechanism(obj.get("liquidity").getAsInt(), q, Clock.systemUTC()); + } +} 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 @@ +package market.guess.service.catalog.infrastructure.adapter; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import java.lang.reflect.Type; +import market.guess.service.domain.Market; + +/** Rebuilds a Market through its constructor and public mutator, avoiding reflection on its final fields. */ +public final class MarketDeserializer implements JsonDeserializer { + @Override + public Market deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + var obj = json.getAsJsonObject(); + var market = new Market(obj.get("key").getAsString(), obj.get("name").getAsString()); + market.addVolume(obj.get("volume").getAsLong()); + return market; + } +} 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 @@ +package market.guess.service.catalog.infrastructure.adapter; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import java.lang.reflect.Type; +import market.guess.api.MechanismType; +import market.guess.service.mechanism.LmsrTradingMechanism; +import market.guess.service.mechanism.OrderBookTradingMechanism; +import market.guess.service.mechanism.TradingMechanism; + +/** + * TradingMechanism is a sealed interface, so Gson can't tell which implementation to build from + * the JSON alone; this tags the JSON with a type discriminator and dispatches on it. + */ +public final class TradingMechanismAdapter + implements JsonSerializer, JsonDeserializer { + static final String TYPE_FIELD = "mechanismType"; + + @Override + public JsonElement serialize( + TradingMechanism src, Type typeOfSrc, JsonSerializationContext context) { + var json = context.serialize(src, src.getClass()).getAsJsonObject(); + json.addProperty(TYPE_FIELD, src.getType().name()); + return json; + } + + @Override + public TradingMechanism deserialize( + JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + var obj = json.getAsJsonObject(); + var type = MechanismType.valueOf(obj.get(TYPE_FIELD).getAsString()); + + return switch (type) { + case LMSR -> context.deserialize(obj, LmsrTradingMechanism.class); + case ORDER_BOOK -> context.deserialize(obj, OrderBookTradingMechanism.class); + }; + } +} 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 @@ +package market.guess.service.catalog.infrastructure.adapter; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import java.lang.reflect.Type; +import market.guess.service.domain.User; +import market.guess.service.ledger.Ledger; + +/** Rebuilds a User through its constructor instead of Gson's default reflective field-setting, since its fields are final. */ +public final class UserDeserializer implements JsonDeserializer { + @Override + public User deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + var obj = json.getAsJsonObject(); + Ledger account = context.deserialize(obj.get("account"), Ledger.class); + return new User(obj.get("name").getAsString(), account); + } +} 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 { new LmsrTradingMechanism(source.getGMMethod().getGMLMSR().getB(), options.size()), EventStatus.ACTIVE, options, - "Tester"); // ponytail: only user in this console app; swap for a real market-maker - // concept if multi-user support lands + "Tester"); } 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 { private final EventRepository eventRepository; private final UserRepository userRepository; private final EventMapperV1 mapper; + private final XMLValidatorV1 validator; public XMLLoaderV1( - EventRepository eventRepository, UserRepository userRepository, EventMapperV1 mapper) { + EventRepository eventRepository, + UserRepository userRepository, + EventMapperV1 mapper, + XMLValidatorV1 validator) { super(); this.eventRepository = eventRepository; this.userRepository = userRepository; this.mapper = mapper; + this.validator = validator; try { context = JAXBContext.newInstance(GuessMarket.class); } catch (JAXBException e) { @@ -36,14 +41,25 @@ public final class XMLLoaderV1 implements Loader { @Override public Result load(Path path) { + if (!Files.isRegularFile(path)) { + return Result.error("FILE_NOT_FOUND", "No such file."); + } + if (path.getFileName() == null || !path.getFileName().toString().toLowerCase().endsWith(".xml")) { return Result.error("NOT_XML", "The file must be an XML."); } try (var stream = Files.newInputStream(path)) { - var doc = (GuessMarket) context.createUnmarshaller().unmarshal(stream); - for (var event : doc.getGMEvents().getGMEvent()) { + var seed = (GuessMarket) context.createUnmarshaller().unmarshal(stream); + + var validation = validator.validate(seed); + + if (!validation.isSuccess()) { + return Result.error(validation.getMessage(), validation.getDetails()); + } + + for (var event : seed.getGMEvents().getGMEvent()) { eventRepository.add(mapper.toDomain(event)); } 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 @@ +package market.guess.service.catalog.infrastructure.provider.v1; + +import java.util.HashSet; +import market.guess.api.Result; +import market.guess.service.catalog.model.v1.GuessMarket; + +public final class XMLValidatorV1 { + private static final int MIN_COMMISSION = 0; + private static final int MAX_COMMISSION = 90; + + public Result validate(GuessMarket seed) { + + var seenIds = new HashSet(); + var duplicated = new HashSet(); + + for (var e : seed.getGMEvents().getGMEvent()) { + + if (!seenIds.add(e.getId()) && duplicated.add(e.getId())) { + return Result.error( + "DUPLICATE_ID", + "Event id " + e.getId() + " appears more than once, every event id must be unique."); + } + + var pct = e.getComision().getValue(); + if (pct < MIN_COMMISSION || pct > MAX_COMMISSION) { + return Result.error( + "COMMISSION_OUT_OF_RANGE", + "Commission is %d%%, it must be between %d%% and %d%%." + .formatted(pct, MIN_COMMISSION, MAX_COMMISSION)); + } + } + + return Result.ok(); + } +} 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 { this.state = status; } + /** Reconstructs an event with its full accumulated state, e.g. from a saved snapshot. */ + public Event( + String eventKey, + int displayId, + String name, + String description, + int commissionPercent, + CommissionTiming commissionTiming, + TradingMechanism mechanism, + List markets, + String marketMaker, + Ledger ledger, + int runningId, + List trades, + EventStatus state, + String winningOptionKey, + BigDecimal settlementCommission) { + this.eventKey = eventKey; + this.displayId = displayId; + this.name = name; + this.description = description; + this.commissionPercent = commissionPercent; + this.commissionTiming = commissionTiming; + this.mechanism = mechanism; + this.markets = markets; + this.marketMaker = marketMaker; + this.ledger = ledger; + this.runningId = runningId; + this.trades.addAll(trades); + this.state = state; + this.winningOptionKey = winningOptionKey; + this.settlementCommission = settlementCommission; + } + public TradingMechanism getMechanism() { return mechanism; } 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 { this.account = new Ledger(name, initialBalance); } + /** Reconstructs a user with its full accumulated ledger, e.g. from a saved snapshot. */ + public User(String name, Ledger account) { + this.name = name; + this.account = account; + } + public String getName() { return name; } 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 @@ package market.guess.service.matching; +import java.time.Clock; import java.util.List; import market.guess.service.domain.Order; import market.guess.service.domain.Trade; public final class LocalMatchingEngine implements MatchingEngine { + private final Clock clock; + + public LocalMatchingEngine(Clock clock) { + super(); + this.clock = clock; + } @Override public List match(Order order) { var mechanism = order.event().getMechanism(); - return mechanism.buy(order.event(), order.market(), order.quantity()); + return mechanism.buy(order.event(), order.market(), clock.instant(), order.quantity()); } @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; import java.math.BigDecimal; import java.time.Clock; +import java.time.Instant; import java.util.Arrays; import java.util.List; import market.guess.api.MechanismType; @@ -14,17 +15,23 @@ public final class LmsrTradingMechanism implements TradingMechanism { private final int liquidity; private final int[] q; - private final Clock clock; - public LmsrTradingMechanism(int liquidity, int optionCount) { - this(liquidity, optionCount, Clock.systemUTC()); + this.q = new int[optionCount]; + this.liquidity = liquidity; } - public LmsrTradingMechanism(int liquidity, int optionCount, Clock clock) { + public LmsrTradingMechanism(int liquidity, int[] q, Clock clock) { super(); this.liquidity = liquidity; - this.q = new int[optionCount]; - this.clock = clock; + this.q = q; + } + + public int getLiquidity() { + return liquidity; + } + + public int[] getQuantities() { + return q; } @Override @@ -33,7 +40,7 @@ public final class LmsrTradingMechanism implements TradingMechanism { } @Override - public List buy(Event event, Market market, long quantity) { + public List buy(Event event, Market market, Instant at, long quantity) { var optionIndex = event.getMarkets().indexOf(market); var before = cost(q); @@ -46,7 +53,7 @@ public final class LmsrTradingMechanism implements TradingMechanism { return List.of( new Trade( 0, - clock.instant(), + at, null, market.getKey(), 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; import java.math.BigDecimal; import java.math.RoundingMode; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import market.guess.api.MechanismType; @@ -18,15 +19,15 @@ public final class OrderBookTradingMechanism implements TradingMechanism { private static final class RestingOrder { private final BigDecimal price; - private int quantity; + private long quantity; - private RestingOrder(BigDecimal price, int quantity) { + private RestingOrder(BigDecimal price, long quantity) { this.price = price; this.quantity = quantity; } } - private final boolean allowMint; // ponytail: mint deferred — cross-option matching, its own pass + private final boolean allowMint; private final int initial; private final int d; private final List> @@ -56,17 +57,17 @@ public final class OrderBookTradingMechanism implements TradingMechanism { } @Override - public List buy(Event event, Market market, long quantity) { + public List buy(Event event, Market market, Instant at, long quantity) { var optionIndex = event.getMarkets().indexOf(market); - return List.of(match(optionIndex, Side.BUY, (int) quantity, null, false)); + return List.of(match(optionIndex, Side.BUY, at, quantity, null, false)); } /** Priced limit order — not on the shared interface, no engine call site yet. */ - public Trade placeOrder(int optionIndex, Side side, int quantity, BigDecimal price) { + public Trade placeOrder(int optionIndex, Side side, Instant at, long quantity, BigDecimal price) { if (price.signum() < 0 || price.compareTo(BigDecimal.valueOf(d)) > 0) { throw new IllegalArgumentException("Price must be between 0 and " + d); } - return match(optionIndex, side, quantity, price, true); + return match(optionIndex, side, at, quantity, price, true); } @Override @@ -97,7 +98,12 @@ public final class OrderBookTradingMechanism implements TradingMechanism { } private Trade match( - int optionIndex, Side side, int quantity, BigDecimal limitPrice, boolean restRemainder) { + int optionIndex, + Side side, + Instant at, + long quantity, + BigDecimal limitPrice, + boolean restRemainder) { var opposing = side == Side.BUY ? asks.get(optionIndex) : bids.get(optionIndex); var filled = 0; @@ -127,7 +133,7 @@ public final class OrderBookTradingMechanism implements TradingMechanism { } return new Trade( - filled, null, null, null, null, filled, BigDecimalOptions.toMoney(proceeds), proceeds); + filled, at, null, null, null, filled, BigDecimalOptions.toMoney(proceeds), proceeds); } 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 @@ package market.guess.service.mechanism; import java.math.BigDecimal; +import java.time.Instant; import java.util.List; import market.guess.api.MechanismType; import market.guess.service.domain.Event; @@ -11,7 +12,7 @@ public sealed interface TradingMechanism permits LmsrTradingMechanism, OrderBook MechanismType getType(); - List buy(Event event, Market market, long quantity); + List buy(Event event, Market market, Instant at, long quantity); BigDecimal[] prices(); } -- cgit v1.2.3