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>
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* 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);
|
||||
Reference in New Issue
Block a user