Files
polymarket-terminal/src/mm.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

224 lines
8.8 KiB
JavaScript

/**
* mm.js
* Entry point for the Market Maker bot.
* Detects new Bitcoin 5-minute markets and executes the MM strategy.
* Run with: npm run mm (live)
* npm run mm-sim (simulation / dry-run)
*/
import { validateMMConfig } from './config/index.js';
import config from './config/index.js';
import logger from './utils/logger.js';
import { initClient, getClient } from './services/client.js';
import { initDashboard, appendLog, updateStatus, isDashboardActive } from './ui/dashboard.js';
import { startMMDetector, stopMMDetector } from './services/mmDetector.js';
import { executeMMStrategy, getActiveMMPositions } from './services/mmExecutor.js';
import { getUsdcBalance } from './services/client.js';
import { cleanupOpenPositions, redeemMMPositions, MIN_SHARES_PER_SIDE } from './services/ctf.js';
// ── Validate config ────────────────────────────────────────────────────────────
try {
validateMMConfig();
} 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);
}
// ── Validate MM_TRADE_SIZE minimum ────────────────────────────────────────────
if (config.mmTradeSize < MIN_SHARES_PER_SIDE) {
logger.error(
`MM_TRADE_SIZE=${config.mmTradeSize} is below Polymarket minimum of ${MIN_SHARES_PER_SIDE} shares. ` +
`Set MM_TRADE_SIZE ≥ ${MIN_SHARES_PER_SIDE} in your .env and restart.`
);
process.exit(1);
}
// ── Cleanup leftover positions on startup ─────────────────────────────────────
try {
await cleanupOpenPositions(getClient());
} catch (err) {
logger.warn(`MM: startup cleanup failed (non-fatal): ${err.message}`);
}
// ── Status panel refresh ──────────────────────────────────────────────────────
async function buildStatusContent() {
let lines = [];
// Balance
let balance = '?';
if (!config.dryRun) {
try { balance = (await getUsdcBalance()).toFixed(2); } catch { /* ignore */ }
} else {
balance = '{yellow-fg}SIM{/yellow-fg}';
}
lines.push(`{bold}BALANCE{/bold}`);
lines.push(` USDC.e: {green-fg}$${balance}{/green-fg}`);
lines.push('');
// Mode
lines.push(`{bold}MODE{/bold}`);
lines.push(` ${config.dryRun ? '{yellow-fg}SIMULATION{/yellow-fg}' : '{green-fg}LIVE{/green-fg}'}`);
lines.push('');
// MM Config
lines.push(`{bold}MM CONFIG{/bold}`);
lines.push(` Assets : ${config.mmAssets.join(', ').toUpperCase()}`);
lines.push(` Duration : ${config.mmDuration}`);
lines.push(` Trade sz : $${config.mmTradeSize} per side`);
lines.push(` Sell @ : $${config.mmSellPrice}`);
lines.push(` Cut loss : ${config.mmCutLossTime}s before close`);
lines.push('');
// Active positions
const positions = getActiveMMPositions();
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()}] ` : '';
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}`);
// YES side
const yFill = pos.yes.filled
? `{green-fg}FILLED @ $${pos.yes.fillPrice?.toFixed(3)}{/green-fg}`
: `{yellow-fg}waiting $${config.mmSellPrice}{/yellow-fg}`;
lines.push(` YES ${pos.yes.shares?.toFixed(3)} sh @ $${pos.yes.entryPrice?.toFixed(3)}${yFill}`);
// NO side
const nFill = pos.no.filled
? `{green-fg}FILLED @ $${pos.no.fillPrice?.toFixed(3)}{/green-fg}`
: `{yellow-fg}waiting $${config.mmSellPrice}{/yellow-fg}`;
lines.push(` NO ${pos.no.shares?.toFixed(3)} sh @ $${pos.no.entryPrice?.toFixed(3)}${nFill}`);
lines.push('');
}
}
return '\n' + lines.join('\n');
}
let refreshTimer = null;
let redeemTimer = null;
function startRefresh() {
refreshTimer = setInterval(async () => {
if (!isDashboardActive()) return;
const content = await buildStatusContent();
updateStatus(content);
}, 3000);
// Also do one immediate refresh
buildStatusContent().then(updateStatus);
}
function startRedeemer() {
// Run once immediately, then every redeemInterval (default 60s)
redeemMMPositions().catch((err) => logger.error('MM redeemer error:', err.message));
redeemTimer = setInterval(
() => redeemMMPositions().catch((err) => logger.error('MM redeemer error:', err.message)),
config.redeemInterval,
);
logger.info(`MM redeemer started — checking every ${config.redeemInterval / 1000}s`);
}
// ── Market handler with per-asset queue ──────────────────────────────────────
// Each asset can hold one pending market while its current position is active.
const pendingByAsset = new Map(); // asset → market
async function runStrategy(market) {
try {
await executeMMStrategy(market);
} catch (err) {
logger.error(`MM strategy error (${market.asset?.toUpperCase()}): ${err.message}`);
}
// After position clears, execute the queued market for this asset if still valid
const queued = pendingByAsset.get(market.asset);
if (queued) {
pendingByAsset.delete(market.asset);
const endMs = new Date(queued.endTime).getTime();
const secsLeft = Math.round((endMs - Date.now()) / 1000);
if (secsLeft > config.mmCutLossTime) {
logger.success(
`MM[${market.asset?.toUpperCase()}]: position cleared — ` +
`executing queued "${queued.question.substring(0, 40)}" (${secsLeft}s left)`
);
runStrategy(queued); // non-blocking
} else {
logger.warn(
`MM[${market.asset?.toUpperCase()}]: queued market "${queued.question.substring(0, 40)}" ` +
`expired (${secsLeft}s left) — discarding`
);
}
}
}
async function handleNewMarket(market) {
const active = getActiveMMPositions();
const isAssetBusy = active.some((p) => p.asset === market.asset);
if (isAssetBusy) {
// Queue this market for this asset — runs once the current position exits
pendingByAsset.set(market.asset, market);
logger.warn(
`MM[${market.asset?.toUpperCase()}]: queued "${market.question.substring(0, 40)}" — ` +
`will enter after current ${market.asset?.toUpperCase()} position clears`
);
return;
}
runStrategy(market); // non-blocking
}
// ── Graceful shutdown ─────────────────────────────────────────────────────────
function shutdown() {
logger.warn('MM: shutting down...');
stopMMDetector();
if (refreshTimer) clearInterval(refreshTimer);
if (redeemTimer) clearInterval(redeemTimer);
process.exit(0);
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
// ── Start ─────────────────────────────────────────────────────────────────────
logger.info(`MM bot starting — ${config.dryRun ? 'SIMULATION MODE' : 'LIVE MODE'} | assets: ${config.mmAssets.join(', ').toUpperCase()} | ${config.mmDuration}`);
startRefresh();
startRedeemer();
startMMDetector(handleNewMarket);