/** * maker.js * TUI version — Buy Low, Sell High Market Maker (blessed dashboard). * * Strategy: Place limit BUY on UP+DOWN at low price, sell at target when filled. * No splitPosition — pure orderbook-based market making. * * Run with: npm run maker (live) * npm run maker-sim (simulation with real orderbook via WebSocket) */ import './utils/proxy-patch.cjs'; import { validateMakerConfig } from './config/index.js'; import config from './config/index.js'; import logger from './utils/logger.js'; import { initClient, getUsdcBalance } from './services/client.js'; import { initDashboard, appendLog, updateStatus, isDashboardActive } from './ui/dashboard.js'; import { startMakerDetector, stopMakerDetector } from './services/makerDetector.js'; import { executeMakerStrategy, getActiveMakerPositions, getSimStats } from './services/makerExecutor.js'; import { OrderbookWs } from './services/makerWs.js'; // ── Validate config ──────────────────────────────────────────────────────────── try { validateMakerConfig(); } catch (err) { console.error(`Config error: ${err.message}`); process.exit(1); } // ── Init TUI ────────────────────────────────────────────────────────────────── initDashboard(); logger.setOutput(appendLog); // ── Init CLOB client ────────────────────────────────────────────────────────── try { await initClient(); } catch (err) { logger.error(`Client init error: ${err.message}`); process.exit(1); } // ── WebSocket orderbook (for sim visualization) ────────────────────────────── const orderbookWs = new OrderbookWs(); let activeWsTokens = { up: null, down: null }; // ── Status panel refresh ────────────────────────────────────────────────────── async function buildStatusContent() { const lines = []; // Balance + Sim Stats if (config.dryRun) { const s = getSimStats(); const pnlColor = s.cumulativePnl >= 0 ? 'green' : 'red'; const winRate = s.wins + s.losses > 0 ? ((s.wins / (s.wins + s.losses)) * 100).toFixed(1) : '0.0'; const pnlSign = s.cumulativePnl >= 0 ? '+' : ''; lines.push('{bold}SIMULATION{/bold}'); lines.push(` Balance : {green-fg}$${s.balance.toFixed(2)}{/green-fg} (start: $${s.startBalance.toFixed(2)})`); lines.push(` PnL : {${pnlColor}-fg}${pnlSign}$${s.cumulativePnl.toFixed(4)}{/${pnlColor}-fg}`); lines.push(` Trades : ${s.totalTrades} total`); lines.push(` {green-fg}WIN ${s.wins}x{/green-fg} | {red-fg}LOSS ${s.losses}x{/red-fg} | {gray-fg}SKIP ${s.skips}x{/gray-fg}`); lines.push(` Win% : ${winRate}%`); lines.push(''); // Recent trade history if (s.history.length > 0) { lines.push('{bold}TRADE HISTORY{/bold}'); const recent = s.history.slice(-8); for (const h of recent) { const rColor = h.result === 'win' ? 'green' : h.result === 'loss' ? 'red' : 'gray'; const pSign = h.pnl >= 0 ? '+' : ''; lines.push(` {gray-fg}${h.time}{/gray-fg} {${rColor}-fg}${h.result.toUpperCase().padEnd(4)}{/${rColor}-fg} ${(h.side || '-').padEnd(4)} {${rColor}-fg}${pSign}$${h.pnl.toFixed(4)}{/${rColor}-fg} → $${h.balance.toFixed(2)}`); } lines.push(''); } } else { let balance = '?'; try { balance = (await getUsdcBalance()).toFixed(2); } catch { /* ignore */ } lines.push('{bold}BALANCE{/bold}'); lines.push(` USDC.e: {green-fg}$${balance}{/green-fg}`); lines.push(''); lines.push('{bold}MODE{/bold}'); lines.push(' {green-fg}LIVE{/green-fg}'); lines.push(''); } // Maker Config lines.push('{bold}MAKER CONFIG{/bold}'); lines.push(` Assets : ${config.makerAssets.join(', ').toUpperCase()}`); lines.push(` Duration : ${config.makerDurations.join(', ')}`); lines.push(` Buy @ : $${config.makerBuyPrice} per share`); lines.push(` Sell @ : $${config.makerSellPrice} per share`); lines.push(` Size : ${config.makerTradeSize} shares/side`); lines.push(` Cost/side: $${(config.makerTradeSize * config.makerBuyPrice).toFixed(2)}`); lines.push(` Profit : $${((config.makerSellPrice - config.makerBuyPrice) * config.makerTradeSize).toFixed(2)}/cycle`); lines.push(` CL : cancel buys at ${config.makerCLSeconds || 10}s, sell until close`); lines.push(''); // Active positions const positions = getActiveMakerPositions(); lines.push(`{bold}ACTIVE POSITIONS (${positions.length}){/bold}`); if (positions.length === 0) { lines.push(' {gray-fg}Waiting for market...{/gray-fg}'); } else { for (const pos of positions) { const assetTag = pos.asset ? `[${pos.asset.toUpperCase()}/${pos.duration || '5m'}] ` : ''; const label = pos.question.substring(0, 32); const msLeft = new Date(pos.endTime).getTime() - Date.now(); const secsLeft = Math.max(0, Math.round(msLeft / 1000)); const timeStr = secsLeft > 60 ? `${Math.floor(secsLeft / 60)}m${secsLeft % 60}s` : `{red-fg}${secsLeft}s{/red-fg}`; lines.push(` {cyan-fg}${assetTag}${label}{/cyan-fg}`); lines.push(` Status : ${pos.status} | Time left: ${timeStr}`); // UP side const upFill = pos.up.buyFilled > 0 ? `{green-fg}BOUGHT ${pos.up.buyFilled.toFixed(1)}sh{/green-fg}` : '{gray-fg}waiting...{/gray-fg}'; const upSold = pos.up.totalSellFilled > 0 ? ` → {green-fg}SOLD ${pos.up.totalSellFilled.toFixed(1)}sh{/green-fg}` : pos.up.sellOrders.length > 0 ? ' → {yellow-fg}selling...{/yellow-fg}' : ''; lines.push(` UP ${upFill}${upSold}`); // DOWN side const downFill = pos.down.buyFilled > 0 ? `{green-fg}BOUGHT ${pos.down.buyFilled.toFixed(1)}sh{/green-fg}` : '{gray-fg}waiting...{/gray-fg}'; const downSold = pos.down.totalSellFilled > 0 ? ` → {green-fg}SOLD ${pos.down.totalSellFilled.toFixed(1)}sh{/green-fg}` : pos.down.sellOrders.length > 0 ? ' → {yellow-fg}selling...{/yellow-fg}' : ''; lines.push(` DOWN ${downFill}${downSold}`); // P&L const pnl = pos.totalRevenue - pos.totalCost; const pnlColor = pnl >= 0 ? 'green' : 'red'; lines.push(` P&L: {${pnlColor}-fg}$${pnl.toFixed(4)}{/${pnlColor}-fg}`); lines.push(''); } } // Orderbook display (always show when tokens are active) if (activeWsTokens.up) { lines.push('{bold}LIVE ORDERBOOK{/bold}'); for (const [label, tokenId] of [['UP', activeWsTokens.up], ['DOWN', activeWsTokens.down]]) { if (!tokenId) continue; const book = orderbookWs.getBook(tokenId); const bestBid = orderbookWs.getBestBid(tokenId); const bestAsk = orderbookWs.getBestAsk(tokenId); const mid = bestBid && bestAsk ? ((bestBid + bestAsk) / 2) : 0; lines.push(` {cyan-fg}${label}{/cyan-fg} mid: $${mid.toFixed(3)} | bid: $${bestBid.toFixed(2)} ask: $${bestAsk.toFixed(2)}`); // Top 5 asks (reversed so lowest is closest to spread) const topAsks = book.asks.slice(0, 5).reverse(); for (const ask of topAsks) { const bar = '█'.repeat(Math.min(10, Math.round(ask.size / 100))); lines.push(` {red-fg}$${ask.price.toFixed(2)} ${ask.size.toFixed(0).padStart(7)} ${bar}{/red-fg}`); } // Spread line if (bestBid && bestAsk) { const spread = bestAsk - bestBid; lines.push(` {yellow-fg}── spread $${spread.toFixed(2)} ──{/yellow-fg}`); } // Top 5 bids const topBids = book.bids.slice(0, 5); for (const bid of topBids) { const bar = '█'.repeat(Math.min(10, Math.round(bid.size / 100))); lines.push(` {green-fg}$${bid.price.toFixed(2)} ${bid.size.toFixed(0).padStart(7)} ${bar}{/green-fg}`); } lines.push(''); } } return '\n' + lines.join('\n'); } let refreshTimer = null; function startRefresh() { refreshTimer = setInterval(async () => { if (!isDashboardActive()) return; updateStatus(await buildStatusContent()); }, 2000); buildStatusContent().then(updateStatus); } // ── Market handler with per-asset queue ────────────────────────────────────── const pendingByAsset = new Map(); function slotKey(market) { return `${market.asset}-${market.duration || '5m'}`; } async function runStrategy(market) { const key = slotKey(market); const tag = `${market.asset?.toUpperCase()}/${market.duration || '5m'}`; // Connect WebSocket for orderbook visualization in sim mode if (config.dryRun) { activeWsTokens = { up: market.yesTokenId, down: market.noTokenId }; orderbookWs.subscribe(market.conditionId, [market.yesTokenId, market.noTokenId]); } try { await executeMakerStrategy(market); } catch (err) { logger.error(`MAKER strategy error (${tag}): ${err.message}`); } // Disconnect WS after strategy ends if (config.dryRun) { activeWsTokens = { up: null, down: null }; } // Process queued market const queued = pendingByAsset.get(key); if (queued) { pendingByAsset.delete(key); const endMs = new Date(queued.endTime).getTime(); const secsLeft = Math.round((endMs - Date.now()) / 1000); if (secsLeft > 30) { logger.success(`MAKER[${tag}]: executing queued market (${secsLeft}s left)`); runStrategy(queued); } else { logger.warn(`MAKER[${tag}]: queued market expired (${secsLeft}s left)`); } } } async function handleNewMarket(market) { const key = slotKey(market); const tag = `${market.asset?.toUpperCase()}/${market.duration || '5m'}`; const active = getActiveMakerPositions(); const isSlotBusy = active.some((p) => p.asset === market.asset && p.duration === (market.duration || '5m')); if (isSlotBusy) { pendingByAsset.set(key, market); logger.warn(`MAKER[${tag}]: queued — will enter after current position clears`); return; } runStrategy(market); } // ── Graceful shutdown ───────────────────────────────────────────────────────── function shutdown() { logger.warn('MAKER: shutting down...'); stopMakerDetector(); orderbookWs.shutdown(); if (refreshTimer) clearInterval(refreshTimer); process.exit(0); } process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); // ── Start ───────────────────────────────────────────────────────────────────── const costPerSide = config.makerTradeSize * config.makerBuyPrice; const profitPerCycle = (config.makerSellPrice - config.makerBuyPrice) * config.makerTradeSize; logger.info(`MAKER starting — ${config.dryRun ? 'SIMULATION' : 'LIVE'}`); logger.info(`Assets: ${config.makerAssets.join(', ').toUpperCase()} | Durations: ${config.makerDurations.join(', ')} | BUY @ $${config.makerBuyPrice} → SELL @ $${config.makerSellPrice}`); logger.info(`Size: ${config.makerTradeSize} sh/side | Cost: $${costPerSide.toFixed(2)}/side | Profit: $${profitPerCycle.toFixed(2)}/cycle`); startRefresh(); startMakerDetector(handleNewMarket);