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:
direkturcrypto
2026-03-01 16:24:25 +07:00
parent f4490c12ac
commit f31f257da5
14 changed files with 803 additions and 139 deletions
+34 -68
View File
@@ -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);