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
|
.env
|
||||||
data/*.json
|
data/*.json
|
||||||
.DS_Store
|
.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": {
|
"scripts": {
|
||||||
"start": "node src/index.js",
|
"start": "node src/index.js",
|
||||||
"dev": "nodemon --ignore 'data/*.json' 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": "DRY_RUN=false node src/mm.js",
|
||||||
"mm-sim": "DRY_RUN=true 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-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