shufflecom-calculations 2.2.4 → 2.2.5

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.
@@ -1,130 +1,129 @@
1
1
  import BigNumber from 'bignumber.js';
2
- import { limitTotalOddsDecimalPlaces, oddsFractionToDecimal } from "./odds";
3
- import { SportsBetSelectionInterface, SportsMarketSelectionInterface, SportsBetSelectionStatus, OutcomeResult } from "./sports.types";
4
- import { SportsSystemBetType, SportsSystemBetTypeCombinations } from "./system-bet.types";
5
-
2
+ import { limitTotalOddsDecimalPlaces, oddsFractionToDecimal } from './odds';
3
+ import { OutcomeResult, SportsBetSelectionInterface, SportsBetSelectionStatus, SportsMarketSelectionInterface } from './sports.types';
4
+ import { SportsSystemBetType, SportsSystemBetTypeCombinations, SportsSystemBetTypeSelectionCountsToBetCounts } from './system-bet.types';
6
5
 
7
6
  export const CUSTOM_BET_VOIDED_STATUSES = [
8
- SportsBetSelectionStatus.VOIDED,
9
- SportsBetSelectionStatus.PROVIDER_VOIDED,
10
- SportsBetSelectionStatus.PUSHED,
11
- ];
12
-
13
- export function generateSystemBetIndexCombinations(length: number, size: number): number[][] {
14
- if (size === 0) return [[]];
15
- if (size > length) return [];
16
- if (size === length) return [Array.from({ length }, (_, i) => i)];
17
-
18
- const combinations: number[][] = [];
19
-
20
- function backtrack(start: number, combo: number[]) {
21
- if (combo.length === size) {
22
- combinations.push([...combo]);
23
- return;
24
- }
25
-
26
- for (let i = start; i < length; i++) {
27
- combo.push(i);
28
- backtrack(i + 1, combo);
29
- combo.pop();
30
- }
7
+ SportsBetSelectionStatus.VOIDED,
8
+ SportsBetSelectionStatus.PROVIDER_VOIDED,
9
+ SportsBetSelectionStatus.PUSHED,
10
+ ];
11
+
12
+ export function generateSystemBetIndexCombinations(length: number, size: number): number[][] {
13
+ if (size === 0) return [[]];
14
+ if (size > length) return [];
15
+ if (size === length) return [Array.from({ length }, (_, i) => i)];
16
+
17
+ const combinations: number[][] = [];
18
+
19
+ function backtrack(start: number, combo: number[]) {
20
+ if (combo.length === size) {
21
+ combinations.push([...combo]);
22
+ return;
23
+ }
24
+
25
+ for (let i = start; i < length; i++) {
26
+ combo.push(i);
27
+ backtrack(i + 1, combo);
28
+ combo.pop();
31
29
  }
32
-
33
- backtrack(0, []);
34
- return combinations;
35
30
  }
36
-
31
+
32
+ backtrack(0, []);
33
+ return combinations;
34
+ }
37
35
 
38
36
  export function getSystemBetSubBetSelections(
39
- selections: SportsBetSelectionInterface[],
40
- sportsSystemBetType: SportsSystemBetType,
41
- ): SportsBetSelectionInterface[][] {
42
- const systemBetSizes = SportsSystemBetTypeCombinations[sportsSystemBetType];
43
- if (!systemBetSizes) {
44
- throw new Error('Invalid system bet type');
45
- }
46
- const combinations = systemBetSizes.flatMap(size => generateSystemBetIndexCombinations(selections.length, size));
47
- const combinationSelections = combinations.map(combination => combination.map(index => selections[index]));
48
-
49
- return combinationSelections;
37
+ selections: SportsBetSelectionInterface[],
38
+ sportsSystemBetType: SportsSystemBetType,
39
+ ): SportsBetSelectionInterface[][] {
40
+ const systemBetSizes = SportsSystemBetTypeCombinations[sportsSystemBetType];
41
+ if (!systemBetSizes) {
42
+ throw new Error('Invalid system bet type');
50
43
  }
44
+ if (!SportsSystemBetTypeSelectionCountsToBetCounts[selections.length][sportsSystemBetType]) {
45
+ throw new Error('Invalid selection count');
46
+ }
47
+ const combinations = systemBetSizes.flatMap(size => generateSystemBetIndexCombinations(selections.length, size));
48
+ const combinationSelections = combinations.map(combination => combination.map(index => selections[index]));
49
+
50
+ return combinationSelections;
51
+ }
51
52
 
52
53
  export function calculateSportsPayoutOdds(
53
- selections: SportsBetSelectionInterface[],
54
- marketSelections: SportsMarketSelectionInterface[],
55
- opt: { sportsCustomBetTotalOddsDecimal: BigNumber | null; subBetSelections: SportsBetSelectionInterface[][] | null },
56
- ): BigNumber {
57
- const sportsCustomBetTotalOddsDecimal = opt?.sportsCustomBetTotalOddsDecimal;
58
- const subBetSelections = opt?.subBetSelections;
59
- if (sportsCustomBetTotalOddsDecimal) {
60
- if (selections.some(({ status }) => CUSTOM_BET_VOIDED_STATUSES.includes(status))) {
61
- return new BigNumber(1);
62
- } else if (selections.some(({ status }) => status === SportsBetSelectionStatus.LOST)) {
63
- return new BigNumber(0);
64
- } else {
65
- return limitTotalOddsDecimalPlaces(sportsCustomBetTotalOddsDecimal);
66
- }
54
+ selections: SportsBetSelectionInterface[],
55
+ marketSelections: SportsMarketSelectionInterface[],
56
+ opt: {
57
+ sportsCustomBetTotalOddsDecimal: BigNumber | null;
58
+ subBetSelections: SportsBetSelectionInterface[][] | null;
59
+ isEstimation: boolean; // treats PENDING leg as WON
60
+ },
61
+ ): BigNumber {
62
+ const sportsCustomBetTotalOddsDecimal = opt?.sportsCustomBetTotalOddsDecimal;
63
+ const subBetSelections = opt?.subBetSelections;
64
+ if (sportsCustomBetTotalOddsDecimal) {
65
+ if (selections.some(({ status }) => CUSTOM_BET_VOIDED_STATUSES.includes(status))) {
66
+ return new BigNumber(1);
67
+ } else if (selections.some(({ status }) => status === SportsBetSelectionStatus.LOST)) {
68
+ return new BigNumber(0);
69
+ } else {
70
+ return limitTotalOddsDecimalPlaces(sportsCustomBetTotalOddsDecimal);
67
71
  }
68
-
69
- const marketSelectionMap = marketSelections.reduce(
70
- (map, selection) => {
71
- map[selection.id] = selection;
72
- return map;
73
- },
74
- {} as Record<string, SportsMarketSelectionInterface>,
75
- );
76
-
77
- if (subBetSelections) {
78
- let totalOddsDecimal = new BigNumber(0);
79
- for (const subBetSelection of subBetSelections) {
80
- const subBetMarketSelections = subBetSelection.map(selection => marketSelectionMap[selection.marketSelectionId]);
81
- if (subBetMarketSelections.some(marketSelection => !marketSelection)) {
82
- throw new Error('Sub bet market selections not found');
83
- }
84
- const subBetOdds = calculateSportsPayoutOdds(subBetSelection, subBetMarketSelections as SportsMarketSelectionInterface[], {
85
- sportsCustomBetTotalOddsDecimal: null,
86
- subBetSelections: null,
87
- });
88
- totalOddsDecimal = totalOddsDecimal.plus(subBetOdds);
72
+ }
73
+
74
+ const marketSelectionMap = marketSelections.reduce(
75
+ (map, selection) => {
76
+ map[selection.id] = selection;
77
+ return map;
78
+ },
79
+ {} as Record<string, SportsMarketSelectionInterface>,
80
+ );
81
+
82
+ if (subBetSelections) {
83
+ let totalOddsDecimal = new BigNumber(0);
84
+ for (const subBetSelection of subBetSelections) {
85
+ const subBetMarketSelections = subBetSelection.map(selection => marketSelectionMap[selection.marketSelectionId]);
86
+ if (subBetMarketSelections.some(marketSelection => !marketSelection)) {
87
+ throw new Error('Sub bet market selections not found');
89
88
  }
90
-
91
- return limitTotalOddsDecimalPlaces(totalOddsDecimal);
89
+ const subBetOdds = calculateSportsPayoutOdds(subBetSelection, subBetMarketSelections as SportsMarketSelectionInterface[], {
90
+ sportsCustomBetTotalOddsDecimal: null,
91
+ subBetSelections: null,
92
+ isEstimation: opt?.isEstimation,
93
+ });
94
+ totalOddsDecimal = totalOddsDecimal.plus(subBetOdds);
92
95
  }
93
-
94
- return limitTotalOddsDecimalPlaces(
95
- selections.reduce((totalOddsDecimal, selection): BigNumber => {
96
- // https://geniussports.atlassian.net/wiki/spaces/BID/pages/1239941172/Back-End+Integrations#Back-EndIntegrations-Whatareselectionresultingvalues%3F
97
- const selectionOddsDecimal = oddsFractionToDecimal(selection.oddsNumerator, selection.oddsDenominator);
98
- const marketSelection = marketSelectionMap[selection.marketSelectionId];
99
-
100
- if (!marketSelection) {
101
- throw new Error('Market selection not found');
102
- }
103
-
104
- if (selection.status === SportsBetSelectionStatus.LOST) {
105
- return BigNumber(0);
106
- }
107
-
108
- if (selection.status === SportsBetSelectionStatus.WON) {
109
- return totalOddsDecimal.multipliedBy(selectionOddsDecimal);
110
- }
111
-
112
- if (selection.status === SportsBetSelectionStatus.PARTIAL) {
113
- if (
114
- marketSelection.resultAdditionalData &&
115
- 'percentageWin' in marketSelection.resultAdditionalData &&
116
- 'percentagePush' in marketSelection.resultAdditionalData
117
- ) {
96
+
97
+ return limitTotalOddsDecimalPlaces(totalOddsDecimal);
98
+ }
99
+
100
+ return limitTotalOddsDecimalPlaces(
101
+ selections.reduce((totalOddsDecimal, selection): BigNumber => {
102
+ // https://geniussports.atlassian.net/wiki/spaces/BID/pages/1239941172/Back-End+Integrations#Back-EndIntegrations-Whatareselectionresultingvalues%3F
103
+ const selectionOddsDecimal = oddsFractionToDecimal(selection.oddsNumerator, selection.oddsDenominator);
104
+ const marketSelection = marketSelectionMap[selection.marketSelectionId];
105
+
106
+ if (!marketSelection) {
107
+ throw new Error('Market selection not found');
108
+ }
109
+
110
+ if (selection.status === SportsBetSelectionStatus.LOST) {
111
+ return BigNumber(0);
112
+ }
113
+
114
+ if (selection.status === SportsBetSelectionStatus.WON) {
115
+ return totalOddsDecimal.multipliedBy(selectionOddsDecimal);
116
+ }
117
+
118
+ if (selection.status === SportsBetSelectionStatus.PARTIAL) {
119
+ if (marketSelection.resultAdditionalData) {
120
+ if ('percentageWin' in marketSelection.resultAdditionalData && 'percentagePush' in marketSelection.resultAdditionalData) {
118
121
  return totalOddsDecimal.multipliedBy(
119
122
  selectionOddsDecimal
120
123
  .multipliedBy(BigNumber(marketSelection.resultAdditionalData?.percentageWin).dividedBy(100))
121
124
  .plus(BigNumber(marketSelection.resultAdditionalData?.percentagePush).dividedBy(100)),
122
125
  );
123
- } else if (
124
- marketSelection.resultAdditionalData &&
125
- 'voidFactor' in marketSelection.resultAdditionalData &&
126
- 'outcome' in marketSelection.resultAdditionalData
127
- ) {
126
+ } else if ('voidFactor' in marketSelection.resultAdditionalData && 'outcome' in marketSelection.resultAdditionalData) {
128
127
  if (marketSelection.resultAdditionalData.outcome === OutcomeResult.WINNING) {
129
128
  return totalOddsDecimal.multipliedBy(
130
129
  selectionOddsDecimal
@@ -132,114 +131,122 @@ export function calculateSportsPayoutOdds(
132
131
  .plus(BigNumber(marketSelection.resultAdditionalData?.voidFactor)),
133
132
  );
134
133
  }
135
-
136
- return totalOddsDecimal.multipliedBy(marketSelection.resultAdditionalData?.voidFactor || 0);
137
- } else if (marketSelection.resultAdditionalData && 'deadHeatFactor' in marketSelection.resultAdditionalData) {
138
- return totalOddsDecimal.multipliedBy(selectionOddsDecimal.multipliedBy(marketSelection.resultAdditionalData?.deadHeatFactor || 0));
139
- }
140
-
141
- return totalOddsDecimal;
142
- }
143
-
144
- if (selection.status === SportsBetSelectionStatus.PLACED) {
145
- if (marketSelection.resultAdditionalData && 'countInPlace' in marketSelection.resultAdditionalData) {
146
- return totalOddsDecimal.multipliedBy(selectionOddsDecimal.dividedBy(marketSelection.resultAdditionalData.countInPlace));
134
+
135
+ return totalOddsDecimal.multipliedBy(marketSelection.resultAdditionalData.voidFactor || 0);
136
+ } else if ('deadHeatFactor' in marketSelection.resultAdditionalData) {
137
+ return totalOddsDecimal.multipliedBy(selectionOddsDecimal.multipliedBy(marketSelection.resultAdditionalData.deadHeatFactor || 0));
147
138
  }
148
-
149
- return totalOddsDecimal;
150
139
  }
151
-
152
- if (selection.status === SportsBetSelectionStatus.PUSHED) {
153
- return totalOddsDecimal;
154
- }
155
-
156
- if (selection.status === SportsBetSelectionStatus.VOIDED || selection.status == SportsBetSelectionStatus.PROVIDER_VOIDED) {
157
- return totalOddsDecimal;
158
- }
159
-
160
- if (selection.status === SportsBetSelectionStatus.PENDING || selection.status === SportsBetSelectionStatus.CASHED_OUT) {
161
- return calculateSelectionCashoutOdds(totalOddsDecimal, selectionOddsDecimal, marketSelection);
162
- }
163
-
164
- selection.status satisfies never;
165
-
140
+
166
141
  return totalOddsDecimal;
167
- }, BigNumber(1)),
168
- );
169
- }
142
+ }
170
143
 
171
- function calculateSelectionCashoutOdds(
172
- totalOddsDecimal: BigNumber,
173
- selectionOddsDecimal: BigNumber,
174
- marketSelection: SportsMarketSelectionInterface,
175
- ) {
176
- if (marketSelection?.additionalProbability) {
177
- let cashoutOdds = BigNumber(0);
178
-
179
- if (marketSelection.additionalProbability?.winProbability) {
180
- if (marketSelection.additionalProbability.winProbability > 1) {
181
- throw new Error('Win Probability is greater than 1');
144
+ if (selection.status === SportsBetSelectionStatus.PLACED) {
145
+ if (marketSelection.resultAdditionalData && 'countInPlace' in marketSelection.resultAdditionalData) {
146
+ return totalOddsDecimal.multipliedBy(selectionOddsDecimal.dividedBy(marketSelection.resultAdditionalData.countInPlace));
182
147
  }
183
-
184
- // payoutIfTicketWins = ticketStake * ticketOdds
185
- // probability(ticket wins) * payoutIfTicketWins
186
- cashoutOdds = cashoutOdds.plus(
187
- totalOddsDecimal.multipliedBy(selectionOddsDecimal).multipliedBy(marketSelection.additionalProbability.winProbability),
188
- );
148
+
149
+ return totalOddsDecimal;
189
150
  }
190
-
191
- if (marketSelection.additionalProbability?.loseProbability) {
192
- if (marketSelection.additionalProbability.loseProbability > 1) {
193
- throw new Error('Lose Probability is greater than 1');
194
- }
195
-
196
- // payoutIfTicketLoses = 0
197
- // probability(ticket loses) * payoutIfTicketLoses
198
- cashoutOdds = cashoutOdds.plus(BigNumber(0).multipliedBy(marketSelection.additionalProbability.loseProbability));
151
+
152
+ if (selection.status === SportsBetSelectionStatus.PUSHED) {
153
+ return totalOddsDecimal;
199
154
  }
200
-
201
- if (marketSelection.additionalProbability?.halfWinProbability) {
202
- if (marketSelection.additionalProbability.halfWinProbability > 1) {
203
- throw new Error('Half Win Probability is greater than 1');
204
- }
205
-
206
- // payoutIfTicketWinsHalf = ticketStake * (((ticketOdds - 1) / 2) + 1)
207
- // probability(ticket wins half) * payoutIfTicketWinsHalf
208
- cashoutOdds = cashoutOdds.plus(
209
- totalOddsDecimal
210
- .multipliedBy(selectionOddsDecimal.minus(1).dividedBy(2).plus(1))
211
- .multipliedBy(marketSelection.additionalProbability.halfWinProbability),
212
- );
155
+
156
+ if (selection.status === SportsBetSelectionStatus.VOIDED || selection.status == SportsBetSelectionStatus.PROVIDER_VOIDED) {
157
+ return totalOddsDecimal;
213
158
  }
214
-
215
- if (marketSelection.additionalProbability?.halfLoseProbability) {
216
- if (marketSelection.additionalProbability.halfLoseProbability > 1) {
217
- throw new Error('Half Lose Probability is greater than 1');
159
+
160
+ if (selection.status === SportsBetSelectionStatus.CASHED_OUT) {
161
+ return calculateSelectionCashoutOdds(totalOddsDecimal, selectionOddsDecimal, marketSelection);
162
+ }
163
+
164
+ if (selection.status === SportsBetSelectionStatus.PENDING) {
165
+ if (opt?.isEstimation) {
166
+ return totalOddsDecimal.multipliedBy(selectionOddsDecimal);
218
167
  }
219
-
220
- // payoutIfTicketLosesHalf = ticketStake / 2
221
- // probability(ticket loses half) * payoutIfTicketLosesHalf
222
- cashoutOdds = cashoutOdds.plus(totalOddsDecimal.dividedBy(2).multipliedBy(marketSelection.additionalProbability.halfLoseProbability));
168
+ return calculateSelectionCashoutOdds(totalOddsDecimal, selectionOddsDecimal, marketSelection);
169
+ }
170
+
171
+ selection.status satisfies never;
172
+
173
+ return totalOddsDecimal;
174
+ }, BigNumber(1)),
175
+ );
176
+ }
177
+
178
+ function calculateSelectionCashoutOdds(
179
+ totalOddsDecimal: BigNumber,
180
+ selectionOddsDecimal: BigNumber,
181
+ marketSelection: SportsMarketSelectionInterface,
182
+ ) {
183
+ if (marketSelection?.additionalProbability) {
184
+ let cashoutOdds = BigNumber(0);
185
+
186
+ if (marketSelection.additionalProbability?.winProbability) {
187
+ if (marketSelection.additionalProbability.winProbability > 1) {
188
+ throw new Error('Win Probability is greater than 1');
223
189
  }
224
-
225
- if (marketSelection.additionalProbability?.refundProbability) {
226
- // payoutIfTicketVoids = ticketStake
227
- // probability(ticket voids) * payoutIfTicketVoids
228
- cashoutOdds = cashoutOdds.plus(totalOddsDecimal.multipliedBy(marketSelection.additionalProbability.refundProbability));
190
+
191
+ // payoutIfTicketWins = ticketStake * ticketOdds
192
+ // probability(ticket wins) * payoutIfTicketWins
193
+ cashoutOdds = cashoutOdds.plus(
194
+ totalOddsDecimal.multipliedBy(selectionOddsDecimal).multipliedBy(marketSelection.additionalProbability.winProbability),
195
+ );
196
+ }
197
+
198
+ if (marketSelection.additionalProbability?.loseProbability) {
199
+ if (marketSelection.additionalProbability.loseProbability > 1) {
200
+ throw new Error('Lose Probability is greater than 1');
201
+ }
202
+
203
+ // payoutIfTicketLoses = 0
204
+ // probability(ticket loses) * payoutIfTicketLoses
205
+ cashoutOdds = cashoutOdds.plus(BigNumber(0).multipliedBy(marketSelection.additionalProbability.loseProbability));
206
+ }
207
+
208
+ if (marketSelection.additionalProbability?.halfWinProbability) {
209
+ if (marketSelection.additionalProbability.halfWinProbability > 1) {
210
+ throw new Error('Half Win Probability is greater than 1');
229
211
  }
230
-
231
- return cashoutOdds;
212
+
213
+ // payoutIfTicketWinsHalf = ticketStake * (((ticketOdds - 1) / 2) + 1)
214
+ // probability(ticket wins half) * payoutIfTicketWinsHalf
215
+ cashoutOdds = cashoutOdds.plus(
216
+ totalOddsDecimal
217
+ .multipliedBy(selectionOddsDecimal.minus(1).dividedBy(2).plus(1))
218
+ .multipliedBy(marketSelection.additionalProbability.halfWinProbability),
219
+ );
232
220
  }
233
-
234
- if (marketSelection?.probability) {
235
- if (marketSelection.probability.isGreaterThan(1)) {
236
- throw new Error('Probability is greater than 1');
221
+
222
+ if (marketSelection.additionalProbability?.halfLoseProbability) {
223
+ if (marketSelection.additionalProbability.halfLoseProbability > 1) {
224
+ throw new Error('Half Lose Probability is greater than 1');
237
225
  }
238
-
239
- return totalOddsDecimal.multipliedBy(selectionOddsDecimal.multipliedBy(marketSelection.probability));
226
+
227
+ // payoutIfTicketLosesHalf = ticketStake / 2
228
+ // probability(ticket loses half) * payoutIfTicketLosesHalf
229
+ cashoutOdds = cashoutOdds.plus(totalOddsDecimal.dividedBy(2).multipliedBy(marketSelection.additionalProbability.halfLoseProbability));
240
230
  }
241
-
242
- return totalOddsDecimal
243
- .multipliedBy(selectionOddsDecimal)
244
- .dividedBy(oddsFractionToDecimal(marketSelection.oddsNumerator, marketSelection.oddsDenominator));
245
- }
231
+
232
+ if (marketSelection.additionalProbability?.refundProbability) {
233
+ // payoutIfTicketVoids = ticketStake
234
+ // probability(ticket voids) * payoutIfTicketVoids
235
+ cashoutOdds = cashoutOdds.plus(totalOddsDecimal.multipliedBy(marketSelection.additionalProbability.refundProbability));
236
+ }
237
+
238
+ return cashoutOdds;
239
+ }
240
+
241
+ if (marketSelection?.probability) {
242
+ if (marketSelection.probability.isGreaterThan(1)) {
243
+ throw new Error('Probability is greater than 1');
244
+ }
245
+
246
+ return totalOddsDecimal.multipliedBy(selectionOddsDecimal.multipliedBy(marketSelection.probability));
247
+ }
248
+
249
+ return totalOddsDecimal
250
+ .multipliedBy(selectionOddsDecimal)
251
+ .dividedBy(oddsFractionToDecimal(marketSelection.oddsNumerator, marketSelection.oddsDenominator));
252
+ }