diff --git a/lib/strategies/martingale.js b/lib/strategies/martingale.js new file mode 100644 index 0000000..2f12f88 --- /dev/null +++ b/lib/strategies/martingale.js @@ -0,0 +1,109 @@ +import { BaseStrategy } from './base.js'; + +/** + * Martingale Strategy + * + * Logic: + * - If one side is ~70%+ (configurable), bet the opposite side. + * - On loss, double the bet size (Martingale). + * - On win, reset to base bet size. + * - Max consecutive losses cap to prevent blowup. + */ +export class MartingaleStrategy extends BaseStrategy { + constructor(config = {}) { + super('martingale', { + threshold: config.threshold || 70, // Trigger when one side >= this % + baseBet: config.baseBet || 1, // Base bet in dollars + maxDoublings: config.maxDoublings || 5, // Max consecutive losses before stopping + cooldownMs: config.cooldownMs || 60000, // Min time between trades (1 min) + ...config + }); + + this.consecutiveLosses = 0; + this.currentBetSize = this.config.baseBet; + this.lastTradeTime = 0; + this.lastTradeTicker = null; + } + + evaluate(state) { + if (!state || !this.enabled) return null; + + const now = Date.now(); + + // Cooldown — don't spam trades + if (now - this.lastTradeTime < this.config.cooldownMs) return null; + + // Don't trade same ticker twice + if (state.ticker === this.lastTradeTicker) return null; + + // Check if Martingale limit reached + if (this.consecutiveLosses >= this.config.maxDoublings) { + return null; // Paused — too many consecutive losses + } + + const { yesPct, noPct } = state; + const threshold = this.config.threshold; + + let signal = null; + + // If "Yes" is at 70%+, bet "No" (the underdog) + if (yesPct >= threshold) { + signal = { + strategy: this.name, + side: 'no', + price: noPct, + size: this.currentBetSize, + reason: `Yes at ${yesPct}% (≥${threshold}%), betting No at ${noPct}¢`, + ticker: state.ticker + }; + } + // If "No" is at 70%+, bet "Yes" (the underdog) + else if (noPct >= threshold) { + signal = { + strategy: this.name, + side: 'yes', + price: yesPct, + size: this.currentBetSize, + reason: `No at ${noPct}% (≥${threshold}%), betting Yes at ${yesPct}¢`, + ticker: state.ticker + }; + } + + if (signal) { + this.lastTradeTime = now; + this.lastTradeTicker = state.ticker; + } + + return signal; + } + + onSettlement(result, trade) { + if (!trade || trade.strategy !== this.name) return; + + const won = (trade.side === 'yes' && result === 'yes') || + (trade.side === 'no' && result === 'no'); + + if (won) { + console.log(`[Martingale] WIN — resetting to base bet $${this.config.baseBet}`); + this.consecutiveLosses = 0; + this.currentBetSize = this.config.baseBet; + } else { + this.consecutiveLosses++; + this.currentBetSize = this.config.baseBet * Math.pow(2, this.consecutiveLosses); + console.log(`[Martingale] LOSS #${this.consecutiveLosses} — next bet: $${this.currentBetSize}`); + + if (this.consecutiveLosses >= this.config.maxDoublings) { + console.log(`[Martingale] MAX LOSSES REACHED. Strategy paused.`); + } + } + } + + toJSON() { + return { + ...super.toJSON(), + consecutiveLosses: this.consecutiveLosses, + currentBetSize: this.currentBetSize, + paused: this.consecutiveLosses >= this.config.maxDoublings + }; + } +}