summaryrefslogtreecommitdiffstats
path: root/service/src/test/java/market/guess
diff options
context:
space:
mode:
Diffstat (limited to 'service/src/test/java/market/guess')
-rw-r--r--service/src/test/java/market/guess/service/Ex1FullTest.java343
1 files changed, 343 insertions, 0 deletions
diff --git a/service/src/test/java/market/guess/service/Ex1FullTest.java b/service/src/test/java/market/guess/service/Ex1FullTest.java
new file mode 100644
index 0000000..6718142
--- /dev/null
+++ b/service/src/test/java/market/guess/service/Ex1FullTest.java
@@ -0,0 +1,343 @@
1package market.guess.service;
2
3import static org.junit.jupiter.api.Assertions.*;
4
5import java.math.BigDecimal;
6import java.nio.file.Files;
7import java.nio.file.Path;
8import java.time.Instant;
9import java.util.List;
10import market.guess.api.CommissionTiming;
11import market.guess.model.event.EventStatus;
12import market.guess.service.catalog.LocalCatalogContext;
13import market.guess.service.catalog.infrastructure.MarketContext;
14import market.guess.service.catalog.infrastructure.mapper.v1.EventMapperV1;
15import market.guess.service.catalog.infrastructure.provider.v1.XMLLoaderV1;
16import market.guess.service.catalog.infrastructure.provider.v1.XMLValidatorV1;
17import market.guess.service.catalog.infrastructure.repository.InMemoryEventRepository;
18import market.guess.service.catalog.infrastructure.repository.InMemoryUserRepository;
19import market.guess.service.domain.Event;
20import market.guess.service.domain.Market;
21import market.guess.service.domain.User;
22import market.guess.service.fulfillment.LocalFulfillmentContext;
23import market.guess.service.helpers.BigDecimalOptions;
24import market.guess.service.ledger.LedgerContext;
25import market.guess.service.matching.LocalMatchingEngine;
26import market.guess.service.mechanism.LmsrTradingMechanism;
27import market.guess.service.risk.LocalRiskEngine;
28import market.guess.service.settlement.LocalSettlementContext;
29import org.junit.jupiter.api.BeforeEach;
30import org.junit.jupiter.api.Test;
31
32public class Ex1FullTest {
33
34 private InMemoryEventRepository eventRepo;
35 private InMemoryUserRepository userRepo;
36 private MarketContext marketContext;
37 private XMLLoaderV1 loader;
38 private XMLValidatorV1 validator;
39 private LocalCatalogContext catalog;
40 private LocalGuessMarketContext guessMarket;
41 private LedgerContext ledgerContext;
42 private LocalSettlementContext settlementContext;
43
44 @BeforeEach
45 public void setUp() {
46 eventRepo = new InMemoryEventRepository();
47 userRepo = new InMemoryUserRepository();
48 marketContext = new MarketContext(eventRepo, userRepo);
49 validator = new XMLValidatorV1();
50 loader = new XMLLoaderV1(eventRepo, userRepo, new EventMapperV1(), validator);
51 catalog = new LocalCatalogContext(loader, marketContext, eventRepo);
52 ledgerContext = new LedgerContext(userRepo, java.time.Clock.systemUTC());
53 settlementContext = new LocalSettlementContext(ledgerContext);
54 guessMarket =
55 new LocalGuessMarketContext(
56 marketContext,
57 new LocalRiskEngine(),
58 new LocalMatchingEngine(java.time.Clock.systemUTC()),
59 new LocalFulfillmentContext(ledgerContext),
60 settlementContext);
61 }
62
63 @Test
64 public void testLmsrMathCalculations() {
65 var lmsr = new LmsrTradingMechanism(100, 2);
66
67 // Initial prices should be 0.50 and 0.50
68 var initialPrices = lmsr.prices();
69 assertEquals(new BigDecimal("0.50"), initialPrices[0]);
70 assertEquals(new BigDecimal("0.50"), initialPrices[1]);
71
72 // Initial max loss / subsidy = 100 * ln(2) = 69.31
73 assertEquals(new BigDecimal("69.31"), lmsr.maxLoss());
74
75 // Buy 100 shares of option 0 (YES)
76 var dummyEvent =
77 new Event(
78 "1",
79 1,
80 "Test Event",
81 "Desc",
82 0,
83 CommissionTiming.ON_PURCHASE,
84 lmsr,
85 EventStatus.ACTIVE,
86 List.of(new Market("1:0", "YES"), new Market("1:1", "NO")),
87 "Tester");
88
89 var trades = lmsr.buy(dummyEvent, dummyEvent.getMarkets().get(0), Instant.now(), 100);
90 assertEquals(1, trades.size());
91 assertEquals(100, trades.get(0).quantity());
92 assertEquals(new BigDecimal("62.01"), trades.get(0).sharesCost());
93
94 // Price after 100 shares of YES should be ~0.73 and ~0.27
95 var pricesAfter = lmsr.prices();
96 assertEquals(new BigDecimal("0.73"), pricesAfter[0]);
97 assertEquals(new BigDecimal("0.27"), pricesAfter[1]);
98 }
99
100 @Test
101 public void testXmlLoadingAndAtomicity() {
102 var xmlPath = Path.of("test-data/ex-1/multiple.xml");
103 assertTrue(Files.exists(xmlPath), "test-data/ex-1/multiple.xml must exist");
104
105 var result = catalog.loadEvents(xmlPath);
106 assertTrue(result.isSuccess(), "Loading multiple.xml should succeed");
107 assertEquals(3, result.getData().eventsLoaded());
108 assertEquals(3, eventRepo.getAll().size());
109
110 // Re-loading multiple.xml should replace, NOT accumulate (size must remain 3)
111 var reloadResult = catalog.loadEvents(xmlPath);
112 assertTrue(reloadResult.isSuccess());
113 assertEquals(3, eventRepo.getAll().size());
114
115 // Loading non-existent file should fail and preserve 3 events
116 var failResult = catalog.loadEvents(Path.of("non_existent_file.xml"));
117 assertFalse(failResult.isSuccess());
118 assertEquals(3, eventRepo.getAll().size());
119
120 // Loading non-xml file should fail and preserve 3 events
121 var notXmlResult = catalog.loadEvents(Path.of("README.md"));
122 assertFalse(notXmlResult.isSuccess());
123 assertEquals(3, eventRepo.getAll().size());
124 }
125
126 @Test
127 public void testPlaceOrderWithOnPurchaseCommission() {
128 var xmlPath = Path.of("test-data/ex-1/multiple.xml");
129 catalog.loadEvents(xmlPath);
130
131 // Event 1 has 5% commission on-purchase
132 var event1 = eventRepo.get("1").orElseThrow();
133 assertEquals(5, event1.getCommissionPercent());
134 assertEquals(CommissionTiming.ON_PURCHASE, event1.getCommissionTiming());
135
136 var option0Key = event1.getMarkets().get(0).getKey();
137 var buyResult = guessMarket.placeOrder("Tester", "1", option0Key, new BigDecimal("0.50"), 100);
138
139 assertTrue(buyResult.isSuccess());
140 var receipt = buyResult.getData();
141 assertNotNull(receipt);
142 assertEquals("62.01", receipt.sharesCost());
143 assertEquals("3.10", receipt.commissionPaid()); // 5% of 62.01 = 3.1005 -> 3.10
144 assertEquals("65.11", receipt.totalPaid()); // 62.01 + 3.10 = 65.11
145
146 // History should contain 1 trade
147 assertEquals(1, event1.getHistory().size());
148 assertEquals("62.01", event1.getHistory().get(0).pricePaid());
149 }
150
151 @Test
152 public void testSettleEventWithOnCloseCommission() {
153 var xmlPath = Path.of("test-data/ex-1/multiple.xml");
154 catalog.loadEvents(xmlPath);
155
156 // Event 2 has 15% commission on-close, b=50
157 var event2 = eventRepo.get("2").orElseThrow();
158 assertEquals(15, event2.getCommissionPercent());
159 assertEquals(CommissionTiming.ON_CLOSE, event2.getCommissionTiming());
160
161 var option0Key = event2.getMarkets().get(0).getKey();
162 // Buy 50 shares of Argentina
163 var buyResult = guessMarket.placeOrder("Tester", "2", option0Key, new BigDecimal("0.50"), 50);
164 assertTrue(buyResult.isSuccess());
165 var receipt = buyResult.getData();
166 // On purchase commission is 0.00
167 assertEquals("0.00", receipt.commissionPaid());
168
169 // Now settle event choosing option 0 as winner
170 var settleResult = guessMarket.settleEvent("Tester", "2", option0Key);
171 assertTrue(settleResult.isSuccess());
172
173 var detail = settleResult.getData();
174 assertEquals(EventStatus.SETTLED, detail.summary().status());
175 assertEquals(event2.getMarkets().get(0).getName(), detail.settledMarket());
176
177 // 50 winning shares @ $1 gross = $50.00. 15% commission = $7.50. Net payout = $42.50.
178 assertEquals(new BigDecimal("7.50"), event2.getCommission());
179 }
180
181 @Test
182 public void testStateSaveAndRestoreBonusRoundTrip() throws Exception {
183 var xmlPath = Path.of("test-data/ex-1/multiple.xml");
184 catalog.loadEvents(xmlPath);
185
186 // Trade on event 1
187 var optKey = eventRepo.get("1").orElseThrow().getMarkets().get(0).getKey();
188 guessMarket.placeOrder("Tester", "1", optKey, new BigDecimal("0.50"), 100);
189
190 // Save to temp file
191 var tempDir = Files.createTempDirectory("gm_test_save");
192 var savePath = tempDir.resolve("snapshot");
193
194 var saveRes = catalog.saveState(savePath);
195 assertTrue(saveRes.isSuccess(), "Saving state should succeed");
196 assertTrue(Files.exists(tempDir.resolve("snapshot.json")), "snapshot.json must exist");
197
198 // Clear repositories
199 eventRepo.clear();
200 userRepo.clear();
201 assertEquals(0, eventRepo.getAll().size());
202
203 // Restore from path (without extension)
204 var restoreRes = catalog.restoreState(savePath);
205 assertTrue(restoreRes.isSuccess(), "Restoring state should succeed");
206 assertEquals(3, restoreRes.getData().eventsLoaded());
207 assertEquals(3, eventRepo.getAll().size());
208
209 var restoredEvent1 = eventRepo.get("1").orElseThrow();
210 assertEquals(1, restoredEvent1.getTrades().size());
211 assertEquals(100, restoredEvent1.getTrades().get(0).quantity());
212 assertEquals(new BigDecimal("62.01"), restoredEvent1.getTrades().get(0).sharesCost());
213 }
214
215 @Test
216 public void testXmlValidationRules() throws Exception {
217 var tempDir = Files.createTempDirectory("gm_xml_val_test");
218
219 // 1. Commission > 90
220 var xmlInvalidComm =
221 """
222 <?xml version="1.0" encoding="UTF-8"?>
223 <Guess-Market>
224 <GM-events>
225 <GM-event name="High Comm">
226 <id>1</id>
227 <description>Desc</description>
228 <comision type="on-purchase">95</comision>
229 <GM-options>
230 <GM-option>Yes</GM-option>
231 <GM-option>No</GM-option>
232 </GM-options>
233 <GM-method><GM-LMSR><b>100</b></GM-LMSR></GM-method>
234 </GM-event>
235 </GM-events>
236 </Guess-Market>
237 """;
238 var p1 = tempDir.resolve("high_comm.xml");
239 Files.writeString(p1, xmlInvalidComm);
240 var res1 = catalog.loadEvents(p1);
241 assertFalse(res1.isSuccess());
242 assertTrue(res1.getDetails().contains("between 0% and 90%"));
243
244 // 2. Duplicate Event IDs
245 var xmlDupId =
246 """
247 <?xml version="1.0" encoding="UTF-8"?>
248 <Guess-Market>
249 <GM-events>
250 <GM-event name="Event 1">
251 <id>1</id>
252 <description>Desc 1</description>
253 <comision type="on-purchase">10</comision>
254 <GM-options><GM-option>Yes</GM-option><GM-option>No</GM-option></GM-options>
255 <GM-method><GM-LMSR><b>100</b></GM-LMSR></GM-method>
256 </GM-event>
257 <GM-event name="Event 2">
258 <id>1</id>
259 <description>Desc 2</description>
260 <comision type="on-close">10</comision>
261 <GM-options><GM-option>Yes</GM-option><GM-option>No</GM-option></GM-options>
262 <GM-method><GM-LMSR><b>100</b></GM-LMSR></GM-method>
263 </GM-event>
264 </GM-events>
265 </Guess-Market>
266 """;
267 var p2 = tempDir.resolve("dup_id.xml");
268 Files.writeString(p2, xmlDupId);
269 var res2 = catalog.loadEvents(p2);
270 assertFalse(res2.isSuccess());
271 assertTrue(res2.getDetails().contains("Every event ID must be unique"));
272
273 // 3. Three options (must be exactly 2 for Ex1)
274 var xml3Opts =
275 """
276 <?xml version="1.0" encoding="UTF-8"?>
277 <Guess-Market>
278 <GM-events>
279 <GM-event name="Three Options">
280 <id>1</id>
281 <description>Desc</description>
282 <comision type="on-purchase">10</comision>
283 <GM-options>
284 <GM-option>Opt 1</GM-option>
285 <GM-option>Opt 2</GM-option>
286 <GM-option>Opt 3</GM-option>
287 </GM-options>
288 <GM-method><GM-LMSR><b>100</b></GM-LMSR></GM-method>
289 </GM-event>
290 </GM-events>
291 </Guess-Market>
292 """;
293 var p3 = tempDir.resolve("three_opts.xml");
294 Files.writeString(p3, xml3Opts);
295 var res3 = catalog.loadEvents(p3);
296 assertFalse(res3.isSuccess());
297 assertTrue(res3.getDetails().contains("must have exactly 2 options"));
298
299 // 4. Invalid liquidity b <= 0
300 var xmlBadB =
301 """
302 <?xml version="1.0" encoding="UTF-8"?>
303 <Guess-Market>
304 <GM-events>
305 <GM-event name="Bad B">
306 <id>1</id>
307 <description>Desc</description>
308 <comision type="on-purchase">10</comision>
309 <GM-options><GM-option>Yes</GM-option><GM-option>No</GM-option></GM-options>
310 <GM-method><GM-LMSR><b>0</b></GM-LMSR></GM-method>
311 </GM-event>
312 </GM-events>
313 </Guess-Market>
314 """;
315 var p4 = tempDir.resolve("bad_b.xml");
316 Files.writeString(p4, xmlBadB);
317 var res4 = catalog.loadEvents(p4);
318 assertFalse(res4.isSuccess());
319 assertTrue(res4.getDetails().contains("positive integer"));
320
321 // 5. Invalid commission type
322 var xmlBadCommType =
323 """
324 <?xml version="1.0" encoding="UTF-8"?>
325 <Guess-Market>
326 <GM-events>
327 <GM-event name="Bad Comm Type">
328 <id>1</id>
329 <description>Desc</description>
330 <comision type="invalid-type">10</comision>
331 <GM-options><GM-option>Yes</GM-option><GM-option>No</GM-option></GM-options>
332 <GM-method><GM-LMSR><b>100</b></GM-LMSR></GM-method>
333 </GM-event>
334 </GM-events>
335 </Guess-Market>
336 """;
337 var p5 = tempDir.resolve("bad_comm_type.xml");
338 Files.writeString(p5, xmlBadCommType);
339 var res5 = catalog.loadEvents(p5);
340 assertFalse(res5.isSuccess());
341 assertTrue(res5.getDetails().contains("Must be 'on-close' or 'on-purchase'"));
342 }
343}