Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
kellyGrowthRate,
dutching,
kellyParlay,
hedgeBet,
} from './index';

describe('kelly-js: Kelly Criterion & Sports Betting Analytics', () => {
Expand Down Expand Up @@ -1113,4 +1114,52 @@ describe('kelly-js: Kelly Criterion & Sports Betting Analytics', () => {
expect(() => dutching(horseRace, -100)).toThrow(RangeError);
});
});

// ────────────────────────────────────────────────────────────────────────────
// Hedge Bet Tests
// ────────────────────────────────────────────────────────────────────────────

describe('hedgeBet()', () => {
it('locks in guaranteed profit when original odds were long', () => {
// Bet $100 at +300 early; opposite now available at +100
const result = hedgeBet(100, 300, 100);
expect(result.hedgeStake).toBe(200); // 100 * 4.0 / 2.0
expect(result.grossReturn).toBe(400); // 100 * 4.0
expect(result.totalRisked).toBe(300);
expect(result.guaranteedProfit).toBe(100);
expect(result.isProfit).toBe(true);
expect(result.roi).toBeCloseTo(1 / 3, 3);
});

it('breaks even when original and hedge odds are identical', () => {
// Same odds on both sides: gross return exactly covers both stakes
const result = hedgeBet(100, 100, 100);
expect(result.guaranteedProfit).toBe(0);
expect(result.isProfit).toBe(false);
expect(result.hedgeStake).toBe(100); // equal odds → hedge = original stake
});

it('reflects a loss when hedge odds are shorter than original', () => {
// Original at +100 (decimal 2.0), hedge at -200 (decimal 1.5)
const result = hedgeBet(100, 100, -200);
// hedgeStake = 100 * 2.0 / 1.5 ≈ 133.33
// grossReturn = 200; totalRisked ≈ 233.33; profit ≈ -33.33
expect(result.guaranteedProfit).toBeLessThan(0);
expect(result.isProfit).toBe(false);
});

it('gross return is equal for both outcome scenarios', () => {
// Property: hedgeStake * dHedge === originalStake * dOrig (equal gross)
const r = hedgeBet(200, 250, 150);
const dOrig = toDecimal(250);
const dHedge = toDecimal(150);
expect(r.hedgeStake * dHedge).toBeCloseTo(200 * dOrig, 1);
expect(r.grossReturn).toBeCloseTo(200 * dOrig, 1);
});

it('throws on non-positive stake', () => {
expect(() => hedgeBet(0, 200, -150)).toThrow(RangeError);
expect(() => hedgeBet(-100, 200, -150)).toThrow(RangeError);
});
});
});
67 changes: 67 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1160,3 +1160,70 @@ export function dutching(
overround: Math.round(overround * 10000) / 10000,
};
}

// ─── Hedge Bet ────────────────────────────────────────────────────────────────

export interface HedgeResult {
/** Stake to place on the opposite side to lock in equal gross returns */
hedgeStake: number;
/** Guaranteed net profit regardless of which outcome wins */
guaranteedProfit: number;
/** True when the hedge produces a net positive return */
isProfit: boolean;
/** Combined ROI on all money committed (original + hedge) */
roi: number;
/** Total capital at risk across both legs */
totalRisked: number;
/** Gross payout from whichever side wins (identical for both scenarios) */
grossReturn: number;
}

/**
* Calculate the hedge stake that locks in an equal guaranteed return on an
* existing bet when the opposite side is now available at better odds.
*
* The classic scenario: you placed an early-season bet on an underdog at +300;
* they reach the final and the opposite side now offers +100. Hedging lets you
* guarantee a profit no matter who wins.
*
* The formula equalises the gross payout for both outcomes:
* `hedgeStake = originalStake × decimal(originalOdds) / decimal(hedgeOdds)`
*
* Profit is positive when the original odds are long enough that gross return
* exceeds combined stakes. See `arbitrage()` for the symmetric two-sided case.
*
* @param originalStake Amount placed on your original bet
* @param originalAmericanOdds American odds at which you placed the original bet
* @param hedgeAmericanOdds Current American odds on the opposite outcome
*
* @example
* // Bet $100 at +300 pre-season; they made the final, opposite now at +100
* hedgeBet(100, 300, 100);
* // → { hedgeStake: 200, guaranteedProfit: 100, isProfit: true, roi: 0.333 }
*/
export function hedgeBet(
originalStake: number,
originalAmericanOdds: number,
hedgeAmericanOdds: number,
): HedgeResult {
if (!Number.isFinite(originalStake) || originalStake <= 0) {
throw new RangeError('originalStake must be a positive finite number');
}

const dOrig = toDecimal(originalAmericanOdds);
const dHedge = toDecimal(hedgeAmericanOdds);

const grossReturn = Math.round(originalStake * dOrig * 100) / 100;
const hedgeStake = Math.round((grossReturn / dHedge) * 100) / 100;
const totalRisked = Math.round((originalStake + hedgeStake) * 100) / 100;
const guaranteedProfit = Math.round((grossReturn - totalRisked) * 100) / 100;

return {
hedgeStake,
guaranteedProfit,
isProfit: guaranteedProfit > 0,
roi: totalRisked > 0 ? Math.round((guaranteedProfit / totalRisked) * 10000) / 10000 : 0,
totalRisked,
grossReturn,
};
}
Loading