Files
polymarket-terminal/src/index.js
T
direkturcrypto 526076fe6e feat: full project overhaul — market maker, sniper, WebSocket watcher, terminal UI
- Rename project to polymarket-terminal
- Add Market Maker bot (src/mm.js) with on-chain CTF split/merge/redeem via Gnosis Safe
- Add Orderbook Sniper bot (src/sniper.js) with multi-asset GTC low-price orders
- Add WebSocket watcher (src/services/wsWatcher.js) for real-time RTDS trade events
- Add terminal dashboard UI (src/ui/dashboard.js) using blessed
- Add CTF contract helpers (src/services/ctf.js) for splitPosition, mergePositions, redeemPositions
- Add mmDetector, mmExecutor, sniperDetector, sniperExecutor services
- Add simStats utility for dry-run P&L tracking
- Translate all Indonesian-language strings to professional English across all files
- Rewrite README.md in English with full setup guide, configuration reference, and architecture overview
- Rewrite AGENT.MD in English as comprehensive AI agent and developer reference
- Update package.json name, description, scripts, and keywords

Co-Authored-By: direkturcrypto <direkturcrypto.x@mail3.me>
2026-02-23 23:03:06 +07:00

232 lines
9.3 KiB
JavaScript

import config, { validateConfig } from './config/index.js';
import { initClient, getUsdcBalance, getClient } from './services/client.js';
import { executeBuy, executeSell } from './services/executor.js';
import { checkAndRedeemPositions } from './services/redeemer.js';
import { getOpenPositions } from './services/position.js';
import { startWsWatcher, stopWsWatcher } from './services/wsWatcher.js';
import { getSimStats } from './utils/simStats.js';
import { initDashboard, appendLog, updateStatus } from './ui/dashboard.js';
import logger from './utils/logger.js';
// ── Dashboard init (before any log output) ────────────────────────────────────
initDashboard();
logger.setOutput(appendLog);
// ── Handle a trade event from WebSocket ───────────────────────────────────────
async function handleTrade(trade) {
try {
if (trade.type === 'BUY') {
await executeBuy(trade);
} else if (trade.type === 'SELL') {
await executeSell(trade);
}
} catch (err) {
logger.error(`Error processing trade ${trade.id}: ${err.message}`);
}
}
// ── Build the right-panel status content ──────────────────────────────────────
async function buildStatusContent() {
const lines = [];
// Header
const mode = config.dryRun ? '{yellow-fg}[SIMULATION]{/yellow-fg}' : '{red-fg}[LIVE TRADING]{/red-fg}';
lines.push(` ${mode} Trader: {cyan-fg}${config.traderAddress.substring(0, 12)}…{/cyan-fg}`);
lines.push(` {gray-fg}${'─'.repeat(36)}{/gray-fg}`);
// Balance
try {
const balance = await getUsdcBalance();
const balColor = balance > 0 ? 'green-fg' : 'gray-fg';
lines.push(` {yellow-fg}💵 Balance:{/yellow-fg} {bold}{${balColor}}$${balance.toFixed(2)} USDC.e{/${balColor}}{/bold}`);
} catch {
lines.push(` {yellow-fg}💵 Balance:{/yellow-fg} {gray-fg}N/A{/gray-fg}`);
}
lines.push('');
// Open positions
const positions = getOpenPositions();
if (positions.length === 0) {
lines.push(` {gray-fg}No open positions{/gray-fg}`);
} else {
lines.push(` {cyan-fg}📈 POSITIONS (${positions.length}){/cyan-fg}`);
lines.push(` {gray-fg}${'─'.repeat(36)}{/gray-fg}`);
for (let i = 0; i < positions.length; i++) {
const pos = positions[i];
// Fetch current midpoint price (best-effort)
let currentPrice = null;
try {
const client = getClient();
const mp = await client.getMidpoint(pos.tokenId);
currentPrice = parseFloat(mp?.mid ?? mp ?? '0');
if (!currentPrice || isNaN(currentPrice)) currentPrice = null;
} catch { /* price unavailable */ }
const marketName = (pos.market || pos.tokenId || '').substring(0, 30);
const spent = pos.totalCost || 0;
const maxStr = config.maxPositionSize;
const bar = buildBar(spent, maxStr, 18);
lines.push('');
lines.push(` {bold}{white-fg}${i + 1}. ${marketName}{/white-fg}{/bold}`);
lines.push(` {cyan-fg}${pos.outcome || '?'}{/cyan-fg} | ${pos.shares.toFixed(3)} sh @ $${pos.avgBuyPrice.toFixed(3)}`);
lines.push(` ${bar} {yellow-fg}$${spent.toFixed(2)}/$${maxStr}{/yellow-fg}`);
if (currentPrice !== null) {
const pnl = (currentPrice - pos.avgBuyPrice) * pos.shares;
const pct = spent > 0 ? ((pnl / spent) * 100).toFixed(1) : '0.0';
const sign = pnl >= 0 ? '+' : '';
const pnlColor = pnl >= 0 ? 'green-fg' : 'red-fg';
lines.push(` $${currentPrice.toFixed(3)} now | {${pnlColor}}${sign}$${pnl.toFixed(2)} (${sign}${pct}%){/${pnlColor}}`);
} else {
lines.push(` {gray-fg}Price unavailable{/gray-fg}`);
}
}
}
// Simulation stats (only when dryRun)
if (config.dryRun) {
const s = getSimStats();
lines.push('');
lines.push(` {gray-fg}${'─'.repeat(36)}{/gray-fg}`);
lines.push(` {magenta-fg}📊 SIMULATION STATS{/magenta-fg}`);
lines.push(` Buys tracked : ${s.totalBuys}`);
lines.push(` Resolved : ${s.totalResolved}`);
if (s.totalResolved > 0) {
const rate = ((s.wins / s.totalResolved) * 100).toFixed(0);
lines.push(
` {green-fg}Wins: ${s.wins}{/green-fg} {red-fg}Losses: ${s.losses}{/red-fg} Rate: ${rate}%`,
);
}
const pnl = s.closedPnl || 0;
if (pnl !== 0) {
const sign = pnl >= 0 ? '+' : '';
const c = pnl >= 0 ? 'green-fg' : 'red-fg';
lines.push(` Realized P&L : {${c}}{bold}${sign}$${pnl.toFixed(2)}{/bold}{/${c}}`);
}
if (s.closedPositions && s.closedPositions.length > 0) {
lines.push('');
lines.push(` {gray-fg}── Recent Closed ──{/gray-fg}`);
const recent = s.closedPositions.slice(-5).reverse();
for (const cp of recent) {
const icon = cp.result === 'WIN' ? '{green-fg}✅{/green-fg}' : '{red-fg}❌{/red-fg}';
const sign = cp.pnl >= 0 ? '+' : '';
lines.push(` ${icon} ${(cp.market || '').substring(0, 22)}`);
lines.push(` {gray-fg}${sign}$${cp.pnl.toFixed(2)}{/gray-fg}`);
}
}
}
lines.push('');
lines.push(` {gray-fg}Updated ${new Date().toISOString().substring(11, 19)}{/gray-fg}`);
return lines.join('\n');
}
/** Simple ASCII fill bar */
function buildBar(value, max, width) {
const pct = Math.min(value / max, 1);
const filled = Math.round(pct * width);
const empty = width - filled;
const color = pct >= 1 ? 'red-fg' : pct >= 0.7 ? 'yellow-fg' : 'green-fg';
return `{${color}}${'█'.repeat(filled)}{/${color}}{gray-fg}${'░'.repeat(empty)}{/gray-fg}`;
}
// ── Status panel refresh loop ─────────────────────────────────────────────────
async function refreshStatus() {
try {
const content = await buildStatusContent();
updateStatus(content);
} catch (err) {
updateStatus(` {red-fg}Error refreshing status: ${err.message}{/red-fg}`);
}
}
// ── Redeemer loop ─────────────────────────────────────────────────────────────
async function redeemerLoop() {
try {
await checkAndRedeemPositions();
} catch (err) {
logger.error('Redeemer loop error:', err.message);
}
}
// ── Main ──────────────────────────────────────────────────────────────────────
async function main() {
// Validate config
try {
validateConfig();
} catch (err) {
logger.error(err.message);
process.exit(1);
}
logger.info('=== Settings ===');
logger.info(`Trader : ${config.traderAddress}`);
logger.info(`Proxy wallet : ${config.proxyWallet}`);
logger.info(`Size mode : ${config.sizeMode} (${config.sizePercent}%)`);
logger.info(`Min trade : $${config.minTradeSize}`);
logger.info(`Max position : $${config.maxPositionSize} per market`);
logger.info(`Auto sell : ${config.autoSellEnabled ? `ON (${config.autoSellProfitPercent}%)` : 'OFF'}`);
logger.info(`Sell mode : ${config.sellMode}`);
logger.info(`Mode : ${config.dryRun ? 'SIMULATION (DRY RUN)' : 'LIVE TRADING'}`);
logger.info('================');
// Initialize CLOB client
try {
await initClient();
} catch (err) {
logger.error('Failed to initialize client:', err.message);
process.exit(1);
}
// Initial balance display
try {
const balance = await getUsdcBalance();
logger.money(`USDC.e Balance: $${balance.toFixed(2)}`);
} catch (err) {
logger.warn('Could not fetch balance:', err.message);
}
logger.success(
config.dryRun
? 'Simulation started! Watching trader in real-time...'
: 'Bot started! Watching trader in real-time...',
);
// Initial status render
await refreshStatus();
// Start real-time WebSocket watcher
startWsWatcher(handleTrade);
// Run redeemer immediately then on interval
await redeemerLoop();
const redeemerInterval = setInterval(redeemerLoop, config.redeemInterval);
// Refresh right panel every 5 seconds
const statusInterval = setInterval(refreshStatus, 5000);
// Graceful shutdown
const shutdown = () => {
logger.info('Shutting down...');
stopWsWatcher();
clearInterval(redeemerInterval);
clearInterval(statusInterval);
setTimeout(() => process.exit(0), 300);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
}
main().catch((err) => {
logger.error('Fatal error:', err.message);
process.exit(1);
});