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, slippage: config.slippage || 3, cooldownMs: config.cooldownMs || 20000, ...config }); this.round = 0; this.currentBetSize = this.config.baseBet; this.cycleWins = 0; this.cycleLosses = 0; this.totalCycles = 0; this._lastTrade = { paper: { time: 0, ticker: null }, live: { time: 0, ticker: null } }; } evaluate(state, caller = 'paper') { if (!state || !this.enabled) 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 { yesPct, noPct } = state; const { minPct, maxPct } = this.config; if (yesPct < minPct || yesPct > maxPct) return null; if (noPct < minPct || noPct > maxPct) return null; const flip = crypto.randomInt(0, 2); const side = flip === 0 ? 'yes' : 'no'; const price = side === 'yes' ? yesPct : noPct; const maxPrice = Math.min(price + this.config.slippage, 95); const roundIndex = this.round; const betSize = this.config.baseBet * Math.pow(2, roundIndex); const signal = { strategy: this.name, side, price, maxPrice, size: betSize, reason: `R${roundIndex + 1}/${this.config.maxRounds} coin-flip ${side.toUpperCase()} @ ${price}ยข ($${betSize}) | Market: ${yesPct}/${noPct}`, ticker: state.ticker }; track.time = now; track.ticker = 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; // Reset both callers' ticker locks so new cycle can trade same market this._lastTrade.paper.ticker = null; this._lastTrade.live.ticker = null; } 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 }; } }