From 5ea7f3d2b0719fb9814abe96083dc741aa72ec76 Mon Sep 17 00:00:00 2001 From: direkturcrypto Date: Thu, 26 Feb 2026 02:05:06 +0700 Subject: [PATCH] feat: add PM2-compatible MM entry point (src/mm-bot.js) - src/mm-bot.js: same MM logic as mm.js but no blessed TUI; writes plain text to stdout so pm2 logs works on VPS; status printed every 60s - ecosystem.config.cjs: added polymarket-mm app with separate log files (logs/mm-out.log, logs/mm-error.log); both copy and MM bots now managed - package.json: added mm-bot / mm-bot-sim / mm-bot-dev scripts Usage: pm2 start ecosystem.config.cjs --only polymarket-mm pm2 start ecosystem.config.cjs --only polymarket-mm --env sim pm2 logs polymarket-mm Co-Authored-By: Claude Sonnet 4.6 --- ecosystem.config.cjs | 64 +++++++++++---- package.json | 3 + src/mm-bot.js | 188 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 240 insertions(+), 15 deletions(-) create mode 100644 src/mm-bot.js diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index d4d8b65..213ad68 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -1,47 +1,81 @@ /** * PM2 ecosystem config * - * Usage: + * Start all bots: * pm2 start ecosystem.config.cjs # live trading * pm2 start ecosystem.config.cjs --env sim # simulation / dry-run * - * pm2 logs polymarket-copy # tail logs + * Start individual bot: + * pm2 start ecosystem.config.cjs --only polymarket-copy + * pm2 start ecosystem.config.cjs --only polymarket-mm + * pm2 start ecosystem.config.cjs --only polymarket-copy --env sim + * pm2 start ecosystem.config.cjs --only polymarket-mm --env sim + * + * Logs: + * pm2 logs # all bots + * pm2 logs polymarket-copy # copy trade only + * pm2 logs polymarket-mm # market maker only * pm2 logs polymarket-copy --lines 200 - * pm2 stop / restart / delete polymarket-copy + * + * Management: + * pm2 restart / stop / delete polymarket-copy + * pm2 restart / stop / delete polymarket-mm + * pm2 restart all */ module.exports = { apps: [ + // ── Copy Trade Bot ───────────────────────────────────────────────────── { name: 'polymarket-copy', script: 'src/bot.js', interpreter: 'node', - // Live trading (default) env: { NODE_ENV: 'production', DRY_RUN: 'false', }, - - // Simulation: pm2 start ecosystem.config.cjs --env sim env_sim: { NODE_ENV: 'production', DRY_RUN: 'true', }, - // Log files (relative to project root) - out_file: 'logs/out.log', - error_file: 'logs/error.log', + out_file: 'logs/copy-out.log', + error_file: 'logs/copy-error.log', log_date_format: 'YYYY-MM-DD HH:mm:ss Z', merge_logs: true, - // Restart policy - restart_delay: 5000, // wait 5s before restarting - max_restarts: 10, // give up after 10 crashes in a row - min_uptime: '10s', // must stay up ≥10s to count as "stable" + restart_delay: 5000, + max_restarts: 10, + min_uptime: '10s', max_memory_restart: '256M', + stop_exit_codes: [0], + }, - // Treat non-zero exit as a crash (don't restart on clean SIGTERM) - stop_exit_codes: [0], + // ── Market Maker Bot ─────────────────────────────────────────────────── + { + name: 'polymarket-mm', + script: 'src/mm-bot.js', + interpreter: 'node', + + env: { + NODE_ENV: 'production', + DRY_RUN: 'false', + }, + env_sim: { + NODE_ENV: 'production', + DRY_RUN: 'true', + }, + + out_file: 'logs/mm-out.log', + error_file: 'logs/mm-error.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss Z', + merge_logs: true, + + restart_delay: 5000, + max_restarts: 10, + min_uptime: '10s', + max_memory_restart: '256M', + stop_exit_codes: [0], }, ], }; diff --git a/package.json b/package.json index ce09326..29d1a34 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "mm": "DRY_RUN=false node src/mm.js", "mm-sim": "DRY_RUN=true node src/mm.js", "mm-dev": "DRY_RUN=true nodemon --ignore 'data/*.json' src/mm.js", + "mm-bot": "node src/mm-bot.js", + "mm-bot-sim": "DRY_RUN=true node src/mm-bot.js", + "mm-bot-dev": "DRY_RUN=true nodemon --ignore 'data/*.json' src/mm-bot.js", "sniper": "DRY_RUN=false node src/sniper.js", "sniper-sim": "DRY_RUN=true node src/sniper.js", "sniper-dev": "DRY_RUN=true nodemon --ignore 'data/*.json' src/sniper.js" diff --git a/src/mm-bot.js b/src/mm-bot.js new file mode 100644 index 0000000..3153d17 --- /dev/null +++ b/src/mm-bot.js @@ -0,0 +1,188 @@ +/** + * mm-bot.js — Market Maker, PM2 / VPS entry point (no TUI) + * + * Plain-text stdout output, compatible with: + * pm2 start ecosystem.config.cjs --only polymarket-mm + * pm2 logs polymarket-mm + */ + +import { validateMMConfig } from './config/index.js'; +import config from './config/index.js'; +import logger from './utils/logger.js'; +import { initClient, getClient, getUsdcBalance } from './services/client.js'; +import { startMMDetector, stopMMDetector } from './services/mmDetector.js'; +import { executeMMStrategy, getActiveMMPositions } from './services/mmExecutor.js'; +import { cleanupOpenPositions, redeemMMPositions, MIN_SHARES_PER_SIDE } from './services/ctf.js'; + +logger.interceptConsole(); + +// ── Validate config ──────────────────────────────────────────────────────────── + +try { + validateMMConfig(); +} catch (err) { + logger.error(`Config error: ${err.message}`); + process.exit(1); +} + +// ── 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}`); +} + +// ── Periodic status log (replaces TUI right panel) ──────────────────────────── + +async function printStatus() { + try { + let balanceStr = 'SIM'; + if (!config.dryRun) { + try { balanceStr = `$${(await getUsdcBalance()).toFixed(2)} USDC`; } catch { balanceStr = 'N/A'; } + } + + const positions = getActiveMMPositions(); + const mode = config.dryRun ? 'SIMULATION' : 'LIVE'; + + logger.info( + `--- MM Status [${mode}] | Balance: ${balanceStr} | Active positions: ${positions.length} ---`, + ); + + for (const pos of positions) { + const assetTag = pos.asset ? `[${pos.asset.toUpperCase()}] ` : ''; + const label = pos.question.substring(0, 50); + 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 left` + : `${secsLeft}s left`; + + const yFill = pos.yes.filled + ? `FILLED @ $${pos.yes.fillPrice?.toFixed(3)}` + : `waiting $${config.mmSellPrice}`; + const nFill = pos.no.filled + ? `FILLED @ $${pos.no.fillPrice?.toFixed(3)}` + : `waiting $${config.mmSellPrice}`; + + logger.info( + ` ${assetTag}${label} | ${pos.status} | ${timeStr}` + + ` | YES ${pos.yes.shares?.toFixed(3)}sh@$${pos.yes.entryPrice?.toFixed(3)} → ${yFill}` + + ` | NO ${pos.no.shares?.toFixed(3)}sh@$${pos.no.entryPrice?.toFixed(3)} → ${nFill}`, + ); + } + } catch (err) { + logger.warn(`Status check error: ${err.message}`); + } +} + +// ── 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) { + 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 +} + +// ── Timers ──────────────────────────────────────────────────────────────────── + +// Print status every 60 seconds +const statusTimer = setInterval(printStatus, 60_000); + +// Redeemer: run immediately then every redeemInterval +redeemMMPositions().catch((err) => logger.error('MM redeemer error:', err.message)); +const redeemTimer = setInterval( + () => redeemMMPositions().catch((err) => logger.error('MM redeemer error:', err.message)), + config.redeemInterval, +); + +// ── Graceful shutdown ───────────────────────────────────────────────────────── + +function shutdown() { + logger.warn('MM: shutting down...'); + stopMMDetector(); + clearInterval(statusTimer); + clearInterval(redeemTimer); + setTimeout(() => process.exit(0), 300); +} + +process.on('SIGINT', shutdown); +process.on('SIGTERM', shutdown); + +// ── Start ───────────────────────────────────────────────────────────────────── + +const mode = config.dryRun ? 'SIMULATION' : 'LIVE'; +logger.info(`=== Market Maker [${mode}] ===`); +logger.info(`Assets : ${config.mmAssets.join(', ').toUpperCase()}`); +logger.info(`Duration : ${config.mmDuration}`); +logger.info(`Trade size: $${config.mmTradeSize} per side`); +logger.info(`Sell @ : $${config.mmSellPrice}`); +logger.info(`Cut loss : ${config.mmCutLossTime}s before close`); +logger.info(`Keyword : ${config.mmMarketKeyword}`); +logger.info(`Entry win : ${config.mmEntryWindow}s after open`); +logger.info('=========================================='); + +startMMDetector(handleNewMarket); +logger.success(`MM bot started — watching for ${config.mmDuration} ${config.mmAssets.join('/')} markets...`);