diff --git a/lib/strategies/martingale-alpha.js b/lib/strategies/martingale-alpha.js new file mode 100644 index 0000000..fb43987 --- /dev/null +++ b/lib/strategies/martingale-alpha.js @@ -0,0 +1,120 @@ +import { BaseStrategy } from './base.js'; +import crypto from 'crypto'; + +/** + * Martingale Alpha Strategy + * + * When odds are between 40-60% for both sides (a ~coin-flip market): + * - Use crypto.randomInt to pick yes/no + * - Round 1: bet $1, Round 2: bet $2, Round 3: bet $4 + * - If any round wins, reset to round 1 + * - If all 3 lose, reset to round 1 anyway (cap losses at $7 per cycle) + * + * Probability of losing 3 consecutive 50/50s = 12.5% + * Probability of winning at least 1 of 3 = 87.5% + */ +export class MartingaleAlphaStrategy extends BaseStrategy { + constructor(config = {}) { + super('martingale-alpha', { + minPct: config.minPct || 40, + maxPct: config.maxPct || 60, + baseBet: config.baseBet || 1, + maxRounds: config.maxRounds || 3, + cooldownMs: config.cooldownMs || 60000, + ...config + }); + + this.round = 0; // 0 = waiting, 1-3 = active round + this.currentBetSize = this.config.baseBet; + this.lastTradeTime = 0; + this.lastTradeTicker = null; + this.cycleWins = 0; + this.cycleLosses = 0; + this.totalCycles = 0; + } + + evaluate(state) { + if (!state || !this.enabled) return null; + + const now = Date.now(); + if (now - this.lastTradeTime < this.config.cooldownMs) return null; + if (state.ticker === this.lastTradeTicker) return null; + + const { yesPct, noPct } = state; + const { minPct, maxPct } = this.config; + + // Only trade when both sides are in the 40-60% range (coin-flip territory) + if (yesPct < minPct || yesPct > maxPct) return null; + if (noPct < minPct || noPct > maxPct) return null; + + // Secure random coin flip: 0 = yes, 1 = no + const flip = crypto.randomInt(0, 2); + const side = flip === 0 ? 'yes' : 'no'; + const price = side === 'yes' ? yesPct : noPct; + + // Determine bet size based on current round + const roundIndex = this.round; // 0, 1, or 2 + const betSize = this.config.baseBet * Math.pow(2, roundIndex); + + const signal = { + strategy: this.name, + side, + price, + size: betSize, + reason: `R${roundIndex + 1}/${this.config.maxRounds} coin-flip ${side.toUpperCase()} @ ${price}ยข ($${betSize}) | Market: ${yesPct}/${noPct}`, + ticker: state.ticker + }; + + this.lastTradeTime = now; + this.lastTradeTicker = state.ticker; + this.currentBetSize = betSize; + + return signal; + } + + onSettlement(result, trade) { + if (!trade || trade.strategy !== this.name) return; + + const won = trade.side === result; + + if (won) { + console.log(`[MartingaleAlpha] WIN on round ${this.round + 1} โ€” cycle complete, resetting`); + this.cycleWins++; + this.totalCycles++; + this._resetCycle(); + } else { + this.round++; + if (this.round >= this.config.maxRounds) { + console.log(`[MartingaleAlpha] LOST all ${this.config.maxRounds} rounds โ€” cycle failed, resetting`); + this.cycleLosses++; + this.totalCycles++; + this._resetCycle(); + } else { + const nextBet = this.config.baseBet * Math.pow(2, this.round); + console.log(`[MartingaleAlpha] LOSS round ${this.round}/${this.config.maxRounds} โ€” next bet: $${nextBet}`); + this.currentBetSize = nextBet; + } + } + } + + _resetCycle() { + this.round = 0; + this.currentBetSize = this.config.baseBet; + this.lastTradeTicker = null; // Allow trading same ticker in new cycle + } + + toJSON() { + return { + ...super.toJSON(), + round: this.round + 1, + maxRounds: this.config.maxRounds, + currentBetSize: this.currentBetSize, + cycleWins: this.cycleWins, + cycleLosses: this.cycleLosses, + totalCycles: this.totalCycles, + cycleWinRate: this.totalCycles > 0 + ? parseFloat(((this.cycleWins / this.totalCycles) * 100).toFixed(1)) + : 0 + }; + } +}