feat: sniper bot updates — TUI separation, session scheduling, proxy support, win-only redeem
- Separate TUI from sniper: npm run sniper (console), npm run sniper-tui (TUI) - Add trading session scheduling per asset (BTC/ETH/SOL/XRP) in UTC+8 - Add PROXY_URL support for Polymarket CLOB/Gamma/Data APIs (not Polygon RPC) - Create redeemSniperPositions: win-only filter, bulk MultiSend batching - Add gas estimation with 10s timeout in execSafeCall - Replace native fetch with proxyFetch in all Polymarket API services
This commit is contained in:
+34
-68
@@ -1,21 +1,22 @@
|
||||
/**
|
||||
* sniper.js
|
||||
* Entry point for the Orderbook Sniper bot.
|
||||
* Places tiny GTC BUY orders at $0.01 on both sides of ETH/SOL/XRP 5-min markets.
|
||||
* Console-only entry point for the Orderbook Sniper bot.
|
||||
* Places tiny GTC BUY orders at a low price on both sides of 5-min markets.
|
||||
*
|
||||
* Run with: npm run sniper (live)
|
||||
* npm run sniper-sim (simulation)
|
||||
* Run with: npm run sniper (live, console)
|
||||
* npm run sniper-sim (simulation, console)
|
||||
*
|
||||
* For the TUI dashboard version, use: npm run sniper-tui
|
||||
*/
|
||||
|
||||
import { validateMMConfig } from './config/index.js';
|
||||
import config from './config/index.js';
|
||||
import logger from './utils/logger.js';
|
||||
import { initClient } from './services/client.js';
|
||||
import { getUsdcBalance } from './services/client.js';
|
||||
import { initDashboard, appendLog, updateStatus, isDashboardActive } from './ui/dashboard.js';
|
||||
import { startSniperDetector, stopSniperDetector } from './services/sniperDetector.js';
|
||||
import { executeSnipe, getActiveSnipes } from './services/sniperExecutor.js';
|
||||
import { redeemMMPositions } from './services/ctf.js';
|
||||
import { executeSnipe } from './services/sniperExecutor.js';
|
||||
import { redeemSniperPositions } from './services/ctf.js';
|
||||
import { getSchedule, isAssetInSession, getNextSessionInfo } from './services/schedule.js';
|
||||
|
||||
// ── Validate config ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -31,11 +32,6 @@ if (config.sniperAssets.length === 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── Init TUI ──────────────────────────────────────────────────────────────────
|
||||
|
||||
initDashboard();
|
||||
logger.setOutput(appendLog);
|
||||
|
||||
// ── Init CLOB client ──────────────────────────────────────────────────────────
|
||||
|
||||
try {
|
||||
@@ -45,66 +41,37 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── Status panel ──────────────────────────────────────────────────────────────
|
||||
// ── Log session schedule ──────────────────────────────────────────────────────
|
||||
|
||||
async function buildStatusContent() {
|
||||
const lines = [];
|
||||
|
||||
// Balance
|
||||
let balance = '?';
|
||||
if (!config.dryRun) {
|
||||
try { balance = (await getUsdcBalance()).toFixed(2); } catch { /* ignore */ }
|
||||
} else {
|
||||
balance = '{yellow-fg}SIM{/yellow-fg}';
|
||||
}
|
||||
lines.push('{bold}BALANCE{/bold}');
|
||||
lines.push(` USDC.e: {green-fg}$${balance}{/green-fg}`);
|
||||
lines.push('');
|
||||
|
||||
lines.push('{bold}MODE{/bold}');
|
||||
lines.push(` ${config.dryRun ? '{yellow-fg}SIMULATION{/yellow-fg}' : '{green-fg}LIVE{/green-fg}'}`);
|
||||
lines.push('');
|
||||
|
||||
lines.push('{bold}SNIPER CONFIG{/bold}');
|
||||
lines.push(` Assets : ${config.sniperAssets.join(', ').toUpperCase()}`);
|
||||
lines.push(` Price : $${config.sniperPrice} per share`);
|
||||
lines.push(` Shares : ${config.sniperShares} per side`);
|
||||
lines.push(` Cost : $${(config.sniperPrice * config.sniperShares * 2 * config.sniperAssets.length).toFixed(3)} per slot`);
|
||||
lines.push('');
|
||||
|
||||
// Recent snipe orders
|
||||
const snipes = getActiveSnipes();
|
||||
lines.push(`{bold}SNIPE ORDERS (${snipes.length} total){/bold}`);
|
||||
|
||||
if (snipes.length === 0) {
|
||||
lines.push(' {gray-fg}Waiting for next slot...{/gray-fg}');
|
||||
} else {
|
||||
// Show last 10 orders (most recent first)
|
||||
const recent = snipes.slice(-10).reverse();
|
||||
for (const s of recent) {
|
||||
const payout = s.potentialPayout.toFixed(2);
|
||||
lines.push(` {cyan-fg}${s.asset}{/cyan-fg} ${s.side} @ $${s.price} × ${s.shares}sh | pay $${payout} if win`);
|
||||
function logSchedule() {
|
||||
const schedule = getSchedule();
|
||||
logger.info('─── Session Schedule (UTC+8) ───');
|
||||
for (const asset of config.sniperAssets) {
|
||||
const sessions = schedule[asset];
|
||||
const active = isAssetInSession(asset);
|
||||
const status = active ? '● ACTIVE' : '○ IDLE';
|
||||
if (sessions) {
|
||||
const sessionStr = sessions.map(s => `${s.startUtc8}–${s.endUtc8}`).join(', ');
|
||||
logger.info(` ${asset.toUpperCase()} [${status}] ${sessionStr}`);
|
||||
if (!active) {
|
||||
const next = getNextSessionInfo(asset);
|
||||
if (next) logger.info(` → Next in ${next}`);
|
||||
}
|
||||
} else {
|
||||
logger.info(` ${asset.toUpperCase()} [NO SCHEDULE] (always active)`);
|
||||
}
|
||||
}
|
||||
|
||||
return '\n' + lines.join('\n');
|
||||
logger.info('────────────────────────────────');
|
||||
}
|
||||
|
||||
let refreshTimer = null;
|
||||
let redeemTimer = null;
|
||||
// ── Redeemer ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function startRefresh() {
|
||||
refreshTimer = setInterval(async () => {
|
||||
if (!isDashboardActive()) return;
|
||||
updateStatus(await buildStatusContent());
|
||||
}, 3000);
|
||||
buildStatusContent().then(updateStatus);
|
||||
}
|
||||
let redeemTimer = null;
|
||||
|
||||
function startRedeemer() {
|
||||
redeemMMPositions().catch((err) => logger.error('Sniper redeemer error:', err.message));
|
||||
redeemSniperPositions().catch((err) => logger.error('Sniper redeemer error:', err.message));
|
||||
redeemTimer = setInterval(
|
||||
() => redeemMMPositions().catch((err) => logger.error('Sniper redeemer error:', err.message)),
|
||||
() => redeemSniperPositions().catch((err) => logger.error('Sniper redeemer error:', err.message)),
|
||||
config.redeemInterval,
|
||||
);
|
||||
logger.info(`Sniper redeemer started — checking every ${config.redeemInterval / 1000}s`);
|
||||
@@ -123,12 +90,11 @@ async function handleNewMarket(market) {
|
||||
function shutdown() {
|
||||
logger.warn('SNIPER: shutting down...');
|
||||
stopSniperDetector();
|
||||
if (refreshTimer) clearInterval(refreshTimer);
|
||||
if (redeemTimer) clearInterval(redeemTimer);
|
||||
if (redeemTimer) clearInterval(redeemTimer);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
|
||||
// ── Start ─────────────────────────────────────────────────────────────────────
|
||||
@@ -137,6 +103,6 @@ const costPerSlot = (config.sniperPrice * config.sniperShares * 2 * config.snipe
|
||||
logger.info(`SNIPER starting — ${config.dryRun ? 'SIMULATION' : 'LIVE'}`);
|
||||
logger.info(`Assets: ${config.sniperAssets.join(', ').toUpperCase()} | $${config.sniperPrice} × ${config.sniperShares}sh = $${costPerSlot}/slot`);
|
||||
|
||||
startRefresh();
|
||||
logSchedule();
|
||||
startRedeemer();
|
||||
startSniperDetector(handleNewMarket);
|
||||
|
||||
Reference in New Issue
Block a user