mirror of
https://github.com/multipleof4/KalBot.git
synced 2026-03-17 05:51:02 +00:00
Compare commits
15 Commits
0cb4a082b1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| babeb7605a | |||
| 0bf1150d62 | |||
| fd53f12f63 | |||
| 149f5091fe | |||
| ab06fa683d | |||
| 6314ed7d5e | |||
| fa5303d9dc | |||
| 0301b8a0ae | |||
| 135f623789 | |||
| 6f208da27a | |||
| f9cb0e1d7a | |||
| 05fd36ca1e | |||
| 55573ed7aa | |||
| a034b26069 | |||
| 076480d05d |
@@ -11,7 +11,6 @@ export default function LiveDashboard() {
|
|||||||
const [trades, setTrades] = useState([]);
|
const [trades, setTrades] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [toggling, setToggling] = useState(null);
|
const [toggling, setToggling] = useState(null);
|
||||||
const [activeTrade, setActiveTrade] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchState = async () => {
|
const fetchState = async () => {
|
||||||
@@ -296,9 +295,28 @@ function MarketCard({ market }) {
|
|||||||
function LiveOrderRow({ order, isOpen }) {
|
function LiveOrderRow({ order, isOpen }) {
|
||||||
const won = order.result && order.side?.toLowerCase() === order.result?.toLowerCase();
|
const won = order.result && order.side?.toLowerCase() === order.result?.toLowerCase();
|
||||||
const isNeutral = order.result === 'cancelled' || order.result === 'expired';
|
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 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 (
|
return (
|
||||||
<div className="bg-gray-900 rounded-lg p-3 border border-gray-800 mb-2">
|
<div className="bg-gray-900 rounded-lg p-3 border border-gray-800 mb-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -316,10 +334,26 @@ function LiveOrderRow({ order, isOpen }) {
|
|||||||
{pnlVal != null ? `${pnlVal >= 0 ? '+' : ''}$${pnlVal.toFixed(2)}` : 'open'}
|
{pnlVal != null ? `${pnlVal >= 0 ? '+' : ''}$${pnlVal.toFixed(2)}` : 'open'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between mt-1">
|
<div className="flex justify-between mt-1">
|
||||||
<span className="text-[10px] text-gray-600">{order.reason}</span>
|
<span className="text-[10px] text-gray-600">{order.reason}</span>
|
||||||
<span className="text-[10px] text-gray-600 capitalize">{order.strategy}</span>
|
<span className="text-[10px] text-gray-600 capitalize">{order.strategy}</span>
|
||||||
</div>
|
</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 && (
|
{order.result && !isOpen && (
|
||||||
<div className="flex justify-between items-center mt-0.5">
|
<div className="flex justify-between items-center mt-0.5">
|
||||||
<span className="text-[10px] text-gray-600">Result: {order.result}</span>
|
<span className="text-[10px] text-gray-600">Result: {order.result}</span>
|
||||||
|
|||||||
@@ -345,6 +345,14 @@ function TradeRow({ trade, isOpen }) {
|
|||||||
const isNeutral = trade.result === 'cancelled' || trade.result === 'expired';
|
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 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 (
|
return (
|
||||||
<div className="bg-white rounded-lg p-3 border border-gray-200 mb-2 shadow-sm">
|
<div className="bg-white rounded-lg p-3 border border-gray-200 mb-2 shadow-sm">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -362,9 +370,25 @@ function TradeRow({ trade, isOpen }) {
|
|||||||
{trade.pnl != null ? `${trade.pnl >= 0 ? '+' : ''}$${trade.pnl}` : 'open'}
|
{trade.pnl != null ? `${trade.pnl >= 0 ? '+' : ''}$${trade.pnl}` : 'open'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between mt-1">
|
<div className="flex justify-between mt-1">
|
||||||
<span className="text-[10px] text-gray-400">{trade.reason}</span>
|
<span className="text-[10px] text-gray-400">{trade.reason}</span>
|
||||||
</div>
|
</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">
|
<div className="flex justify-between items-center mt-0.5">
|
||||||
{trade.result && !isOpen && (
|
{trade.result && !isOpen && (
|
||||||
<span className="text-[10px] text-gray-400">Result: {trade.result}</span>
|
<span className="text-[10px] text-gray-400">Result: {trade.result}</span>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export class BullDipBuyer extends BaseStrategy {
|
|||||||
super('bull-dip-buyer', {
|
super('bull-dip-buyer', {
|
||||||
maxYesPrice: config.maxYesPrice || 45,
|
maxYesPrice: config.maxYesPrice || 45,
|
||||||
minYesPrice: config.minYesPrice || 15,
|
minYesPrice: config.minYesPrice || 15,
|
||||||
betSize: config.betSize || 1,
|
betSize: config.betSize || 2,
|
||||||
slippage: config.slippage || 3,
|
slippage: config.slippage || 3,
|
||||||
cooldownMs: config.cooldownMs || 20000,
|
cooldownMs: config.cooldownMs || 20000,
|
||||||
marketDurationMin: config.marketDurationMin || 15,
|
marketDurationMin: config.marketDurationMin || 15,
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
102
lib/strategies/early-fader.js
Normal file
102
lib/strategies/early-fader.js
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
80
lib/strategies/late-momentum.js
Normal file
80
lib/strategies/late-momentum.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ export class MomentumRiderStrategy extends BaseStrategy {
|
|||||||
constructor(config = {}) {
|
constructor(config = {}) {
|
||||||
super('momentum-rider', {
|
super('momentum-rider', {
|
||||||
triggerPct: config.triggerPct || 75,
|
triggerPct: config.triggerPct || 75,
|
||||||
betSize: config.betSize || 2,
|
betSize: config.betSize || 4,
|
||||||
slippage: config.slippage || 3,
|
slippage: config.slippage || 3,
|
||||||
cooldownMs: config.cooldownMs || 20000,
|
cooldownMs: config.cooldownMs || 20000,
|
||||||
...config
|
...config
|
||||||
|
|||||||
Reference in New Issue
Block a user