526076fe6e
- 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>
224 lines
8.8 KiB
JavaScript
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);
|