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 <noreply@anthropic.com>
This commit is contained in:
+49
-15
@@ -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],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
+188
@@ -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...`);
|
||||
Reference in New Issue
Block a user