feat: add PM2-compatible entry point (src/bot.js) and ecosystem config

- src/bot.js: same logic as index.js but no blessed TUI; writes plain
  text to stdout so pm2 logs works cleanly on VPS
- ecosystem.config.cjs: pm2 config with live/sim envs, log files at
  logs/out.log + logs/error.log, auto-restart policy
- logs/.gitkeep: track the logs/ dir (*.log files are gitignored)
- package.json: added bot / bot-sim / bot-dev scripts

Usage on VPS:
  pm2 start ecosystem.config.cjs            # live
  pm2 start ecosystem.config.cjs --env sim  # simulation
  pm2 logs polymarket-copy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
direkturcrypto
2026-02-25 04:17:16 +07:00
parent 0650cb9ef7
commit 526fc7a057
5 changed files with 200 additions and 0 deletions
+1
View File
@@ -2,3 +2,4 @@ node_modules/
.env
data/*.json
.DS_Store
logs/*.log
+47
View File
@@ -0,0 +1,47 @@
/**
* PM2 ecosystem config
*
* Usage:
* pm2 start ecosystem.config.cjs # live trading
* pm2 start ecosystem.config.cjs --env sim # simulation / dry-run
*
* pm2 logs polymarket-copy # tail logs
* pm2 logs polymarket-copy --lines 200
* pm2 stop / restart / delete polymarket-copy
*/
module.exports = {
apps: [
{
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',
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"
max_memory_restart: '256M',
// Treat non-zero exit as a crash (don't restart on clean SIGTERM)
stop_exit_codes: [0],
},
],
};
View File
+3
View File
@@ -7,6 +7,9 @@
"scripts": {
"start": "node src/index.js",
"dev": "nodemon --ignore 'data/*.json' src/index.js",
"bot": "node src/bot.js",
"bot-sim": "DRY_RUN=true node src/bot.js",
"bot-dev": "DRY_RUN=true nodemon --ignore 'data/*.json' src/bot.js",
"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",
+149
View File
@@ -0,0 +1,149 @@
/**
* bot.js — PM2 / VPS entry point (no TUI)
*
* Plain-text stdout output, compatible with:
* pm2 start ecosystem.config.cjs
* pm2 logs polymarket-copy
*/
import config, { validateConfig } from './config/index.js';
import { initClient, getUsdcBalance, getClient } from './services/client.js';
import { executeBuy, executeSell } from './services/executor.js';
import { checkAndRedeemPositions } from './services/redeemer.js';
import { getOpenPositions } from './services/position.js';
import { startWsWatcher, stopWsWatcher } from './services/wsWatcher.js';
import { getSimStats } from './utils/simStats.js';
import logger from './utils/logger.js';
logger.interceptConsole(); // strip auth headers from CLOB axios error dumps
// ── Handle a trade event from WebSocket ───────────────────────────────────────
async function handleTrade(trade) {
try {
if (trade.type === 'BUY') await executeBuy(trade);
if (trade.type === 'SELL') await executeSell(trade);
} catch (err) {
logger.error(`Error processing trade ${trade.id}: ${err.message}`);
}
}
// ── Periodic status log (replaces TUI right panel) ────────────────────────────
async function printStatus() {
try {
const balance = await getUsdcBalance();
const positions = getOpenPositions();
logger.info(`--- Status | Balance: $${balance.toFixed(2)} USDC | Open positions: ${positions.length} ---`);
for (const pos of positions) {
let pnlStr = '';
try {
const client = getClient();
const mp = await client.getMidpoint(pos.tokenId);
const mid = parseFloat(mp?.mid ?? mp ?? '0');
if (mid > 0) {
const pnl = (mid - pos.avgBuyPrice) * pos.shares;
const sign = pnl >= 0 ? '+' : '';
const pct = pos.totalCost > 0 ? ((pnl / pos.totalCost) * 100).toFixed(1) : '0.0';
pnlStr = ` | unrealized ${sign}$${pnl.toFixed(2)} (${sign}${pct}%)`;
}
} catch { /* price unavailable */ }
const name = (pos.market || pos.tokenId || '').substring(0, 50);
logger.info(
` [${pos.outcome || '?'}] ${name}` +
` | ${pos.shares.toFixed(4)} sh @ $${pos.avgBuyPrice.toFixed(4)}` +
` | spent $${(pos.totalCost || 0).toFixed(2)}${pnlStr}`,
);
}
if (config.dryRun) {
const s = getSimStats();
if (s.totalBuys > 0 || s.totalResolved > 0) {
const rate = s.totalResolved > 0
? `${((s.wins / s.totalResolved) * 100).toFixed(0)}% win`
: 'no resolved yet';
logger.info(
` [SIM] ${s.totalBuys} buys tracked | ${s.wins}W/${s.losses}L (${rate})` +
` | realized P&L: $${(s.closedPnl || 0).toFixed(2)}`,
);
}
}
} catch (err) {
logger.warn(`Status check error: ${err.message}`);
}
}
// ── Redeemer loop ─────────────────────────────────────────────────────────────
async function redeemerLoop() {
try {
await checkAndRedeemPositions();
} catch (err) {
logger.error('Redeemer loop error:', err.message);
}
}
// ── Main ──────────────────────────────────────────────────────────────────────
async function main() {
try {
validateConfig();
} catch (err) {
logger.error(err.message);
process.exit(1);
}
const mode = config.dryRun ? 'SIMULATION' : 'LIVE TRADING';
logger.info(`=== Polymarket Copy Trade [${mode}] ===`);
logger.info(`Trader : ${config.traderAddress}`);
logger.info(`Proxy wallet : ${config.proxyWallet}`);
logger.info(`Size mode : ${config.sizeMode} (${config.sizePercent}%)`);
logger.info(`Min trade : $${config.minTradeSize}`);
logger.info(`Max position : $${config.maxPositionSize} per market`);
logger.info(`Auto sell : ${config.autoSellEnabled ? `ON (+${config.autoSellProfitPercent}%)` : 'OFF'}`);
logger.info(`Sell mode : ${config.sellMode}`);
logger.info(`Min time left: ${config.minMarketTimeLeft}s`);
logger.info('==========================================');
try {
await initClient();
} catch (err) {
logger.error('Failed to initialize CLOB client:', err.message);
process.exit(1);
}
try {
const balance = await getUsdcBalance();
logger.money(`USDC.e Balance: $${balance.toFixed(2)}`);
} catch (err) {
logger.warn('Could not fetch balance:', err.message);
}
logger.success(
config.dryRun
? 'Simulation started — watching trader in real-time...'
: 'Bot started — watching trader in real-time...',
);
startWsWatcher(handleTrade);
await redeemerLoop();
const redeemerInterval = setInterval(redeemerLoop, config.redeemInterval);
// Print status every 60 seconds
const statusInterval = setInterval(printStatus, 60_000);
const shutdown = () => {
logger.info('Shutting down...');
stopWsWatcher();
clearInterval(redeemerInterval);
clearInterval(statusInterval);
setTimeout(() => process.exit(0), 300);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
}
main().catch((err) => {
logger.error('Fatal error:', err.message);
process.exit(1);
});