Feat: Add martingale-alpha coin-flip 3-round strat

This commit is contained in:
2026-03-15 16:50:25 -07:00
parent 647b46d1b8
commit 0adcc947ce

View File

@@ -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
};
}
}