shufflecom-calculations 3.3.18 → 3.3.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/sports/calculate-sports-payout-odds.d.ts +4 -0
- package/lib/sports/calculate-sports-payout-odds.js +14 -3
- package/lib/sports/calculate-sports-payout-odds.js.map +1 -1
- package/lib/sports/sports-bet.types.d.ts +1 -0
- package/lib/sports/sports-content-fixture.types.d.ts +3 -0
- package/lib/sports/sports-content-fixture.types.js +4 -1
- package/lib/sports/sports-content-fixture.types.js.map +1 -1
- package/lib/sports/sports.types.d.ts +1 -0
- package/lib/sports/sports.types.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/utils/derive-vip-bonus.d.ts +2 -2
- package/package.json +2 -2
- package/src/sports/calculate-sports-payout-odd.spec.ts +78 -1
- package/src/sports/calculate-sports-payout-odds.ts +31 -12
- package/src/sports/sports-bet.types.ts +1 -0
- package/src/sports/sports-content-fixture.types.ts +4 -0
- package/src/sports/sports.types.ts +3 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Dayjs } from 'dayjs';
|
|
2
2
|
import { VipBonusIssuanceModel, VipBonusPartial } from './vip-bonus.type';
|
|
3
|
-
export declare function getReloadAvailableDate(activationTime: Dayjs | null, cadence: number, index: number, createdAt: Dayjs): Date;
|
|
4
|
-
export declare function getReloadExpiryDate(activationTime: Dayjs | null, cadence: number, expiryPerOccurrence: number, index: number, createdAt: Dayjs, firstOccurrenceActivationPeriod: number): Date;
|
|
3
|
+
export declare function getReloadAvailableDate(activationTime: Dayjs | null, cadence: number, index: number, createdAt: Dayjs): Date | null;
|
|
4
|
+
export declare function getReloadExpiryDate(activationTime: Dayjs | null, cadence: number, expiryPerOccurrence: number, index: number, createdAt: Dayjs, firstOccurrenceActivationPeriod: number): Date | null;
|
|
5
5
|
export declare function deriveBonusIssuances(vipBonus: VipBonusPartial, now?: Dayjs): VipBonusIssuanceModel[];
|
|
6
6
|
export declare function updateBonusIssuanceStatus(issuance: VipBonusIssuanceModel, vipBonusCancelledAt: Date | null, now: Dayjs): VipBonusIssuanceModel;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shufflecom-calculations",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.20",
|
|
4
4
|
"description": "",
|
|
5
5
|
"types": "lib/index.d.ts",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -14,5 +14,5 @@
|
|
|
14
14
|
},
|
|
15
15
|
"author": "",
|
|
16
16
|
"license": "ISC",
|
|
17
|
-
"gitHead": "
|
|
17
|
+
"gitHead": "680a93d4430f249efb6b00ac6a79421a9dcf71a1"
|
|
18
18
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import BigNumber from 'bignumber.js';
|
|
2
|
-
import { calculateSportsPayoutOdds, getSystemBetSubBetSelections } from './calculate-sports-payout-odds';
|
|
2
|
+
import { calculateSportsPayoutOdds, calculateSportsPayoutOddsFromLegs, getSystemBetSubBetSelections } from './calculate-sports-payout-odds';
|
|
3
3
|
import {
|
|
4
4
|
OutcomeResult,
|
|
5
|
+
SportsBetLegInterface,
|
|
6
|
+
SportsBetLegType,
|
|
5
7
|
SportsBetSelectionInterface,
|
|
6
8
|
SportsBetSelectionStatus,
|
|
7
9
|
SportsMarketProvider,
|
|
@@ -565,6 +567,81 @@ describe('calculateSportsPayoutOdds', () => {
|
|
|
565
567
|
});
|
|
566
568
|
});
|
|
567
569
|
|
|
570
|
+
// Multi-of-SGMs: a CUSTOM_BET with more than one CUSTOM leg. These go through
|
|
571
|
+
// calculateSportsPayoutOddsFromLegs with the real legs, so each leg's odds/void/loss is
|
|
572
|
+
// isolated — unlike the flat selection façade, which collapses every selection into one
|
|
573
|
+
// custom leg and would return 1 for the whole ticket on any void.
|
|
574
|
+
describe('multi-SGM (multiple custom legs)', () => {
|
|
575
|
+
it('multiplies each custom leg odds when all legs win', () => {
|
|
576
|
+
const legs: SportsBetLegInterface[] = [
|
|
577
|
+
{
|
|
578
|
+
type: SportsBetLegType.CUSTOM,
|
|
579
|
+
oddsDecimal: new BigNumber(3),
|
|
580
|
+
selections: [createSelection('a1', SportsBetSelectionStatus.WON, 150, 100), createSelection('a2', SportsBetSelectionStatus.WON, 200, 100)],
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
type: SportsBetLegType.CUSTOM,
|
|
584
|
+
oddsDecimal: new BigNumber(2),
|
|
585
|
+
selections: [createSelection('b1', SportsBetSelectionStatus.WON, 150, 100), createSelection('b2', SportsBetSelectionStatus.WON, 200, 100)],
|
|
586
|
+
},
|
|
587
|
+
];
|
|
588
|
+
const marketSelections = [
|
|
589
|
+
createMarketSelection('a1', 150, 100),
|
|
590
|
+
createMarketSelection('a2', 200, 100),
|
|
591
|
+
createMarketSelection('b1', 150, 100),
|
|
592
|
+
createMarketSelection('b2', 200, 100),
|
|
593
|
+
];
|
|
594
|
+
|
|
595
|
+
const result = calculateSportsPayoutOddsFromLegs(legs, marketSelections, { subBetLegs: null, isEstimation: false });
|
|
596
|
+
|
|
597
|
+
expect(result.toFixed(4)).toBe('6.0000');
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it('voids only the affected custom leg and keeps the other legs odds', () => {
|
|
601
|
+
const legs: SportsBetLegInterface[] = [
|
|
602
|
+
{
|
|
603
|
+
type: SportsBetLegType.CUSTOM,
|
|
604
|
+
oddsDecimal: new BigNumber(3),
|
|
605
|
+
selections: [createSelection('a1', SportsBetSelectionStatus.WON, 150, 100), createSelection('a2', SportsBetSelectionStatus.WON, 250, 100)],
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
type: SportsBetLegType.CUSTOM,
|
|
609
|
+
oddsDecimal: new BigNumber(2),
|
|
610
|
+
selections: [
|
|
611
|
+
createSelection('b1', SportsBetSelectionStatus.WON, 150, 100),
|
|
612
|
+
createSelection('b2', SportsBetSelectionStatus.VOIDED, 200, 100),
|
|
613
|
+
],
|
|
614
|
+
},
|
|
615
|
+
];
|
|
616
|
+
const marketSelections = [createMarketSelection('a1', 150, 100), createMarketSelection('a2', 250, 100), createMarketSelection('b1', 150, 100), createMarketSelection('b2', 200, 100)];
|
|
617
|
+
|
|
618
|
+
// Leg B voids (→ odds 1); leg A survives at 3.0. The flat façade would return 1.0 for the whole ticket.
|
|
619
|
+
const result = calculateSportsPayoutOddsFromLegs(legs, marketSelections, { subBetLegs: null, isEstimation: false });
|
|
620
|
+
|
|
621
|
+
expect(result.toFixed(4)).toBe('3.0000');
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('returns 0 when a custom leg loses', () => {
|
|
625
|
+
const legs: SportsBetLegInterface[] = [
|
|
626
|
+
{
|
|
627
|
+
type: SportsBetLegType.CUSTOM,
|
|
628
|
+
oddsDecimal: new BigNumber(3),
|
|
629
|
+
selections: [createSelection('a1', SportsBetSelectionStatus.WON, 150, 100)],
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
type: SportsBetLegType.CUSTOM,
|
|
633
|
+
oddsDecimal: new BigNumber(2),
|
|
634
|
+
selections: [createSelection('b1', SportsBetSelectionStatus.WON, 150, 100), createSelection('b2', SportsBetSelectionStatus.LOST, 200, 100)],
|
|
635
|
+
},
|
|
636
|
+
];
|
|
637
|
+
const marketSelections = [createMarketSelection('a1', 150, 100), createMarketSelection('b1', 150, 100), createMarketSelection('b2', 200, 100)];
|
|
638
|
+
|
|
639
|
+
const result = calculateSportsPayoutOddsFromLegs(legs, marketSelections, { subBetLegs: null, isEstimation: false });
|
|
640
|
+
|
|
641
|
+
expect(result.toFixed(4)).toBe('0.0000');
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
|
|
568
645
|
describe('system bet', () => {
|
|
569
646
|
it('can calculate payout odds correctly', () => {
|
|
570
647
|
const selections = [
|
|
@@ -18,8 +18,6 @@ export const BPS_DIVISOR = 10_000;
|
|
|
18
18
|
export const SPORTS_BET_CASHOUT_MARGIN = 500; // in bps
|
|
19
19
|
export const SPORTS_BET_CASHOUT_PENDING_SELECTION_PENALTY = 300; // in bps
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
21
|
export const CUSTOM_BET_VOIDED_STATUSES = [
|
|
24
22
|
SportsBetSelectionStatus.VOIDED,
|
|
25
23
|
SportsBetSelectionStatus.PROVIDER_VOIDED,
|
|
@@ -76,14 +74,13 @@ export function getSystemBetSubBetSelections(
|
|
|
76
74
|
selections: SportsBetSelectionInterface[],
|
|
77
75
|
sportsSystemBetType: SportsSystemBetType,
|
|
78
76
|
): SportsBetSelectionInterface[][] {
|
|
79
|
-
|
|
80
77
|
const subBetSelectionsAndIndex = getSystemBetSubBetSelectionsAndIndex(selections, sportsSystemBetType);
|
|
81
78
|
return subBetSelectionsAndIndex.map(({ subBetSelection }) => subBetSelection);
|
|
82
79
|
}
|
|
83
80
|
|
|
84
81
|
// Selection-shaped façade over `calculateSportsPayoutOddsFromLegs`. Builds legs from the input
|
|
85
82
|
// selections, maps subBetSelections to subBetLegs, and delegates. Callers that already have legs
|
|
86
|
-
//
|
|
83
|
+
// deprecated function and is only used for cashout atm, as only regular bet / system bets are allowed for cashouts
|
|
87
84
|
export function calculateSportsPayoutOdds(
|
|
88
85
|
selections: SportsBetSelectionInterface[],
|
|
89
86
|
marketSelections: SportsMarketSelectionInterface[],
|
|
@@ -99,10 +96,7 @@ export function calculateSportsPayoutOdds(
|
|
|
99
96
|
return calculateSportsPayoutOddsFromLegs(legs, marketSelections, { subBetLegs, isEstimation: opt.isEstimation });
|
|
100
97
|
}
|
|
101
98
|
|
|
102
|
-
function mapSubBetSelectionsToLegs(
|
|
103
|
-
subBetSelections: SportsBetSelectionInterface[][],
|
|
104
|
-
legs: SportsBetLegInterface[],
|
|
105
|
-
): SportsBetLegInterface[][] {
|
|
99
|
+
function mapSubBetSelectionsToLegs(subBetSelections: SportsBetSelectionInterface[][], legs: SportsBetLegInterface[]): SportsBetLegInterface[][] {
|
|
106
100
|
const legByMarketSelectionId = new Map<string, SportsBetLegInterface>();
|
|
107
101
|
for (const leg of legs) {
|
|
108
102
|
for (const selection of leg.selections) {
|
|
@@ -196,14 +190,26 @@ function calculateSelectionCashoutOdds(
|
|
|
196
190
|
.dividedBy(oddsFractionToDecimal(marketSelection.oddsNumerator, marketSelection.oddsDenominator));
|
|
197
191
|
}
|
|
198
192
|
|
|
199
|
-
|
|
200
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Structural mapping from (bet type, selections) → legs.
|
|
195
|
+
*
|
|
196
|
+
* @deprecated Reconstructing legs from a flat selection list only works for regular/system bets and
|
|
197
|
+
* single-custom-leg bets — it cannot represent a multi-custom-leg (multi-SGM) bet and throws for
|
|
198
|
+
* one. Confined to the cashout odds path; anywhere persisted legs are available, pass them to
|
|
199
|
+
* `calculateSportsPayoutOddsFromLegs` instead of calling this.
|
|
200
|
+
*/
|
|
201
201
|
export function deriveLegsFromSelections(
|
|
202
202
|
sportsBetType: SportsBetType,
|
|
203
203
|
selections: SportsBetSelectionInterface[],
|
|
204
204
|
customBetTotalOddsDecimal: BigNumber,
|
|
205
205
|
): SportsBetLegInterface[] {
|
|
206
206
|
if (sportsBetType === SportsBetType.CUSTOM_BET) {
|
|
207
|
+
const distinctLegIds = new Set(selections.map(selection => selection.sportsBetLegId).filter(Boolean));
|
|
208
|
+
if (distinctLegIds.size > 1) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
'deriveLegsFromSelections cannot reconstruct a multi-custom-leg bet from a flat selection list; use persisted legs with calculateSportsPayoutOddsFromLegs',
|
|
211
|
+
);
|
|
212
|
+
}
|
|
207
213
|
return [{ type: SportsBetLegType.CUSTOM, oddsDecimal: customBetTotalOddsDecimal, selections }];
|
|
208
214
|
}
|
|
209
215
|
return selections.map(selection => ({
|
|
@@ -214,6 +220,13 @@ export function deriveLegsFromSelections(
|
|
|
214
220
|
}
|
|
215
221
|
|
|
216
222
|
export function getSystemBetSubBetLegs<TLeg>(legs: TLeg[], sportsSystemBetType: SportsSystemBetType): TLeg[][] {
|
|
223
|
+
return getSystemBetSubBetLegsAndIndex(legs, sportsSystemBetType).map(({ subBetLegs }) => subBetLegs);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function getSystemBetSubBetLegsAndIndex<TLeg>(
|
|
227
|
+
legs: TLeg[],
|
|
228
|
+
sportsSystemBetType: SportsSystemBetType,
|
|
229
|
+
): { subBetLegs: TLeg[]; combinationIndex: string }[] {
|
|
217
230
|
const systemBetSizes = SportsSystemBetTypeCombinations[sportsSystemBetType];
|
|
218
231
|
if (!systemBetSizes) {
|
|
219
232
|
throw new Error('Invalid system bet type');
|
|
@@ -222,7 +235,10 @@ export function getSystemBetSubBetLegs<TLeg>(legs: TLeg[], sportsSystemBetType:
|
|
|
222
235
|
throw new Error('Invalid selection count');
|
|
223
236
|
}
|
|
224
237
|
const combinations = systemBetSizes.flatMap(size => generateSystemBetIndexCombinations(legs.length, size));
|
|
225
|
-
return combinations.map(combination =>
|
|
238
|
+
return combinations.map(combination => ({
|
|
239
|
+
subBetLegs: combination.map(index => legs[index]),
|
|
240
|
+
combinationIndex: combination.map(index => index + 1).join('/'),
|
|
241
|
+
}));
|
|
226
242
|
}
|
|
227
243
|
|
|
228
244
|
export function calculateSportsPayoutOddsFromLegs(
|
|
@@ -256,7 +272,10 @@ export function calculateSportsPayoutOddsFromLegs(
|
|
|
256
272
|
}
|
|
257
273
|
|
|
258
274
|
return limitTotalOddsDecimalPlaces(
|
|
259
|
-
legs.reduce(
|
|
275
|
+
legs.reduce(
|
|
276
|
+
(totalOddsDecimal, leg) => totalOddsDecimal.multipliedBy(getLegPayoutOddsDecimal(leg, marketSelectionMap, opt.isEstimation)),
|
|
277
|
+
new BigNumber(1),
|
|
278
|
+
),
|
|
260
279
|
);
|
|
261
280
|
}
|
|
262
281
|
|
|
@@ -144,6 +144,9 @@ export interface SportsBetSelectionInterface {
|
|
|
144
144
|
status: SportsBetSelectionStatus;
|
|
145
145
|
oddsNumerator: BigNumber;
|
|
146
146
|
oddsDenominator: BigNumber;
|
|
147
|
+
// Present on persisted selections; used to detect multi-custom-leg bets that a flat selection
|
|
148
|
+
// list cannot faithfully represent (see deriveLegsFromSelections).
|
|
149
|
+
sportsBetLegId?: string | null;
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
export enum SportsBetLegType {
|