diff options
| author | Kostya <mail@sartin.in> | 2026-09-06 15:42:59 +0300 |
|---|---|---|
| committer | Kostya <mail@sartin.in> | 2026-09-06 15:42:59 +0300 |
| commit | 1cf436ccd3efd45d67749102363b9a7f0d242c54 (patch) | |
| tree | 9332676f5a5353172a0c54384f960dd66c105aed /service/src/main/java/market | |
| parent | 5bc7c4ed9d052668af66d3116a6b7afbe62c6eec (diff) | |
| download | guess-market-1cf436ccd3efd45d67749102363b9a7f0d242c54.tar.gz guess-market-1cf436ccd3efd45d67749102363b9a7f0d242c54.tar.xz guess-market-1cf436ccd3efd45d67749102363b9a7f0d242c54.zip | |
service: add ex-2 XML loader and carry order side through matching
Ex-2 documents declare users and their market-maker assignments next
to the events, and the loader has to reject files that are
schema-valid but logically inconsistent, which the v1 loader has no
notion of.
Add XMLLoaderV2, which checks the extension, unmarshals the document,
runs it through XMLValidatorV2 and only then replaces the event and
user repositories, so a rejected file leaves the previous catalog
intact. The validator enforces unique positive event ids, non-empty
names and descriptions, commission within 0..90 with a known type, at
least two distinct options, a positive LMSR b or order-book d with
non-negative initial inventory, unique user names, positive initial
cash, and exactly one existing event per market-maker reference. Let
EventMapperV2 take the resolved market maker's name so it lands on the
domain event.
Give Order a side and the placing user, and Trade the seller, so an
order book can match a buyer against a seller. Keep the previous
constructors, defaulting to a BUY with no user or seller, so existing
LMSR call sites stay unchanged. Route LocalMatchingEngine through the
mechanism's placeOrder with side, user and limit price instead of
buy(), and have cancelOrder take the order to cancel.
Also add an isBlocked query to User and Ledger for accounts that went
negative.
Diffstat (limited to 'service/src/main/java/market')
9 files changed, 391 insertions, 8 deletions
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/mapper/v2/EventMapperV2.java b/service/src/main/java/market/guess/service/catalog/infrastructure/mapper/v2/EventMapperV2.java index bcdce94..dc13d16 100644 --- a/service/src/main/java/market/guess/service/catalog/infrastructure/mapper/v2/EventMapperV2.java +++ b/service/src/main/java/market/guess/service/catalog/infrastructure/mapper/v2/EventMapperV2.java | |||
| @@ -18,6 +18,10 @@ public final class EventMapperV2 implements Mapper<GMEvent, Event> { | |||
| 18 | 18 | ||
| 19 | @Override | 19 | @Override |
| 20 | public Event toDomain(GMEvent source) { | 20 | public Event toDomain(GMEvent source) { |
| 21 | return toDomain(source, ""); | ||
| 22 | } | ||
| 23 | |||
| 24 | public Event toDomain(GMEvent source, String marketMaker) { | ||
| 21 | var options = getOptions(source); | 25 | var options = getOptions(source); |
| 22 | 26 | ||
| 23 | return new Event( | 27 | return new Event( |
| @@ -30,7 +34,7 @@ public final class EventMapperV2 implements Mapper<GMEvent, Event> { | |||
| 30 | getMechanism(source, options.size()), | 34 | getMechanism(source, options.size()), |
| 31 | EventStatus.DRAFT, | 35 | EventStatus.DRAFT, |
| 32 | options, | 36 | options, |
| 33 | ""); | 37 | marketMaker != null ? marketMaker : ""); |
| 34 | } | 38 | } |
| 35 | 39 | ||
| 36 | private static String getEventKey(GMEvent event) { | 40 | private static String getEventKey(GMEvent event) { |
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v2/XMLLoaderV2.java b/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v2/XMLLoaderV2.java new file mode 100644 index 0000000..7ca141f --- /dev/null +++ b/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v2/XMLLoaderV2.java | |||
| @@ -0,0 +1,119 @@ | |||
| 1 | package market.guess.service.catalog.infrastructure.provider.v2; | ||
| 2 | |||
| 3 | import jakarta.xml.bind.JAXBContext; | ||
| 4 | import jakarta.xml.bind.JAXBException; | ||
| 5 | import java.math.BigDecimal; | ||
| 6 | import java.nio.file.Files; | ||
| 7 | import java.nio.file.Path; | ||
| 8 | import java.util.ArrayList; | ||
| 9 | import java.util.HashMap; | ||
| 10 | import java.util.Map; | ||
| 11 | import market.guess.api.CatalogContext; | ||
| 12 | import market.guess.api.LoadResult; | ||
| 13 | import market.guess.api.Result; | ||
| 14 | import market.guess.service.catalog.infrastructure.mapper.v2.EventMapperV2; | ||
| 15 | import market.guess.service.catalog.infrastructure.provider.Loader; | ||
| 16 | import market.guess.service.catalog.infrastructure.repository.EventRepository; | ||
| 17 | import market.guess.service.catalog.infrastructure.repository.UserRepository; | ||
| 18 | import market.guess.service.catalog.model.v2.Event; | ||
| 19 | import market.guess.service.catalog.model.v2.GMEvent; | ||
| 20 | import market.guess.service.catalog.model.v2.GMUser; | ||
| 21 | import market.guess.service.catalog.model.v2.GuessMarket; | ||
| 22 | import market.guess.service.domain.User; | ||
| 23 | |||
| 24 | public final class XMLLoaderV2 implements Loader { | ||
| 25 | private final JAXBContext contextV2; | ||
| 26 | private final EventRepository eventRepository; | ||
| 27 | private final UserRepository userRepository; | ||
| 28 | private final EventMapperV2 mapperV2; | ||
| 29 | private final XMLValidatorV2 validatorV2; | ||
| 30 | |||
| 31 | public XMLLoaderV2( | ||
| 32 | EventRepository eventRepository, | ||
| 33 | UserRepository userRepository, | ||
| 34 | EventMapperV2 mapperV2, | ||
| 35 | XMLValidatorV2 validatorV2) { | ||
| 36 | this.eventRepository = eventRepository; | ||
| 37 | this.userRepository = userRepository; | ||
| 38 | this.mapperV2 = mapperV2; | ||
| 39 | this.validatorV2 = validatorV2; | ||
| 40 | try { | ||
| 41 | this.contextV2 = JAXBContext.newInstance(GuessMarket.class); | ||
| 42 | } catch (JAXBException e) { | ||
| 43 | throw new IllegalStateException("Failed to initialize JAXB context: " + e.getMessage(), e); | ||
| 44 | } | ||
| 45 | } | ||
| 46 | |||
| 47 | @Override | ||
| 48 | public Result<LoadResult> load(Path path) { | ||
| 49 | if (path == null) { | ||
| 50 | return Result.error("INVALID_PATH", "File path cannot be null."); | ||
| 51 | } | ||
| 52 | |||
| 53 | if (!Files.isRegularFile(path)) { | ||
| 54 | return Result.error("FILE_NOT_FOUND", "No such file: " + path); | ||
| 55 | } | ||
| 56 | |||
| 57 | if (path.getFileName() == null | ||
| 58 | || !path.getFileName().toString().toLowerCase().endsWith(".xml")) { | ||
| 59 | return Result.error("NOT_XML", "The file must have a .xml extension."); | ||
| 60 | } | ||
| 61 | |||
| 62 | try (var stream = Files.newInputStream(path)) { | ||
| 63 | var seed = (GuessMarket) contextV2.createUnmarshaller().unmarshal(stream); | ||
| 64 | |||
| 65 | var validation = validatorV2.validate(seed); | ||
| 66 | if (!validation.isSuccess()) { | ||
| 67 | return Result.error(validation.getMessage(), validation.getDetails()); | ||
| 68 | } | ||
| 69 | |||
| 70 | // Map market makers | ||
| 71 | Map<Integer, String> mmByEventId = new HashMap<>(); | ||
| 72 | for (GMUser u : seed.getGMUsers().getGMUser()) { | ||
| 73 | if (u.getGMMarketMaker() != null && u.getGMMarketMaker().getEvent() != null) { | ||
| 74 | for (Event mmEvent : u.getGMMarketMaker().getEvent()) { | ||
| 75 | mmByEventId.put(mmEvent.getId(), u.getName()); | ||
| 76 | } | ||
| 77 | } | ||
| 78 | } | ||
| 79 | |||
| 80 | var domainEvents = new ArrayList<market.guess.service.domain.Event>(); | ||
| 81 | for (GMEvent e : seed.getGMEvents().getGMEvent()) { | ||
| 82 | String mm = mmByEventId.getOrDefault(e.getId(), ""); | ||
| 83 | domainEvents.add(mapperV2.toDomain(e, mm)); | ||
| 84 | } | ||
| 85 | |||
| 86 | var domainUsers = new ArrayList<User>(); | ||
| 87 | for (GMUser u : seed.getGMUsers().getGMUser()) { | ||
| 88 | domainUsers.add(new User(u.getName(), new BigDecimal(u.getInitialCash()))); | ||
| 89 | } | ||
| 90 | |||
| 91 | // Ingestion succeeds atomically: update repositories | ||
| 92 | eventRepository.clear(); | ||
| 93 | userRepository.clear(); | ||
| 94 | |||
| 95 | for (var u : domainUsers) { | ||
| 96 | userRepository.add(u); | ||
| 97 | } | ||
| 98 | |||
| 99 | for (var e : domainEvents) { | ||
| 100 | eventRepository.add(e); | ||
| 101 | } | ||
| 102 | |||
| 103 | return Result.ok(new LoadResult(path.toString(), eventRepository.getAll().size())); | ||
| 104 | } catch (JAXBException e) { | ||
| 105 | var cause = | ||
| 106 | e.getLinkedException() != null ? e.getLinkedException().getMessage() : e.getMessage(); | ||
| 107 | return Result.error( | ||
| 108 | "XML_PARSE_ERROR", | ||
| 109 | "The XML file is malformed or invalid: " + (cause != null ? cause : e.getMessage())); | ||
| 110 | } catch (Exception e) { | ||
| 111 | return Result.error("UNREADABLE", "The file could not be read: " + e.getMessage()); | ||
| 112 | } | ||
| 113 | } | ||
| 114 | |||
| 115 | @Override | ||
| 116 | public Result<LoadResult> seed(CatalogContext context) { | ||
| 117 | throw new UnsupportedOperationException("Unimplemented method 'seed'"); | ||
| 118 | } | ||
| 119 | } | ||
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v2/XMLValidatorV2.java b/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v2/XMLValidatorV2.java new file mode 100644 index 0000000..0146025 --- /dev/null +++ b/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v2/XMLValidatorV2.java | |||
| @@ -0,0 +1,219 @@ | |||
| 1 | package market.guess.service.catalog.infrastructure.provider.v2; | ||
| 2 | |||
| 3 | import java.util.HashMap; | ||
| 4 | import java.util.HashSet; | ||
| 5 | import market.guess.api.Result; | ||
| 6 | import market.guess.service.catalog.model.v2.Event; | ||
| 7 | import market.guess.service.catalog.model.v2.GMEvent; | ||
| 8 | import market.guess.service.catalog.model.v2.GMLMSR; | ||
| 9 | import market.guess.service.catalog.model.v2.GMOrderBook; | ||
| 10 | import market.guess.service.catalog.model.v2.GMUser; | ||
| 11 | import market.guess.service.catalog.model.v2.GuessMarket; | ||
| 12 | |||
| 13 | public final class XMLValidatorV2 { | ||
| 14 | private static final int MIN_COMMISSION = 0; | ||
| 15 | private static final int MAX_COMMISSION = 90; | ||
| 16 | |||
| 17 | public Result<Void> validate(GuessMarket seed) { | ||
| 18 | if (seed == null) { | ||
| 19 | return Result.error("INVALID_XML", "The XML file contains no data."); | ||
| 20 | } | ||
| 21 | |||
| 22 | if (seed.getGMEvents() == null || seed.getGMEvents().getGMEvent() == null) { | ||
| 23 | return Result.error("INVALID_XML", "The XML file contains no GM-events element."); | ||
| 24 | } | ||
| 25 | |||
| 26 | var events = seed.getGMEvents().getGMEvent(); | ||
| 27 | if (events.isEmpty()) { | ||
| 28 | return Result.error("EMPTY_EVENTS", "The XML file does not contain any events."); | ||
| 29 | } | ||
| 30 | |||
| 31 | var seenEventIds = new HashSet<Integer>(); | ||
| 32 | |||
| 33 | for (GMEvent e : events) { | ||
| 34 | // 1. Event ID must be positive integer (> 0) | ||
| 35 | if (e.getId() <= 0) { | ||
| 36 | return Result.error( | ||
| 37 | "INVALID_ID", "Event ID must be a positive integer (> 0), found: " + e.getId()); | ||
| 38 | } | ||
| 39 | if (!seenEventIds.add(e.getId())) { | ||
| 40 | return Result.error( | ||
| 41 | "DUPLICATE_ID", | ||
| 42 | "Event ID " + e.getId() + " appears more than once. Every event ID must be unique."); | ||
| 43 | } | ||
| 44 | |||
| 45 | // 2. Event name must not be empty | ||
| 46 | if (e.getName() == null || e.getName().isBlank()) { | ||
| 47 | return Result.error("MISSING_NAME", "Event ID " + e.getId() + " has an empty name."); | ||
| 48 | } | ||
| 49 | |||
| 50 | // 3. Event description must not be empty | ||
| 51 | if (e.getDescription() == null || e.getDescription().isBlank()) { | ||
| 52 | return Result.error( | ||
| 53 | "MISSING_DESCRIPTION", "Event ID " + e.getId() + " has an empty description."); | ||
| 54 | } | ||
| 55 | |||
| 56 | // 4. Commission validation | ||
| 57 | if (e.getCommission() == null) { | ||
| 58 | return Result.error( | ||
| 59 | "MISSING_COMMISSION", "Event ID " + e.getId() + " is missing commission."); | ||
| 60 | } | ||
| 61 | var pct = e.getCommission().getValue(); | ||
| 62 | if (pct < MIN_COMMISSION || pct > MAX_COMMISSION) { | ||
| 63 | return Result.error( | ||
| 64 | "COMMISSION_OUT_OF_RANGE", | ||
| 65 | "Event ID " | ||
| 66 | + e.getId() | ||
| 67 | + " commission is " | ||
| 68 | + pct | ||
| 69 | + "%. It must be between " | ||
| 70 | + MIN_COMMISSION | ||
| 71 | + "% and " | ||
| 72 | + MAX_COMMISSION | ||
| 73 | + "%."); | ||
| 74 | } | ||
| 75 | |||
| 76 | var type = e.getCommission().getType(); | ||
| 77 | if (type == null | ||
| 78 | || (!type.equalsIgnoreCase("on-close") && !type.equalsIgnoreCase("on-purchase"))) { | ||
| 79 | return Result.error( | ||
| 80 | "INVALID_COMMISSION_TYPE", | ||
| 81 | "Event ID " | ||
| 82 | + e.getId() | ||
| 83 | + " has invalid commission type '" | ||
| 84 | + type | ||
| 85 | + "'. Must be 'on-close' or 'on-purchase'."); | ||
| 86 | } | ||
| 87 | |||
| 88 | // 5. GM-options validation (at least 2 options) | ||
| 89 | if (e.getGMOptions() == null || e.getGMOptions().getGMOption() == null) { | ||
| 90 | return Result.error("MISSING_OPTIONS", "Event ID " + e.getId() + " is missing GM-options."); | ||
| 91 | } | ||
| 92 | var options = e.getGMOptions().getGMOption(); | ||
| 93 | if (options.size() < 2) { | ||
| 94 | return Result.error( | ||
| 95 | "INVALID_OPTIONS_COUNT", | ||
| 96 | "Event ID " | ||
| 97 | + e.getId() | ||
| 98 | + " must have at least 2 options, but found " | ||
| 99 | + options.size() | ||
| 100 | + "."); | ||
| 101 | } | ||
| 102 | var seenOptions = new HashSet<String>(); | ||
| 103 | for (String opt : options) { | ||
| 104 | if (opt == null || opt.trim().isEmpty()) { | ||
| 105 | return Result.error( | ||
| 106 | "EMPTY_OPTION", "Event ID " + e.getId() + " has an empty option name."); | ||
| 107 | } | ||
| 108 | if (!seenOptions.add(opt.trim().toLowerCase())) { | ||
| 109 | return Result.error( | ||
| 110 | "DUPLICATE_OPTION", | ||
| 111 | "Event ID " + e.getId() + " has duplicate option names: '" + opt.trim() + "'."); | ||
| 112 | } | ||
| 113 | } | ||
| 114 | |||
| 115 | // 6. GM-method validation (LMSR or Order Book) | ||
| 116 | if (e.getGMMethod() == null) { | ||
| 117 | return Result.error("MISSING_METHOD", "Event ID " + e.getId() + " is missing GM-method."); | ||
| 118 | } | ||
| 119 | if (e.getGMMethod().getGMLMSR() instanceof GMLMSR lmsr) { | ||
| 120 | if (lmsr.getB() <= 0) { | ||
| 121 | return Result.error( | ||
| 122 | "INVALID_LIQUIDITY", | ||
| 123 | "Event ID " | ||
| 124 | + e.getId() | ||
| 125 | + " has invalid liquidity b=" | ||
| 126 | + lmsr.getB() | ||
| 127 | + ". b must be a positive integer (> 0)."); | ||
| 128 | } | ||
| 129 | } else if (e.getGMMethod().getGMOrderBook() instanceof GMOrderBook ob) { | ||
| 130 | if (ob.getD() <= 0) { | ||
| 131 | return Result.error( | ||
| 132 | "INVALID_DENOMINATOR", | ||
| 133 | "Event ID " | ||
| 134 | + e.getId() | ||
| 135 | + " has invalid denominator d=" | ||
| 136 | + ob.getD() | ||
| 137 | + ". d must be a positive integer (> 0)."); | ||
| 138 | } | ||
| 139 | if (ob.getInitial() < 0) { | ||
| 140 | return Result.error( | ||
| 141 | "INVALID_INITIAL", | ||
| 142 | "Event ID " | ||
| 143 | + e.getId() | ||
| 144 | + " has invalid initial inventory: " | ||
| 145 | + ob.getInitial() | ||
| 146 | + ". Must be >= 0."); | ||
| 147 | } | ||
| 148 | } else { | ||
| 149 | return Result.error( | ||
| 150 | "INVALID_METHOD", | ||
| 151 | "Event ID " + e.getId() + " has unknown or missing trading method."); | ||
| 152 | } | ||
| 153 | } | ||
| 154 | |||
| 155 | // 7. Users validation | ||
| 156 | if (seed.getGMUsers() == null || seed.getGMUsers().getGMUser() == null) { | ||
| 157 | return Result.error("MISSING_USERS", "The XML file contains no GM-users element."); | ||
| 158 | } | ||
| 159 | var users = seed.getGMUsers().getGMUser(); | ||
| 160 | if (users.isEmpty()) { | ||
| 161 | return Result.error("EMPTY_USERS", "The XML file does not contain any users."); | ||
| 162 | } | ||
| 163 | |||
| 164 | var seenUserNames = new HashSet<String>(); | ||
| 165 | var mmByEventId = new HashMap<Integer, String>(); | ||
| 166 | for (GMUser u : users) { | ||
| 167 | if (u.getName() == null || u.getName().isBlank()) { | ||
| 168 | return Result.error("MISSING_USER_NAME", "A user has an empty name."); | ||
| 169 | } | ||
| 170 | if (!seenUserNames.add(u.getName().trim().toLowerCase())) { | ||
| 171 | return Result.error( | ||
| 172 | "DUPLICATE_USER", "Duplicate user name found: '" + u.getName().trim() + "'."); | ||
| 173 | } | ||
| 174 | if (u.getInitialCash() <= 0) { | ||
| 175 | return Result.error( | ||
| 176 | "INVALID_INITIAL_CASH", | ||
| 177 | "User '" | ||
| 178 | + u.getName() | ||
| 179 | + "' initial cash must be greater than 0, found: " | ||
| 180 | + u.getInitialCash()); | ||
| 181 | } | ||
| 182 | if (u.getGMMarketMaker() != null && u.getGMMarketMaker().getEvent() != null) { | ||
| 183 | for (Event mmEvent : u.getGMMarketMaker().getEvent()) { | ||
| 184 | if (!seenEventIds.contains(mmEvent.getId())) { | ||
| 185 | return Result.error( | ||
| 186 | "UNKNOWN_MM_EVENT", | ||
| 187 | "Market maker '" | ||
| 188 | + u.getName() | ||
| 189 | + "' references non-existent event ID " | ||
| 190 | + mmEvent.getId()); | ||
| 191 | } | ||
| 192 | var previous = mmByEventId.putIfAbsent(mmEvent.getId(), u.getName()); | ||
| 193 | if (previous != null) { | ||
| 194 | return Result.error( | ||
| 195 | "DUPLICATE_MM", | ||
| 196 | "Event ID " | ||
| 197 | + mmEvent.getId() | ||
| 198 | + " has more than one market maker: '" | ||
| 199 | + previous | ||
| 200 | + "' and '" | ||
| 201 | + u.getName() | ||
| 202 | + "'. Every event needs exactly one."); | ||
| 203 | } | ||
| 204 | } | ||
| 205 | } | ||
| 206 | } | ||
| 207 | |||
| 208 | // 8. Every event has exactly one market maker (duplicates were rejected above) | ||
| 209 | for (GMEvent e : events) { | ||
| 210 | if (!mmByEventId.containsKey(e.getId())) { | ||
| 211 | return Result.error( | ||
| 212 | "MISSING_MM", | ||
| 213 | "Event ID " + e.getId() + " has no market maker. Every event needs exactly one."); | ||
| 214 | } | ||
| 215 | } | ||
| 216 | |||
| 217 | return Result.ok(); | ||
| 218 | } | ||
| 219 | } | ||
diff --git a/service/src/main/java/market/guess/service/domain/Order.java b/service/src/main/java/market/guess/service/domain/Order.java index 9d54020..89f9fcc 100644 --- a/service/src/main/java/market/guess/service/domain/Order.java +++ b/service/src/main/java/market/guess/service/domain/Order.java | |||
| @@ -3,4 +3,16 @@ package market.guess.service.domain; | |||
| 3 | import java.math.BigDecimal; | 3 | import java.math.BigDecimal; |
| 4 | import java.time.Instant; | 4 | import java.time.Instant; |
| 5 | 5 | ||
| 6 | public record Order(Instant at, Event event, Market market, BigDecimal price, long quantity) {} | 6 | public record Order( |
| 7 | Instant at, | ||
| 8 | Event event, | ||
| 9 | Market market, | ||
| 10 | String side, | ||
| 11 | BigDecimal price, | ||
| 12 | long quantity, | ||
| 13 | String userName) { | ||
| 14 | |||
| 15 | public Order(Instant at, Event event, Market market, BigDecimal price, long quantity) { | ||
| 16 | this(at, event, market, "BUY", price, quantity, null); | ||
| 17 | } | ||
| 18 | } | ||
diff --git a/service/src/main/java/market/guess/service/domain/Trade.java b/service/src/main/java/market/guess/service/domain/Trade.java index 271582b..ac53b01 100644 --- a/service/src/main/java/market/guess/service/domain/Trade.java +++ b/service/src/main/java/market/guess/service/domain/Trade.java | |||
| @@ -11,7 +11,20 @@ public record Trade( | |||
| 11 | String marketName, | 11 | String marketName, |
| 12 | long quantity, | 12 | long quantity, |
| 13 | BigDecimal sharesCost, | 13 | BigDecimal sharesCost, |
| 14 | BigDecimal commission) { | 14 | BigDecimal commission, |
| 15 | String sellerUserName) { | ||
| 16 | |||
| 17 | public Trade( | ||
| 18 | int id, | ||
| 19 | Instant at, | ||
| 20 | String buyerUserName, | ||
| 21 | String marketKey, | ||
| 22 | String marketName, | ||
| 23 | long quantity, | ||
| 24 | BigDecimal sharesCost, | ||
| 25 | BigDecimal commission) { | ||
| 26 | this(id, at, buyerUserName, marketKey, marketName, quantity, sharesCost, commission, null); | ||
| 27 | } | ||
| 15 | 28 | ||
| 16 | public BigDecimal totalPaid() { | 29 | public BigDecimal totalPaid() { |
| 17 | return sharesCost.add(commission); | 30 | return sharesCost.add(commission); |
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 86ba5af..18f751e 100644 --- a/service/src/main/java/market/guess/service/domain/User.java +++ b/service/src/main/java/market/guess/service/domain/User.java | |||
| @@ -30,4 +30,8 @@ public final class User { | |||
| 30 | public Ledger getLedger() { | 30 | public Ledger getLedger() { |
| 31 | return account; | 31 | return account; |
| 32 | } | 32 | } |
| 33 | |||
| 34 | public boolean isBlocked() { | ||
| 35 | return account.getBalance().signum() < 0; | ||
| 36 | } | ||
| 33 | } | 37 | } |
diff --git a/service/src/main/java/market/guess/service/ledger/Ledger.java b/service/src/main/java/market/guess/service/ledger/Ledger.java index 72bf387..0a6f66f 100644 --- a/service/src/main/java/market/guess/service/ledger/Ledger.java +++ b/service/src/main/java/market/guess/service/ledger/Ledger.java | |||
| @@ -9,6 +9,7 @@ import market.guess.service.helpers.BigDecimalOptions; | |||
| 9 | public final class Ledger { | 9 | public final class Ledger { |
| 10 | private final String owner; | 10 | private final String owner; |
| 11 | private BigDecimal balance; | 11 | private BigDecimal balance; |
| 12 | private boolean blocked; | ||
| 12 | private final List<LedgerEntry> entries = new ArrayList<>(); | 13 | private final List<LedgerEntry> entries = new ArrayList<>(); |
| 13 | 14 | ||
| 14 | public Ledger(String owner, BigDecimal initialBalance) { | 15 | public Ledger(String owner, BigDecimal initialBalance) { |
| @@ -25,6 +26,10 @@ public final class Ledger { | |||
| 25 | return balance; | 26 | return balance; |
| 26 | } | 27 | } |
| 27 | 28 | ||
| 29 | public boolean isBlocked() { | ||
| 30 | return blocked; | ||
| 31 | } | ||
| 32 | |||
| 28 | public List<LedgerEntry> getEntries() { | 33 | public List<LedgerEntry> getEntries() { |
| 29 | return Collections.unmodifiableList(entries); | 34 | return Collections.unmodifiableList(entries); |
| 30 | } | 35 | } |
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 22e5332..9241265 100644 --- a/service/src/main/java/market/guess/service/matching/LocalMatchingEngine.java +++ b/service/src/main/java/market/guess/service/matching/LocalMatchingEngine.java | |||
| @@ -16,12 +16,19 @@ public final class LocalMatchingEngine implements MatchingEngine { | |||
| 16 | @Override | 16 | @Override |
| 17 | public List<Trade> match(Order order) { | 17 | public List<Trade> match(Order order) { |
| 18 | var mechanism = order.event().getMechanism(); | 18 | var mechanism = order.event().getMechanism(); |
| 19 | return mechanism.buy(order.event(), order.market(), clock.instant(), order.quantity()); | 19 | return mechanism.placeOrder( |
| 20 | order.event(), | ||
| 21 | order.market(), | ||
| 22 | order.userName(), | ||
| 23 | order.side(), | ||
| 24 | clock.instant(), | ||
| 25 | order.quantity(), | ||
| 26 | order.price()); | ||
| 20 | } | 27 | } |
| 21 | 28 | ||
| 22 | @Override | 29 | @Override |
| 23 | public boolean cancelOrder() { | 30 | public boolean cancelOrder(Order order) { |
| 24 | // TODO Auto-generated method stub | 31 | var mechanism = order.event().getMechanism(); |
| 25 | throw new UnsupportedOperationException("Unimplemented method 'cancelOrder'"); | 32 | return mechanism.cancel(); |
| 26 | } | 33 | } |
| 27 | } | 34 | } |
diff --git a/service/src/main/java/market/guess/service/matching/MatchingEngine.java b/service/src/main/java/market/guess/service/matching/MatchingEngine.java index a8fc0c4..5a6e450 100644 --- a/service/src/main/java/market/guess/service/matching/MatchingEngine.java +++ b/service/src/main/java/market/guess/service/matching/MatchingEngine.java | |||
| @@ -7,5 +7,5 @@ import market.guess.service.domain.Trade; | |||
| 7 | public interface MatchingEngine { | 7 | public interface MatchingEngine { |
| 8 | List<Trade> match(Order order); | 8 | List<Trade> match(Order order); |
| 9 | 9 | ||
| 10 | boolean cancelOrder(); | 10 | boolean cancelOrder(Order order); |
| 11 | } | 11 | } |