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:
@@ -2,3 +2,4 @@ node_modules/
|
||||
.env
|
||||
data/*.json
|
||||
.DS_Store
|
||||
logs/*.log
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user