diff --git a/lib/strategies/late-momentum.js b/lib/strategies/late-momentum.js new file mode 100644 index 0000000..057f89c --- /dev/null +++ b/lib/strategies/late-momentum.js @@ -0,0 +1,71 @@ +import { BaseStrategy } from './base.js'; + +/** + * Late Momentum Strategy + * + * Rules: + * - Only buys YES momentum + * - Requires YES 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 || 2, + 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 5 minutes; trade only in configured late window. + if (elapsedMin < this.config.minElapsedMin || elapsedMin > this.config.maxElapsedMin) return null; + + const yesPct = Number(state.yesPct); + if (!Number.isFinite(yesPct)) return null; + + if (yesPct >= this.config.triggerPct && yesPct < 99) { + const signal = { + strategy: this.name, + side: 'yes', + price: yesPct, + maxPrice: Math.min(yesPct + this.config.slippage, 95), + size: this.config.betSize, + reason: `Late momentum: t+${elapsedMin.toFixed(1)}m, YES @ ${yesPct}ยข`, + ticker: state.ticker + }; + + track.time = now; + track.ticker = state.ticker; + return signal; + } + + return null; + } +}