Files
polymarket-terminal/src/maker.js
T
direkturcryptoandClaude Opus 4.6 5f4aa04d13 feat: add buy CL at 10s, multi-duration support, and multi-asset concurrent maker
- CL at 10s before close: cancel unfilled buy orders, place sells for filled
  positions with 3x retry for on-chain settlement delay
- Sell orders stay live until market close (no early cancellation)
- Support multiple durations simultaneously (MAKER_DURATION=5m,15m)
- Queue key changed to asset+duration so BTC/5m and BTC/15m run concurrently
- Update AGENT.MD and README.md with Maker v2 documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 00:58:35 +07:00

282 lines
12 KiB
JavaScript

/**
* 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);