Files
polymarket-terminal/src/services/mmDetector.js
T
direkturcrypto f31f257da5 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
2026-03-01 16:24:25 +07:00

153 lines
6.1 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.
/**
* mmDetector.js
* Detects upcoming markets for configured assets (BTC, ETH, SOL, …)
* using deterministic slug construction — supports 5-minute and 15-minute durations.
*
* Slug format: {asset}-updown-{duration}-{eventStartTimestamp}
* e.g. btc-updown-5m-1771755000
* eth-updown-15m-1771754100
*
* NEVER enters the currently active market — always targets the NEXT upcoming slot.
*/
import config from '../config/index.js';
import logger from '../utils/logger.js';
import { proxyFetch } from '../utils/proxy.js';
// Slot size in seconds (300 for 5m, 900 for 15m)
const SLOT_SEC = config.mmDuration === '15m' ? 900 : 300;
let pollTimer = null;
let onMarketCb = null;
const seenKeys = new Set(); // `${asset}-${slotTimestamp}` already scheduled
// ── 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-${config.mmDuration}-${slotTimestamp}`;
try {
const resp = await proxyFetch(`${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;
// clobTokenIds may arrive as a JSON string or an actual array
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 ?? market.minimumTickSize ?? '0.01'),
};
}
// ── Schedule an asset slot ────────────────────────────────────────────────────
async function scheduleAsset(asset, slotTimestamp) {
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(`MM: skipping ${asset.toUpperCase()} slot ${slotTimestamp} — missing token IDs`);
seenKeys.add(key);
return;
}
seenKeys.add(key);
// Refuse to enter a market already well into its window (e.g., bot restart mid-slot)
const openAt = data.eventStartTime ? new Date(data.eventStartTime).getTime() : slotTimestamp * 1000;
const elapsedSec = Math.round((Date.now() - openAt) / 1000);
if (elapsedSec > 15) {
logger.info(`MM: ${asset.toUpperCase()} next slot already ${elapsedSec}s old — skipping, will catch next`);
return;
}
const secsUntilOpen = Math.round((openAt - Date.now()) / 1000);
if (secsUntilOpen > 0) {
logger.success(`MM: ${asset.toUpperCase()} found "${data.question.slice(0, 40)}" — splitting position now (${secsUntilOpen}s before open)`);
} else {
logger.success(`MM: ${asset.toUpperCase()} found "${data.question.slice(0, 40)}" — splitting position now`);
}
if (onMarketCb) onMarketCb(data);
}
// ── Poll ──────────────────────────────────────────────────────────────────────
async function poll() {
try {
// Schedule NEXT slot only — never the currently active market
const next = nextSlot();
await Promise.all(config.mmAssets.map((asset) => scheduleAsset(asset, next)));
} catch (err) {
logger.error('MM detector poll error:', err.message);
}
}
// ── Public API ────────────────────────────────────────────────────────────────
export function startMMDetector(onNewMarket) {
onMarketCb = onNewMarket;
seenKeys.clear();
poll();
pollTimer = setInterval(poll, config.mmPollInterval);
const ns = nextSlot();
const secsUntil = ns - Math.floor(Date.now() / 1000);
logger.info(`MM detector started — assets: ${config.mmAssets.join(', ').toUpperCase()} | duration: ${config.mmDuration}`);
logger.info(`Next slot: *-updown-${config.mmDuration}-${ns} (opens in ${secsUntil}s)`);
logger.info(`Order: $${config.mmTradeSize}/side × 2 sides = $${config.mmTradeSize * 2} per market`);
}
export function stopMMDetector() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}