import BN from "bn.js"; import { PythPriceService } from "../pyth/hermes.js"; import { PythPrice } from "../pyth/types.js"; import { MarketState, PoolType } from "../types.js"; import { calculateOutcome } from "../outcome/calculate.js"; /** * PriceMonitor - Monitors Pyth price and calculates outcomes * * Responsibilities: * - Polls Pyth price at configured interval * - Calculates outcomes for both High and Low pools * - Detects outcome changes and triggers callbacks */ export class PriceMonitor { private priceService: PythPriceService; private checkInterval: number; private verbose: boolean; private timer: NodeJS.Timeout | null = null; private isRunning = false; private lastPrice: PythPrice | null = null; constructor(hermesUrl: string, checkInterval: number = 2000, verbose: boolean = false) { this.priceService = new PythPriceService(hermesUrl); this.checkInterval = checkInterval; this.verbose = verbose; } /** * Start price monitoring * * @param market - Current market state * @param onOutcomeChange - Callback when outcome changes * @param onPriceUpdate - Callback on each price update */ start( market: MarketState, onOutcomeChange: ( poolType: PoolType, newOutcome: number, priceInCents: number ) => Promise, onPriceUpdate?: (price: PythPrice) => void ): void { if (this.isRunning) { throw new Error("PriceMonitor is already running"); } this.isRunning = true; const checkPrice = async () => { if (!this.isRunning) return; // Don't monitor if both pools are settled or market is paused if (market.highPool.isSettled && market.lowPool.isSettled) { return; } if (market.isPaused) { this.log("Market is paused, skipping price monitoring"); return; } try { const price = await this.priceService.getLatestPrice(); this.lastPrice = price; if (onPriceUpdate) { onPriceUpdate(price); } // Calculate outcomes for both pools const highStrikes = market.highPool.strikes as [BN, BN, BN]; const lowStrikes = market.lowPool.strikes as [BN, BN, BN]; const highOutcomeResult = calculateOutcome(price.priceInCents, highStrikes); const lowOutcomeResult = calculateOutcome(price.priceInCents, lowStrikes); // Debug logging if (this.verbose) { const hs0 = highStrikes[0].toNumber(); const hs1 = highStrikes[1].toNumber(); const hs2 = highStrikes[2].toNumber(); this.log( `Price: $${(price.priceInCents / 100).toFixed(2)} (${price.priceInCents} cents)` ); this.log( `High Strikes: $${(hs0 / 100).toFixed(2)}/$${(hs1 / 100).toFixed(2)}/$${( hs2 / 100 ).toFixed(2)}` ); this.log( `High: price >= S0(${hs0})? ${price.priceInCents >= hs0}, >= S1(${hs1})? ${ price.priceInCents >= hs1 }, >= S2(${hs2})? ${price.priceInCents >= hs2} → outcome=${ highOutcomeResult.highOutcome }` ); const ls0 = lowStrikes[0].toNumber(); const ls1 = lowStrikes[1].toNumber(); const ls2 = lowStrikes[2].toNumber(); this.log( `Low Strikes: $${(ls0 / 100).toFixed(2)}/$${(ls1 / 100).toFixed(2)}/$${( ls2 / 100 ).toFixed(2)}` ); this.log( `Low: price < S0(${ls0})? ${price.priceInCents < ls0}, < S1(${ls1})? ${ price.priceInCents < ls1 }, < S2(${ls2})? ${price.priceInCents < ls2} → outcome=${lowOutcomeResult.lowOutcome}` ); this.log( `Current state: High=${market.highPool.winningOutcome}, Low=${market.lowPool.winningOutcome}` ); } // Check High pool for outcome change if (!market.highPool.isSettled) { if (highOutcomeResult.highOutcome > market.highPool.winningOutcome) { await onOutcomeChange(PoolType.High, highOutcomeResult.highOutcome, price.priceInCents); } else { this.log( `High pool: no update needed (current=${market.highPool.winningOutcome} >= calculated=${highOutcomeResult.highOutcome})` ); } } else { this.log("High pool: already settled, skipping"); } // Check Low pool for outcome change if (!market.lowPool.isSettled) { if (lowOutcomeResult.lowOutcome > market.lowPool.winningOutcome) { await onOutcomeChange(PoolType.Low, lowOutcomeResult.lowOutcome, price.priceInCents); } else { this.log( `Low pool: no update needed (current=${market.lowPool.winningOutcome} >= calculated=${lowOutcomeResult.lowOutcome})` ); } } else { this.log("Low pool: already settled, skipping"); } } catch (err) { this.log(`Price check error: ${(err as Error).message}`); throw err; } }; // Run immediately, then on interval checkPrice(); this.timer = setInterval(checkPrice, this.checkInterval); } /** * Stop price monitoring */ stop(): void { this.isRunning = false; if (this.timer) { clearInterval(this.timer); this.timer = null; } } /** * Get last price update */ getLastPrice(): PythPrice | null { return this.lastPrice; } private log(message: string): void { if (this.verbose) { console.log(`[PriceMonitor] ${message}`); } } }