import { BaseStrategy } from './base.js'; export class EarlyFaderStrategy extends BaseStrategy { constructor(config = {}) { super('early-fader', { spikeThreshold: config.spikeThreshold || 82, // When a side spikes to this % maxElapsedMin: config.maxElapsedMin || 6, // Only fade early spikes (first 6 mins) betSize: config.betSize || 2, slippage: config.slippage || 3, cooldownMs: config.cooldownMs || 20000, marketDurationMin: config.marketDurationMin || 15, ...config }); this._lastTrade = { paper: { time: 0, ticker: null }, live: { time: 0, ticker: null } }; } evaluate(state, caller = 'paper') { if (!state || !this.enabled || !state.closeTime) return null; const track = this._lastTrade[caller] || this._lastTrade.paper; const now = Date.now(); if (now - track.time < this.config.cooldownMs) return null; if (state.ticker === track.ticker) return null; // 1 trade per market const closeTs = new Date(state.closeTime).getTime(); const timeLeftMs = closeTs - now; if (!Number.isFinite(timeLeftMs) || timeLeftMs <= 0) return null; const elapsedMin = (this.config.marketDurationMin * 60000 - timeLeftMs) / 60000; // Only trade in the designated early window if (elapsedMin < 0 || elapsedMin > this.config.maxElapsedMin) return null; const { yesPct, noPct } = state; const threshold = this.config.spikeThreshold; let targetSide = null; let targetPrice = null; // If YES spikes early, we fade it by buying NO if (yesPct >= threshold && yesPct <= 95) { targetSide = 'no'; targetPrice = noPct; } // If NO spikes early, we fade it by buying YES else if (noPct >= threshold && noPct <= 95) { targetSide = 'yes'; targetPrice = yesPct; } if (!targetSide) return null; // Ensure the opposite side is actually cheap (sanity check against weird book states) if (targetPrice > (100 - threshold + 5)) return null; // For live trading, check actual orderbook liquidity to avoid blind fills if (caller === 'live') { const bestAsk = this._getBestAsk(state, targetSide); if (bestAsk == null) return null; if (bestAsk > targetPrice + this.config.slippage) return null; } const spikeSide = targetSide === 'no' ? 'YES' : 'NO'; const signal = { strategy: this.name, side: targetSide, price: targetPrice, maxPrice: Math.min(targetPrice + this.config.slippage, 40), size: this.config.betSize, reason: `Fading early spike (t+${elapsedMin.toFixed(1)}m): ${spikeSide} > ${threshold}¢. Bought ${targetSide.toUpperCase()} @ ${targetPrice}¢`, ticker: state.ticker }; track.time = now; track.ticker = state.ticker; return signal; } _getBestAsk(state, side) { const ob = state.orderbook; if (!ob) return null; const oppSide = side === 'yes' ? 'no' : 'yes'; if (ob[oppSide]?.length) { const bestOppBid = ob[oppSide][0]?.[0]; if (bestOppBid != null) return 100 - bestOppBid; } return null; } toJSON() { return { ...super.toJSON(), lastTradeTicker: this._lastTrade.live.ticker || this._lastTrade.paper.ticker }; } }