Compare commits

...

15 Commits

8 changed files with 245 additions and 185 deletions

View File

@@ -11,7 +11,6 @@ export default function LiveDashboard() {
const [trades, setTrades] = useState([]);
const [loading, setLoading] = useState(true);
const [toggling, setToggling] = useState(null);
const [activeTrade, setActiveTrade] = useState(null);
useEffect(() => {
const fetchState = async () => {
@@ -156,7 +155,7 @@ export default function LiveDashboard() {
<p className="text-sm font-medium capitalize text-gray-200">{s.name}</p>
{s.config && (
<p className="text-[10px] text-gray-600">
${s.config.betSize || 1}/trade {s.config.cooldownMs ? `${(s.config.cooldownMs/1000).toFixed(0)}s cd` : ''}
${s.config.betSize || 1}/trade {s.config.cooldownMs ? `${(s.config.cooldownMs / 1000).toFixed(0)}s cd` : ''}
</p>
)}
</div>
@@ -296,9 +295,28 @@ function MarketCard({ market }) {
function LiveOrderRow({ order, isOpen }) {
const won = order.result && order.side?.toLowerCase() === order.result?.toLowerCase();
const isNeutral = order.result === 'cancelled' || order.result === 'expired';
const pnlVal = order.pnl != null ? (typeof order.pnl === 'number' && Math.abs(order.pnl) > 50 ? order.pnl / 100 : order.pnl) : null;
const rawPnl = order?.pnl != null ? Number(order.pnl) : null;
const pnlVal = Number.isFinite(rawPnl)
? (Number.isInteger(rawPnl) ? rawPnl / 100 : rawPnl)
: null;
const pnlColor = pnlVal == null ? 'text-gray-600' : pnlVal > 0 ? 'text-green-400' : pnlVal < 0 ? 'text-red-400' : 'text-gray-400';
const priceRaw = Number(order.priceCents ?? order.price);
const priceCents = Number.isFinite(priceRaw)
? (priceRaw > 0 && priceRaw <= 1 ? Math.round(priceRaw * 100) : Math.round(priceRaw))
: null;
const contractsRaw = Number(order.contracts ?? 1);
const contracts = Number.isFinite(contractsRaw) && contractsRaw > 0 ? contractsRaw : 1;
const hasExpected = isOpen && priceCents != null && priceCents > 0 && priceCents < 100;
const grossPayout = hasExpected ? contracts : null; // $1 per winning contract
const cost = hasExpected ? (priceCents / 100) * contracts : null;
const winPnL = hasExpected ? grossPayout - cost : null;
const losePnL = hasExpected ? -cost : null;
return (
<div className="bg-gray-900 rounded-lg p-3 border border-gray-800 mb-2">
<div className="flex items-center justify-between">
@@ -316,10 +334,26 @@ function LiveOrderRow({ order, isOpen }) {
{pnlVal != null ? `${pnlVal >= 0 ? '+' : ''}$${pnlVal.toFixed(2)}` : 'open'}
</span>
</div>
<div className="flex justify-between mt-1">
<span className="text-[10px] text-gray-600">{order.reason}</span>
<span className="text-[10px] text-gray-600 capitalize">{order.strategy}</span>
</div>
{hasExpected && (
<div className="mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-[10px] text-gray-500">
<span>
If win: <span className="text-green-400 font-medium">+${winPnL.toFixed(2)}</span>
</span>
<span>
If lose: <span className="text-red-400 font-medium">${losePnL.toFixed(2)}</span>
</span>
<span>
Gross payout: <span className="text-gray-300 font-medium">${grossPayout.toFixed(2)}</span>
</span>
</div>
)}
{order.result && !isOpen && (
<div className="flex justify-between items-center mt-0.5">
<span className="text-[10px] text-gray-600">Result: {order.result}</span>

View File

@@ -345,6 +345,14 @@ function TradeRow({ trade, isOpen }) {
const isNeutral = trade.result === 'cancelled' || trade.result === 'expired';
const pnlColor = trade.pnl == null ? 'text-gray-400' : trade.pnl > 0 ? 'text-green-600' : trade.pnl < 0 ? 'text-red-600' : 'text-gray-600';
const price = Number(trade.price);
const cost = Number(trade.cost ?? trade.size);
const hasExpected = isOpen && Number.isFinite(price) && price > 0 && price < 100 && Number.isFinite(cost) && cost > 0;
const grossPayout = hasExpected ? (100 / price) * cost : null;
const winPnL = hasExpected ? grossPayout - cost : null;
const losePnL = hasExpected ? -cost : null;
return (
<div className="bg-white rounded-lg p-3 border border-gray-200 mb-2 shadow-sm">
<div className="flex items-center justify-between">
@@ -362,9 +370,25 @@ function TradeRow({ trade, isOpen }) {
{trade.pnl != null ? `${trade.pnl >= 0 ? '+' : ''}$${trade.pnl}` : 'open'}
</span>
</div>
<div className="flex justify-between mt-1">
<span className="text-[10px] text-gray-400">{trade.reason}</span>
</div>
{hasExpected && (
<div className="mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-[10px] text-gray-500">
<span>
If win: <span className="text-green-600 font-medium">+${winPnL.toFixed(2)}</span>
</span>
<span>
If lose: <span className="text-red-600 font-medium">${losePnL.toFixed(2)}</span>
</span>
<span>
Gross payout: <span className="text-gray-700 font-medium">${grossPayout.toFixed(2)}</span>
</span>
</div>
)}
<div className="flex justify-between items-center mt-0.5">
{trade.result && !isOpen && (
<span className="text-[10px] text-gray-400">Result: {trade.result}</span>

View File

@@ -5,7 +5,7 @@ export class BullDipBuyer extends BaseStrategy {
super('bull-dip-buyer', {
maxYesPrice: config.maxYesPrice || 45,
minYesPrice: config.minYesPrice || 15,
betSize: config.betSize || 1,
betSize: config.betSize || 2,
slippage: config.slippage || 3,
cooldownMs: config.cooldownMs || 20000,
marketDurationMin: config.marketDurationMin || 15,

View File

@@ -1,54 +0,0 @@
import { BaseStrategy } from './base.js';
export class DontDoubtBullStrategy extends BaseStrategy {
constructor(config = {}) {
super('dont-doubt-bull', {
minYesPct: config.minYesPct || 30,
maxYesPct: config.maxYesPct || 40,
betSize: config.betSize || 2,
slippage: config.slippage || 3,
cooldownMs: config.cooldownMs || 20000,
...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 timeLeftMs = new Date(state.closeTime).getTime() - now;
const minsLeft = timeLeftMs / 60000;
if (minsLeft > 14 || minsLeft < 10) return null;
const { yesPct } = state;
if (yesPct >= this.config.minYesPct && yesPct <= this.config.maxYesPct) {
const maxPrice = Math.min(yesPct + this.config.slippage, 95);
const signal = {
strategy: this.name,
side: 'yes',
price: yesPct,
maxPrice,
size: this.config.betSize,
reason: `Early Bullish Dip: ${minsLeft.toFixed(1)}m left, Yes @ ${yesPct}¢`,
ticker: state.ticker
};
track.time = now;
track.ticker = state.ticker;
return signal;
}
return null;
}
}

View File

@@ -0,0 +1,102 @@
import { BaseStrategy } from './base.js';
export class EarlyFaderStrategy extends BaseStrategy {
constructor(config = {}) {
super('early-fader', {
spikeThreshold: config.spikeThreshold || 82, // When a side spikes to this %
maxElapsedMin: config.maxElapsedMin || 6, // Only fade early spikes (first 6 mins)
betSize: config.betSize || 2,
slippage: config.slippage || 3,
cooldownMs: config.cooldownMs || 20000,
marketDurationMin: config.marketDurationMin || 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; // 1 trade per market
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;
// Only trade in the designated early window
if (elapsedMin < 0 || elapsedMin > this.config.maxElapsedMin) return null;
const { yesPct, noPct } = state;
const threshold = this.config.spikeThreshold;
let targetSide = null;
let targetPrice = null;
// If YES spikes early, we fade it by buying NO
if (yesPct >= threshold && yesPct <= 95) {
targetSide = 'no';
targetPrice = noPct;
}
// If NO spikes early, we fade it by buying YES
else if (noPct >= threshold && noPct <= 95) {
targetSide = 'yes';
targetPrice = yesPct;
}
if (!targetSide) return null;
// Ensure the opposite side is actually cheap (sanity check against weird book states)
if (targetPrice > (100 - threshold + 5)) return null;
// For live trading, check actual orderbook liquidity to avoid blind fills
if (caller === 'live') {
const bestAsk = this._getBestAsk(state, targetSide);
if (bestAsk == null) return null;
if (bestAsk > targetPrice + this.config.slippage) return null;
}
const spikeSide = targetSide === 'no' ? 'YES' : 'NO';
const signal = {
strategy: this.name,
side: targetSide,
price: targetPrice,
maxPrice: Math.min(targetPrice + this.config.slippage, 40),
size: this.config.betSize,
reason: `Fading early spike (t+${elapsedMin.toFixed(1)}m): ${spikeSide} > ${threshold}¢. Bought ${targetSide.toUpperCase()} @ ${targetPrice}¢`,
ticker: state.ticker
};
track.time = now;
track.ticker = state.ticker;
return signal;
}
_getBestAsk(state, side) {
const ob = state.orderbook;
if (!ob) return null;
const oppSide = side === 'yes' ? 'no' : 'yes';
if (ob[oppSide]?.length) {
const bestOppBid = ob[oppSide][0]?.[0];
if (bestOppBid != null) return 100 - bestOppBid;
}
return null;
}
toJSON() {
return {
...super.toJSON(),
lastTradeTicker: this._lastTrade.live.ticker || this._lastTrade.paper.ticker
};
}
}

View File

@@ -0,0 +1,80 @@
import { BaseStrategy } from './base.js';
/**
* Late Momentum Strategy
*
* Rules:
* - Buys momentum on YES or NO
* - Requires side price >= triggerPct (default 75)
* - Only allowed after first 6 minutes of market lifecycle
* - Default window: elapsed minute 6 through 15
*/
export class LateMomentumStrategy extends BaseStrategy {
constructor(config = {}) {
super('late-momentum', {
triggerPct: config.triggerPct || 75,
betSize: config.betSize || 4,
slippage: config.slippage || 3,
cooldownMs: config.cooldownMs || 20000,
marketDurationMin: config.marketDurationMin || 15,
minElapsedMin: config.minElapsedMin || 6,
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 minutes; trade only in configured late window.
if (elapsedMin < this.config.minElapsedMin || elapsedMin > this.config.maxElapsedMin) return null;
const yesPct = Number(state.yesPct);
const noPct = Number(state.noPct);
if (!Number.isFinite(yesPct) || !Number.isFinite(noPct)) return null;
const trigger = this.config.triggerPct;
const candidates = [];
if (yesPct >= trigger && yesPct < 99) candidates.push({ side: 'yes', pct: yesPct });
if (noPct >= trigger && noPct < 99) candidates.push({ side: 'no', pct: noPct });
if (!candidates.length) return null;
// Prefer the stronger momentum side if both qualify.
candidates.sort((a, b) => b.pct - a.pct);
const pick = candidates[0];
const signal = {
strategy: this.name,
side: pick.side,
price: pick.pct,
maxPrice: Math.min(pick.pct + this.config.slippage, 95),
size: this.config.betSize,
reason: `Late momentum: t+${elapsedMin.toFixed(1)}m, ${pick.side.toUpperCase()} @ ${pick.pct}¢`,
ticker: state.ticker
};
track.time = now;
track.ticker = state.ticker;
return signal;
}
}

View File

@@ -1,126 +0,0 @@
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
};
}
}

View File

@@ -4,7 +4,7 @@ export class MomentumRiderStrategy extends BaseStrategy {
constructor(config = {}) {
super('momentum-rider', {
triggerPct: config.triggerPct || 75,
betSize: config.betSize || 2,
betSize: config.betSize || 4,
slippage: config.slippage || 3,
cooldownMs: config.cooldownMs || 20000,
...config