Files
polymarket-terminal/src/services/sniperDetector.js
T
direkturcrypto 526076fe6e feat: full project overhaul — market maker, sniper, WebSocket watcher, terminal UI
- Rename project to polymarket-terminal
- Add Market Maker bot (src/mm.js) with on-chain CTF split/merge/redeem via Gnosis Safe
- Add Orderbook Sniper bot (src/sniper.js) with multi-asset GTC low-price orders
- Add WebSocket watcher (src/services/wsWatcher.js) for real-time RTDS trade events
- Add terminal dashboard UI (src/ui/dashboard.js) using blessed
- Add CTF contract helpers (src/services/ctf.js) for splitPosition, mergePositions, redeemPositions
- Add mmDetector, mmExecutor, sniperDetector, sniperExecutor services
- Add simStats utility for dry-run P&L tracking
- Translate all Indonesian-language strings to professional English across all files
- Rewrite README.md in English with full setup guide, configuration reference, and architecture overview
- Rewrite AGENT.MD in English as comprehensive AI agent and developer reference
- Update package.json name, description, scripts, and keywords

Co-Authored-By: direkturcrypto <direkturcrypto.x@mail3.me>
2026-02-23 23:03:06 +07:00

157 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* sniperDetector.js
* Detects upcoming 5-minute markets for configured assets (ETH, SOL, XRP, …)
* using deterministic slug construction — same logic as mmDetector but for
* multiple assets simultaneously.
*
* Slug format: {asset}-updown-5m-{eventStartTimestamp}
* e.g. eth-updown-5m-1771790700
* sol-updown-5m-1771790700
* xrp-updown-5m-1771790700
*
* NEVER enters the currently active market — always the NEXT upcoming slot.
*/
import config from '../config/index.js';
import logger from '../utils/logger.js';
const SLOT_SEC = 5 * 60; // 300 seconds
let pollTimer = null;
let onMarketCb = null;
const seenKeys = new Set(); // `${asset}-${slotTimestamp}` already handled
// ── Slot helpers ──────────────────────────────────────────────────────────────
function currentSlot() {
return Math.floor(Date.now() / 1000 / SLOT_SEC) * SLOT_SEC;
}
function nextSlot() {
return currentSlot() + SLOT_SEC;
}
// ── Gamma API fetch ───────────────────────────────────────────────────────────
async function fetchBySlug(asset, slotTimestamp) {
const slug = `${asset}-updown-5m-${slotTimestamp}`;
try {
const resp = await fetch(`${config.gammaHost}/markets/slug/${slug}`);
if (!resp.ok) return null;
const data = await resp.json();
return data?.conditionId ? data : null;
} catch {
return null;
}
}
// ── Market data extraction ────────────────────────────────────────────────────
function extractMarketData(market, asset) {
const conditionId = market.conditionId || market.condition_id || '';
if (!conditionId) return null;
let tokenIds = market.clobTokenIds ?? market.clob_token_ids;
if (typeof tokenIds === 'string') {
try { tokenIds = JSON.parse(tokenIds); } catch { tokenIds = null; }
}
let yesTokenId, noTokenId;
if (Array.isArray(tokenIds) && tokenIds.length >= 2) {
[yesTokenId, noTokenId] = tokenIds;
} else if (Array.isArray(market.tokens) && market.tokens.length >= 2) {
yesTokenId = market.tokens[0]?.token_id ?? market.tokens[0]?.tokenId;
noTokenId = market.tokens[1]?.token_id ?? market.tokens[1]?.tokenId;
}
if (!yesTokenId || !noTokenId) return null;
return {
asset,
conditionId,
question: market.question || market.title || '',
endTime: market.endDate || market.end_date_iso || market.endDateIso,
eventStartTime: market.eventStartTime || market.event_start_time,
yesTokenId: String(yesTokenId),
noTokenId: String(noTokenId),
negRisk: market.negRisk ?? market.neg_risk ?? false,
tickSize: String(market.orderPriceMinTickSize ?? market.minimum_tick_size ?? '0.01'),
};
}
// ── Schedule an asset slot ────────────────────────────────────────────────────
async function scheduleAsset(asset, slotTimestamp, isCurrent = false) {
const key = `${asset}-${slotTimestamp}`;
if (seenKeys.has(key)) return;
const market = await fetchBySlug(asset, slotTimestamp);
if (!market) return; // not in API yet, poll will retry
const data = extractMarketData(market, asset);
if (!data) {
logger.warn(`SNIPER: skipping ${asset} slot ${slotTimestamp} — missing token IDs`);
seenKeys.add(key);
return;
}
seenKeys.add(key);
if (isCurrent) {
// Current slot: only place orders if there's at least 30 seconds of market left
const endAt = data.endTime ? new Date(data.endTime).getTime() : (slotTimestamp + SLOT_SEC) * 1000;
const secsLeft = Math.round((endAt - Date.now()) / 1000);
if (secsLeft < 30) {
logger.info(`SNIPER: ${asset.toUpperCase()} current market closing soon (${secsLeft}s) — skipping`);
return;
}
logger.success(`SNIPER: ${asset.toUpperCase()} current market active (${secsLeft}s left) — placing orders now`);
} else {
// Next slot: market hasn't opened yet
const openAt = data.eventStartTime ? new Date(data.eventStartTime).getTime() : slotTimestamp * 1000;
const secsUntilOpen = Math.round((openAt - Date.now()) / 1000);
logger.success(`SNIPER: ${asset.toUpperCase()} found "${data.question.slice(0, 40)}"${secsUntilOpen > 0 ? ` — ${secsUntilOpen}s before open` : ''}`);
}
if (onMarketCb) onMarketCb(data);
}
// ── Poll ──────────────────────────────────────────────────────────────────────
async function poll() {
try {
const curr = currentSlot();
const next = nextSlot();
// Check current active market AND the upcoming next one, in parallel for each asset
await Promise.all(config.sniperAssets.flatMap((asset) => [
scheduleAsset(asset, curr, true), // current market (if still has time left)
scheduleAsset(asset, next, false), // next upcoming market
]));
} catch (err) {
logger.error('SNIPER detector poll error:', err.message);
}
}
// ── Public API ────────────────────────────────────────────────────────────────
export function startSniperDetector(onNewMarket) {
onMarketCb = onNewMarket;
seenKeys.clear();
poll();
pollTimer = setInterval(poll, config.mmPollInterval);
const ns = nextSlot();
const secsUntil = ns - Math.floor(Date.now() / 1000);
logger.info(`SNIPER detector started — assets: ${config.sniperAssets.join(', ').toUpperCase()}`);
logger.info(`Next slot: *-updown-5m-${ns} (opens in ${secsUntil}s)`);
logger.info(`Order: $${config.sniperPrice} × ${config.sniperShares} shares per side`);
}
export function stopSniperDetector() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}