import { BaseStrategy } from './base.js'; /** * Late Momentum Strategy * * Rules: * - Buys momentum on YES or NO * - Requires side price >= triggerPct (default 75) * - Only allowed after first 5 minutes of market lifecycle * - Default window: elapsed minute 5 through 15 */ export class LateMomentumStrategy extends BaseStrategy { constructor(config = {}) { super('late-momentum', { triggerPct: config.triggerPct || 75, betSize: config.betSize || 4, slippage: config.slippage || 3, cooldownMs: config.cooldownMs || 20000, marketDurationMin: config.marketDurationMin || 15, minElapsedMin: config.minElapsedMin || 5, maxElapsedMin: config.maxElapsedMin || 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; 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; if (!Number.isFinite(elapsedMin)) return null; // Skip first minutes; trade only in configured late window. if (elapsedMin < this.config.minElapsedMin || elapsedMin > this.config.maxElapsedMin) return null; const yesPct = Number(state.yesPct); const noPct = Number(state.noPct); if (!Number.isFinite(yesPct) || !Number.isFinite(noPct)) return null; const trigger = this.config.triggerPct; const candidates = []; if (yesPct >= trigger && yesPct < 99) candidates.push({ side: 'yes', pct: yesPct }); if (noPct >= trigger && noPct < 99) candidates.push({ side: 'no', pct: noPct }); if (!candidates.length) return null; // Prefer the stronger momentum side if both qualify. candidates.sort((a, b) => b.pct - a.pct); const pick = candidates[0]; const signal = { strategy: this.name, side: pick.side, price: pick.pct, maxPrice: Math.min(pick.pct + this.config.slippage, 95), size: this.config.betSize, reason: `Late momentum: t+${elapsedMin.toFixed(1)}m, ${pick.side.toUpperCase()} @ ${pick.pct}ยข`, ticker: state.ticker }; track.time = now; track.ticker = state.ticker; return signal; } }