package market.guess.ui.desktop; import java.math.BigDecimal; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import market.guess.api.AccountContext; import market.guess.api.CatalogContext; import market.guess.api.GuessMarketContext; import market.guess.model.event.EventDetailDTO; import market.guess.model.event.EventStatus; import market.guess.model.event.EventSummaryDTO; import market.guess.model.event.MechanismType; import market.guess.model.ledger.LedgerDTO; import market.guess.ui.desktop.components.AppTab; import market.guess.ui.desktop.components.CommissionFilter; import market.guess.ui.desktop.components.MechanismFilter; import market.guess.ui.desktop.components.ModalDialog; import market.guess.ui.desktop.components.TradeSide; import market.guess.ui.desktop.model.BookRow; import market.guess.ui.desktop.model.ChartPoint; import market.guess.ui.desktop.model.EventData; import market.guess.ui.desktop.model.OrderBook; import market.guess.ui.desktop.model.ParticipantRow; import market.guess.ui.desktop.model.TradeRow; import market.guess.ui.desktop.model.UserData; import market.guess.ui.desktop.model.UserEventRow; import market.guess.ui.desktop.util.Format; /** Reactive UI state backed by JavaFX properties and observable collections. */ public final class AppState { private final CatalogContext catalogContext; private final GuessMarketContext marketContext; private final AccountContext accountContext; private final StringProperty loadedFile = new SimpleStringProperty(null); public final ObservableList events = FXCollections.observableArrayList(); public final ObservableList users = FXCollections.observableArrayList(); private final StringProperty selectedEventId = new SimpleStringProperty(null); private final ObjectProperty selectedEvent = new SimpleObjectProperty<>(); private final StringProperty actingUserName = new SimpleStringProperty(null); private final ObjectProperty actingUser = new SimpleObjectProperty<>(); private final StringProperty selectedUserEventId = new SimpleStringProperty("1"); private final ObjectProperty filterMethod = new SimpleObjectProperty<>(MechanismFilter.ALL); private final ObjectProperty filterStatus = new SimpleObjectProperty<>(null); private final ObjectProperty filterFee = new SimpleObjectProperty<>(CommissionFilter.ALL); private final ObjectProperty tradeSide = new SimpleObjectProperty<>(TradeSide.BUY); private final StringProperty tradeQty = new SimpleStringProperty("50"); private final StringProperty tradePrice = new SimpleStringProperty("0.62"); private final BooleanProperty tradeOptionYes = new SimpleBooleanProperty(true); private final ObjectProperty skin = new SimpleObjectProperty<>(Skin.ROSE_PINE_DAWN); private final BooleanProperty animationsOn = new SimpleBooleanProperty(true); private final ObjectProperty activeTab = new SimpleObjectProperty<>(AppTab.EVENTS); private final ObjectProperty dialog = new SimpleObjectProperty<>(ModalDialog.NONE); private final ObjectProperty createType = new SimpleObjectProperty<>(MechanismType.LMSR); private final BooleanProperty createMintingOn = new SimpleBooleanProperty(true); private final StringProperty resolveSelectedOption = new SimpleStringProperty(null); private final StringProperty toast = new SimpleStringProperty(null); private final StringProperty pendingFile = new SimpleStringProperty(null); public AppState() { this(new ServiceEngine()); } public AppState(ServiceEngine engine) { this(engine.getCatalogContext(), engine.getMarketContext(), engine.getAccountContext()); } public AppState( CatalogContext catalogContext, GuessMarketContext marketContext, AccountContext accountContext) { this.catalogContext = catalogContext; this.marketContext = marketContext; this.accountContext = accountContext; selectedEventId.addListener((obs, oldVal, newVal) -> updateSelectedEvent()); events.addListener((ListChangeListener) c -> updateSelectedEvent()); actingUserName.addListener((obs, oldVal, newVal) -> updateActingUser()); users.addListener((ListChangeListener) c -> updateActingUser()); refreshData(); } public void refreshData() { var allEventsRes = catalogContext.getAllEvents(); var allAccountsRes = accountContext.getAllAccounts(); var newEventList = new ArrayList(); if (allEventsRes != null && allEventsRes.isSuccess() && allEventsRes.getData() != null) { for (var summary : allEventsRes.getData()) { var detailRes = catalogContext.getEvent(summary.key()); EventDetailDTO detail = (detailRes != null && detailRes.isSuccess()) ? detailRes.getData() : null; newEventList.add(toEventData(summary, detail)); } } this.events.setAll(newEventList); var newUserList = new ArrayList(); if (allAccountsRes != null && allAccountsRes.isSuccess() && allAccountsRes.getData() != null) { for (var acc : allAccountsRes.getData()) { newUserList.add(toUserData(acc, newEventList)); } } this.users.setAll(newUserList); updateSelectedEvent(); updateActingUser(); } private EventData toEventData(EventSummaryDTO summary, EventDetailDTO detail) { double contractVal = 0.0; try { if (summary.accountBalance() != null) { contractVal = Double.parseDouble(summary.accountBalance().replace("$", "").replace(",", "").trim()); } } catch (Exception ignored) { } EventData ev = new EventData( summary.key(), summary.displayId(), summary.name(), summary.description(), summary.mechanism(), summary.status(), summary.marketMaker(), summary.commissionPercent(), summary.commissionTiming(), contractVal, summary.mechanism() == MechanismType.LMSR ? 100 : null, summary.mechanism() == MechanismType.ORDER_BOOK ? BigDecimal.ONE : null); if (detail != null) { if (detail.state() != null && detail.state().markets() != null) { var markets = detail.state().markets(); if (markets.size() >= 1) { try { ev.yesPrice = new BigDecimal(markets.get(0).price()); ev.yesShares = (int) Double.parseDouble(markets.get(0).volume()); } catch (Exception ignored) { } } if (markets.size() >= 2) { try { ev.noPrice = new BigDecimal(markets.get(1).price()); ev.noShares = (int) Double.parseDouble(markets.get(1).volume()); } catch (Exception ignored) { } } } if (detail.history() != null) { int idx = 1; for (var t : detail.history()) { boolean isYes = "YES".equalsIgnoreCase(t.optionName()) || (summary.optionNames() != null && !summary.optionNames().isEmpty() && summary.optionNames().get(0).equalsIgnoreCase(t.optionName())); int qty = 0; try { qty = (int) Long.parseLong(t.quantity()); } catch (Exception ignored) { } ev.trades.add( new TradeRow(idx++, t.userName(), t.optionName(), isYes, qty, "$" + t.pricePaid())); } } ev.resolvedOption = detail.settledMarket(); } // Baseline and historical chart points if (detail != null && detail.history() != null && !detail.history().isEmpty()) { var historyChronological = new ArrayList<>(detail.history()); Collections.reverse(historyChronological); long firstT = parseEpochSecond(historyChronological.get(0).at()); if (firstT == 0L) firstT = Instant.now().getEpochSecond(); ev.chart.add(new ChartPoint(firstT - 60, 0.50)); for (var t : historyChronological) { long time = parseEpochSecond(t.at()); if (time == 0L) time = firstT; boolean isYes = "YES".equalsIgnoreCase(t.optionName()) || (summary.optionNames() != null && !summary.optionNames().isEmpty() && summary.optionNames().get(0).equalsIgnoreCase(t.optionName())); double price = 0.50; try { double cost = Double.parseDouble(t.pricePaid().replace("$", "").replace(",", "").trim()); long qty = Long.parseLong(t.quantity()); if (qty > 0) { double unitP = cost / qty; price = isYes ? unitP : Math.max(0.0, 1.0 - unitP); } } catch (Exception ignored) { price = (ev.yesPrice != null) ? ev.yesPrice.doubleValue() : 0.50; } ev.chart.add(new ChartPoint(time, Math.max(0.0, Math.min(1.0, price)))); } } else { long now = Instant.now().getEpochSecond(); double p = (ev.yesPrice != null) ? ev.yesPrice.doubleValue() : 0.50; ev.chart.add(new ChartPoint(now - 60, p)); ev.chart.add(new ChartPoint(now, p)); } // Order books for OB if (ev.type == MechanismType.ORDER_BOOK) { String yesLast = ev.yesPrice != null ? Format.money(ev.yesPrice) : "$0.50"; String noLast = ev.noPrice != null ? Format.money(ev.noPrice) : "$0.50"; ev.yesBook = new OrderBook("YES", true); ev.yesBook.last = yesLast; ev.yesBook.bid = "—"; ev.yesBook.ask = "—"; ev.yesBook.mid = yesLast; ev.yesBook.spread = "—"; ev.noBook = new OrderBook("NO", false); ev.noBook.last = noLast; ev.noBook.bid = "—"; ev.noBook.ask = "—"; ev.noBook.mid = noLast; ev.noBook.spread = "—"; if (detail != null && detail.orderBooks() != null && !detail.orderBooks().isEmpty()) { for (var obDto : detail.orderBooks()) { boolean isYes = "YES".equalsIgnoreCase(obDto.optionKey()) || "YES".equalsIgnoreCase(obDto.optionName()) || obDto.optionKey().endsWith(":0") || obDto.optionKey().equals("0"); OrderBook target = isYes ? ev.yesBook : ev.noBook; if (target != null) { if (obDto.bestBid() != null) target.bid = obDto.bestBid(); if (obDto.bestAsk() != null) target.ask = obDto.bestAsk(); if (obDto.spread() != null) target.spread = obDto.spread(); target.mid = midOf(target.bid, target.ask); target.rows.clear(); if (obDto.orders() != null) { for (var rowDto : obDto.orders()) { target.rows.add( new BookRow( rowDto.side(), rowDto.user(), (int) rowDto.quantity(), rowDto.price(), rowDto.isBid())); } } } } } } // Participants if (detail != null && detail.participants() != null && !detail.participants().isEmpty()) { ev.participants.clear(); for (var p : detail.participants()) { ev.participants.add( new ParticipantRow( p.user(), p.tag(), p.yes(), p.no(), p.value(), p.fees(), p.pnl())); } } else if (ev.participants.isEmpty() && ev.mm != null && !ev.mm.isBlank()) { ev.participants.add(new ParticipantRow(ev.mm, "MM", 0, 0, "$0.00", "$0.00", "$0.00")); } return ev; } private UserData toUserData(LedgerDTO acc, List allEvents) { double bal = 0.0; try { if (acc.balance() != null) { bal = Double.parseDouble(acc.balance().replace("$", "").replace(",", "").trim()); } } catch (Exception ignored) { } boolean isMm = allEvents.stream().anyMatch(e -> e.mm != null && acc.owner().equalsIgnoreCase(e.mm)); String role = isMm ? "Market Maker" : "Trader"; UserData u = new UserData(acc.owner(), role, bal, acc.blocked(), isMm); if (acc.entries() != null && !acc.entries().isEmpty()) { var entries = acc.entries(); long firstT = parseEpochSecond(entries.get(0).at()); if (firstT == 0L) firstT = Instant.now().getEpochSecond(); double firstAmount = 0.0; double firstBalAfter = 0.0; try { firstAmount = Double.parseDouble(entries.get(0).amount().replace("$", "").replace(",", "").trim()); firstBalAfter = Double.parseDouble( entries.get(0).balanceAfter().replace("$", "").replace(",", "").trim()); } catch (Exception ignored) { } double initialBal = firstBalAfter - firstAmount; u.balanceHistory.add(new ChartPoint(firstT - 60, Math.max(0.0, initialBal))); for (var entry : entries) { long t = parseEpochSecond(entry.at()); if (t == 0L) t = firstT; try { double bAfter = Double.parseDouble(entry.balanceAfter().replace("$", "").replace(",", "").trim()); u.balanceHistory.add(new ChartPoint(t, bAfter)); } catch (Exception ignored) { } } } else { long now = Instant.now().getEpochSecond(); u.balanceHistory.add(new ChartPoint(now - 60, bal)); u.balanceHistory.add(new ChartPoint(now, bal)); } for (var ev : allEvents) { String typeStr = ev.type == MechanismType.LMSR ? "LMSR" : "Order Book"; boolean isEvMm = ev.mm != null && acc.owner().equalsIgnoreCase(ev.mm); long yesShares = 0; long noShares = 0; var pOpt = ev.participants.stream().filter(p -> p.user().equalsIgnoreCase(acc.owner())).findFirst(); if (pOpt.isPresent()) { yesShares = pOpt.get().yes(); noShares = pOpt.get().no(); } else { if (isEvMm && ev.type == MechanismType.ORDER_BOOK) { yesShares += 100; noShares += 100; } for (var trade : ev.trades) { if (acc.owner().equalsIgnoreCase(trade.user())) { if (trade.yesOption()) yesShares += trade.shares(); else noShares += trade.shares(); } } } // Spec: P/L is reported once the event is closed; until then there is no realised result. String valStr = ev.status != EventStatus.SETTLED ? "—" : pOpt.isPresent() ? pOpt.get().pnl() : "$0.00"; if (isEvMm) { u.events.add( new UserEventRow( ev.id, ev.name, "Market Maker", typeStr, (int) Math.max(0, yesShares), (int) Math.max(0, noShares), valStr)); } else if (yesShares > 0 || noShares > 0) { u.events.add( new UserEventRow( ev.id, ev.name, "Trader", typeStr, (int) Math.max(0, yesShares), (int) Math.max(0, noShares), valStr)); } } return u; } /** Mid price between best bid and best ask, or "—" when either side of the book is empty. */ static String midOf(String bid, String ask) { try { var b = new BigDecimal(bid.replace("$", "").trim()); var a = new BigDecimal(ask.replace("$", "").trim()); return Format.money(b.add(a).divide(BigDecimal.TWO)); } catch (RuntimeException e) { return "—"; } } public static long parseEpochSecond(String timeStr) { if (timeStr == null || timeStr.isBlank()) return 0L; try { return LocalDateTime.parse(timeStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) .atZone(ZoneId.systemDefault()) .toEpochSecond(); } catch (Exception e) { try { return Instant.parse(timeStr).getEpochSecond(); } catch (Exception ignored) { return 0L; } } } private void updateSelectedEvent() { String id = selectedEventId.get(); EventData ev = (id != null) ? event(id) : null; if (ev == null && !events.isEmpty()) { ev = events.get(0); if (ev != null) { selectedEventId.set(ev.id); } } selectedEvent.set(null); selectedEvent.set(ev); } private void updateActingUser() { String name = actingUserName.get(); UserData u = (name != null) ? user(name) : null; if (u == null && !users.isEmpty()) { u = users.get(0); if (u != null) { actingUserName.set(u.name); } } actingUser.set(null); actingUser.set(u); } public CatalogContext getCatalogContext() { return catalogContext; } public GuessMarketContext getMarketContext() { return marketContext; } public AccountContext getAccountContext() { return accountContext; } public ObservableList getEvents() { return events; } public ObservableList getUsers() { return users; } public StringProperty loadedFileProperty() { return loadedFile; } public String getLoadedFile() { return loadedFile.get(); } public void setLoadedFile(String file) { this.loadedFile.set(file); } public StringProperty selectedEventIdProperty() { return selectedEventId; } public String getSelectedEventId() { return selectedEventId.get(); } public void setSelectedEventId(String id) { this.selectedEventId.set(id); } public ObjectProperty selectedEventProperty() { return selectedEvent; } public EventData selectedEvent() { return selectedEvent.get(); } public EventData getSelectedEvent() { return selectedEvent.get(); } public void setSelectedEvent(EventData e) { this.selectedEvent.set(e); if (e != null) { this.selectedEventId.set(e.id); } } public StringProperty actingUserNameProperty() { return actingUserName; } public String getActingUserName() { return actingUserName.get(); } public void setActingUserName(String name) { this.actingUserName.set(name); } public ObjectProperty actingUserProperty() { return actingUser; } public UserData actingUser() { return actingUser.get(); } public UserData getActingUser() { return actingUser.get(); } public void setActingUser(UserData u) { this.actingUser.set(u); if (u != null) { this.actingUserName.set(u.name); } } public StringProperty selectedUserEventIdProperty() { return selectedUserEventId; } public String getSelectedUserEventId() { return selectedUserEventId.get(); } public void setSelectedUserEventId(String id) { this.selectedUserEventId.set(id); } public ObjectProperty filterMethodProperty() { return filterMethod; } public MechanismFilter getFilterMethod() { return filterMethod.get(); } public void setFilterMethod(MechanismFilter m) { this.filterMethod.set(m); } public ObjectProperty filterStatusProperty() { return filterStatus; } public EventStatus getFilterStatus() { return filterStatus.get(); } public void setFilterStatus(EventStatus s) { this.filterStatus.set(s); } public ObjectProperty filterFeeProperty() { return filterFee; } public CommissionFilter getFilterFee() { return filterFee.get(); } public void setFilterFee(CommissionFilter f) { this.filterFee.set(f); } public ObjectProperty tradeSideProperty() { return tradeSide; } public TradeSide getTradeSide() { return tradeSide.get(); } public void setTradeSide(TradeSide s) { this.tradeSide.set(s); } public StringProperty tradeQtyProperty() { return tradeQty; } public String getTradeQty() { return tradeQty.get(); } public void setTradeQty(String q) { this.tradeQty.set(q); } public StringProperty tradePriceProperty() { return tradePrice; } public String getTradePrice() { return tradePrice.get(); } public void setTradePrice(String p) { this.tradePrice.set(p); } public BooleanProperty tradeOptionYesProperty() { return tradeOptionYes; } public boolean isTradeOptionYes() { return tradeOptionYes.get(); } public void setTradeOptionYes(boolean y) { this.tradeOptionYes.set(y); } public ObjectProperty skinProperty() { return skin; } public Skin getSkin() { return skin.get(); } public void setSkin(Skin s) { this.skin.set(s); } public BooleanProperty animationsOnProperty() { return animationsOn; } public boolean isAnimationsOn() { return animationsOn.get(); } public void setAnimationsOn(boolean a) { this.animationsOn.set(a); } public ObjectProperty activeTabProperty() { return activeTab; } public AppTab getActiveTab() { return activeTab.get(); } public void setActiveTab(AppTab t) { this.activeTab.set(t); } public ObjectProperty dialogProperty() { return dialog; } public ModalDialog getDialog() { return dialog.get(); } public void setDialog(ModalDialog d) { this.dialog.set(d); } public ObjectProperty createTypeProperty() { return createType; } public MechanismType getCreateType() { return createType.get(); } public void setCreateType(MechanismType t) { this.createType.set(t); } public BooleanProperty createMintingOnProperty() { return createMintingOn; } public boolean isCreateMintingOn() { return createMintingOn.get(); } public void setCreateMintingOn(boolean m) { this.createMintingOn.set(m); } public StringProperty resolveSelectedOptionProperty() { return resolveSelectedOption; } public String getResolveSelectedOption() { return resolveSelectedOption.get(); } public void setResolveSelectedOption(String o) { this.resolveSelectedOption.set(o); } public StringProperty toastProperty() { return toast; } public String getToast() { return toast.get(); } public void setToast(String t) { this.toast.set(t); } public StringProperty pendingFileProperty() { return pendingFile; } public String getPendingFile() { return pendingFile.get(); } public void setPendingFile(String f) { this.pendingFile.set(f); } public EventData event(String id) { if (id == null) return null; return events.stream().filter(e -> e.getId().equals(id)).findFirst().orElse(null); } public UserData user(String name) { if (name == null) return null; return users.stream().filter(u -> u.getName().equals(name)).findFirst().orElse(null); } }