diff options
Diffstat (limited to 'service')
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; | |||
| 7 | import java.nio.charset.StandardCharsets; | 7 | import java.nio.charset.StandardCharsets; |
| 8 | import java.nio.file.Files; | 8 | import java.nio.file.Files; |
| 9 | import java.nio.file.Path; | 9 | import java.nio.file.Path; |
| 10 | import java.nio.file.Paths; | ||
| 11 | import java.time.Instant; | 10 | import java.time.Instant; |
| 12 | import java.util.List; | 11 | import java.util.List; |
| 13 | import market.guess.exception.GuessMarketException; | 12 | import market.guess.exception.GuessMarketException; |
| 13 | import market.guess.service.catalog.infrastructure.adapter.EventDeserializer; | ||
| 14 | import market.guess.service.catalog.infrastructure.adapter.InstantTypeAdapter; | 14 | import market.guess.service.catalog.infrastructure.adapter.InstantTypeAdapter; |
| 15 | import market.guess.service.catalog.infrastructure.adapter.LedgerDeserializer; | ||
| 16 | import market.guess.service.catalog.infrastructure.adapter.LmsrTradingMechanismAdapter; | ||
| 17 | import market.guess.service.catalog.infrastructure.adapter.MarketDeserializer; | ||
| 18 | import market.guess.service.catalog.infrastructure.adapter.TradingMechanismAdapter; | ||
| 19 | import market.guess.service.catalog.infrastructure.adapter.UserDeserializer; | ||
| 15 | import market.guess.service.catalog.infrastructure.repository.EventRepository; | 20 | import market.guess.service.catalog.infrastructure.repository.EventRepository; |
| 16 | import market.guess.service.catalog.infrastructure.repository.UserRepository; | 21 | import market.guess.service.catalog.infrastructure.repository.UserRepository; |
| 17 | import market.guess.service.domain.Event; | 22 | import market.guess.service.domain.Event; |
| 23 | import market.guess.service.domain.Market; | ||
| 18 | import market.guess.service.domain.User; | 24 | import market.guess.service.domain.User; |
| 25 | import market.guess.service.ledger.Ledger; | ||
| 26 | import market.guess.service.mechanism.LmsrTradingMechanism; | ||
| 27 | import market.guess.service.mechanism.TradingMechanism; | ||
| 19 | 28 | ||
| 20 | public final class MarketContext { | 29 | public 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 @@ | |||
| 1 | package market.guess.service.catalog.infrastructure.adapter; | ||
| 2 | |||
| 3 | import com.google.gson.JsonDeserializationContext; | ||
| 4 | import com.google.gson.JsonDeserializer; | ||
| 5 | import com.google.gson.JsonElement; | ||
| 6 | import com.google.gson.JsonParseException; | ||
| 7 | import com.google.gson.reflect.TypeToken; | ||
| 8 | import java.lang.reflect.Type; | ||
| 9 | import java.util.List; | ||
| 10 | import market.guess.api.CommissionTiming; | ||
| 11 | import market.guess.model.event.EventStatus; | ||
| 12 | import market.guess.service.domain.Event; | ||
| 13 | import market.guess.service.domain.Market; | ||
| 14 | import market.guess.service.domain.Trade; | ||
| 15 | import market.guess.service.ledger.Ledger; | ||
| 16 | import 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 | */ | ||
| 22 | public 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 @@ | |||
| 1 | package market.guess.service.catalog.infrastructure.adapter; | ||
| 2 | |||
| 3 | import com.google.gson.JsonDeserializationContext; | ||
| 4 | import com.google.gson.JsonDeserializer; | ||
| 5 | import com.google.gson.JsonElement; | ||
| 6 | import com.google.gson.JsonParseException; | ||
| 7 | import com.google.gson.reflect.TypeToken; | ||
| 8 | import java.lang.reflect.Type; | ||
| 9 | import java.util.List; | ||
| 10 | import market.guess.service.helpers.BigDecimalOptions; | ||
| 11 | import market.guess.service.ledger.Ledger; | ||
| 12 | import market.guess.service.ledger.LedgerEntry; | ||
| 13 | |||
| 14 | /** Rebuilds a Ledger through its constructor and public record() method, avoiding reflection on its final fields. */ | ||
| 15 | public 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 @@ | |||
| 1 | package market.guess.service.catalog.infrastructure.adapter; | ||
| 2 | |||
| 3 | import com.google.gson.JsonDeserializationContext; | ||
| 4 | import com.google.gson.JsonDeserializer; | ||
| 5 | import com.google.gson.JsonElement; | ||
| 6 | import com.google.gson.JsonObject; | ||
| 7 | import com.google.gson.JsonParseException; | ||
| 8 | import com.google.gson.JsonSerializationContext; | ||
| 9 | import com.google.gson.JsonSerializer; | ||
| 10 | import java.lang.reflect.Type; | ||
| 11 | import java.time.Clock; | ||
| 12 | import 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 | */ | ||
| 22 | public 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 @@ | |||
| 1 | package market.guess.service.catalog.infrastructure.adapter; | ||
| 2 | |||
| 3 | import com.google.gson.JsonDeserializationContext; | ||
| 4 | import com.google.gson.JsonDeserializer; | ||
| 5 | import com.google.gson.JsonElement; | ||
| 6 | import com.google.gson.JsonParseException; | ||
| 7 | import java.lang.reflect.Type; | ||
| 8 | import market.guess.service.domain.Market; | ||
| 9 | |||
| 10 | /** Rebuilds a Market through its constructor and public mutator, avoiding reflection on its final fields. */ | ||
| 11 | public 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 @@ | |||
| 1 | package market.guess.service.catalog.infrastructure.adapter; | ||
| 2 | |||
| 3 | import com.google.gson.JsonDeserializationContext; | ||
| 4 | import com.google.gson.JsonDeserializer; | ||
| 5 | import com.google.gson.JsonElement; | ||
| 6 | import com.google.gson.JsonParseException; | ||
| 7 | import com.google.gson.JsonSerializationContext; | ||
| 8 | import com.google.gson.JsonSerializer; | ||
| 9 | import java.lang.reflect.Type; | ||
| 10 | import market.guess.api.MechanismType; | ||
| 11 | import market.guess.service.mechanism.LmsrTradingMechanism; | ||
| 12 | import market.guess.service.mechanism.OrderBookTradingMechanism; | ||
| 13 | import 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 | */ | ||
| 19 | public 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 @@ | |||
| 1 | package market.guess.service.catalog.infrastructure.adapter; | ||
| 2 | |||
| 3 | import com.google.gson.JsonDeserializationContext; | ||
| 4 | import com.google.gson.JsonDeserializer; | ||
| 5 | import com.google.gson.JsonElement; | ||
| 6 | import com.google.gson.JsonParseException; | ||
| 7 | import java.lang.reflect.Type; | ||
| 8 | import market.guess.service.domain.User; | ||
| 9 | import 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. */ | ||
| 12 | public 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 @@ | |||
| 1 | package market.guess.service.catalog.infrastructure.provider.v1; | ||
| 2 | |||
| 3 | import java.util.HashSet; | ||
| 4 | import market.guess.api.Result; | ||
| 5 | import market.guess.service.catalog.model.v1.GuessMarket; | ||
| 6 | |||
| 7 | public 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 @@ | |||
| 1 | package market.guess.service.matching; | 1 | package market.guess.service.matching; |
| 2 | 2 | ||
| 3 | import java.time.Clock; | ||
| 3 | import java.util.List; | 4 | import java.util.List; |
| 4 | import market.guess.service.domain.Order; | 5 | import market.guess.service.domain.Order; |
| 5 | import market.guess.service.domain.Trade; | 6 | import market.guess.service.domain.Trade; |
| 6 | 7 | ||
| 7 | public final class LocalMatchingEngine implements MatchingEngine { | 8 | public 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 | ||
| 3 | import java.math.BigDecimal; | 3 | import java.math.BigDecimal; |
| 4 | import java.time.Clock; | 4 | import java.time.Clock; |
| 5 | import java.time.Instant; | ||
| 5 | import java.util.Arrays; | 6 | import java.util.Arrays; |
| 6 | import java.util.List; | 7 | import java.util.List; |
| 7 | import market.guess.api.MechanismType; | 8 | import 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 | ||
| 3 | import java.math.BigDecimal; | 3 | import java.math.BigDecimal; |
| 4 | import java.math.RoundingMode; | 4 | import java.math.RoundingMode; |
| 5 | import java.time.Instant; | ||
| 5 | import java.util.ArrayList; | 6 | import java.util.ArrayList; |
| 6 | import java.util.List; | 7 | import java.util.List; |
| 7 | import market.guess.api.MechanismType; | 8 | import 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 @@ | |||
| 1 | package market.guess.service.mechanism; | 1 | package market.guess.service.mechanism; |
| 2 | 2 | ||
| 3 | import java.math.BigDecimal; | 3 | import java.math.BigDecimal; |
| 4 | import java.time.Instant; | ||
| 4 | import java.util.List; | 5 | import java.util.List; |
| 5 | import market.guess.api.MechanismType; | 6 | import market.guess.api.MechanismType; |
| 6 | import market.guess.service.domain.Event; | 7 | import 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 | } |