overtime-live-trading-utils 2.1.35 → 2.1.37-rc.0

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.
Files changed (41) hide show
  1. package/.circleci/config.yml +32 -32
  2. package/.prettierrc +9 -9
  3. package/codecov.yml +20 -20
  4. package/index.ts +26 -26
  5. package/jest.config.ts +16 -16
  6. package/main.js +1 -1
  7. package/package.json +30 -30
  8. package/src/constants/common.ts +8 -7
  9. package/src/constants/errors.ts +7 -6
  10. package/src/constants/sports.ts +78 -78
  11. package/src/enums/sports.ts +109 -109
  12. package/src/tests/mock/MockLeagueMap.ts +200 -170
  13. package/src/tests/mock/MockOpticOddsEvents.ts +662 -662
  14. package/src/tests/mock/MockOpticSoccer.ts +9864 -9378
  15. package/src/tests/mock/MockSoccerRedis.ts +2308 -2308
  16. package/src/tests/unit/bookmakers.test.ts +148 -79
  17. package/src/tests/unit/markets.test.ts +176 -156
  18. package/src/tests/unit/odds.test.ts +103 -92
  19. package/src/tests/unit/resolution.test.ts +1488 -1393
  20. package/src/tests/unit/sports.test.ts +58 -58
  21. package/src/tests/unit/spread.test.ts +144 -131
  22. package/src/tests/utils/helper.ts +10 -0
  23. package/src/types/bookmakers.ts +7 -0
  24. package/src/types/missing-types.d.ts +2 -2
  25. package/src/types/odds.ts +80 -61
  26. package/src/types/resolution.ts +96 -96
  27. package/src/types/sports.ts +22 -19
  28. package/src/utils/bookmakers.ts +315 -159
  29. package/src/utils/constraints.ts +210 -210
  30. package/src/utils/gameMatching.ts +81 -81
  31. package/src/utils/markets.ts +119 -119
  32. package/src/utils/odds.ts +947 -918
  33. package/src/utils/opticOdds.ts +71 -71
  34. package/src/utils/resolution.ts +319 -319
  35. package/src/utils/sportPeriodMapping.ts +36 -36
  36. package/src/utils/sports.ts +51 -51
  37. package/src/utils/spread.ts +97 -97
  38. package/tsconfig.json +17 -13
  39. package/webpack.config.js +24 -24
  40. package/CLAUDE.md +0 -84
  41. package/resolution_live_markets.md +0 -356
@@ -1,159 +1,315 @@
1
- import * as oddslib from 'oddslib';
2
- import {
3
- DIFF_BETWEEN_BOOKMAKERS_MESSAGE,
4
- NO_MATCHING_BOOKMAKERS_MESSAGE,
5
- ZERO_ODDS_MESSAGE,
6
- ZERO_ODDS_MESSAGE_SINGLE_BOOKMAKER,
7
- } from '../constants/errors';
8
-
9
- export const getBookmakersArray = (bookmakersData: any[], sportId: any, backupLiveOddsProviders: string[]) => {
10
- const sportBookmakersData = bookmakersData.find((data) => Number(data.sportId) === Number(sportId));
11
- if (sportBookmakersData) {
12
- if (sportBookmakersData.primaryBookmaker == '') {
13
- return backupLiveOddsProviders;
14
- }
15
- const bookmakersArray: string[] = [];
16
-
17
- sportBookmakersData.primaryBookmaker ? bookmakersArray.push(sportBookmakersData.primaryBookmaker) : '';
18
- sportBookmakersData.secondaryBookmaker ? bookmakersArray.push(sportBookmakersData.secondaryBookmaker) : '';
19
- sportBookmakersData.tertiaryBookmaker ? bookmakersArray.push(sportBookmakersData.tertiaryBookmaker) : '';
20
-
21
- return bookmakersArray;
22
- }
23
- return backupLiveOddsProviders;
24
- };
25
-
26
- export const checkOddsFromBookmakers = (
27
- oddsMap: Map<string, any>,
28
- arrayOfBookmakers: string[],
29
- isTwoPositionalSport: boolean,
30
- maxImpliedPercentageDifference: number,
31
- minOddsForDiffChecking: number
32
- ) => {
33
- // Main bookmaker odds
34
- const firstBookmakerOdds = oddsMap.get(arrayOfBookmakers[0].toLowerCase());
35
-
36
- if (!firstBookmakerOdds) {
37
- // If no matching bookmakers are found, return zero odds
38
- return {
39
- homeOdds: 0,
40
- awayOdds: 0,
41
- drawOdds: 0,
42
- errorMessage: NO_MATCHING_BOOKMAKERS_MESSAGE,
43
- };
44
- }
45
-
46
- const homeOdd = firstBookmakerOdds.homeOdds;
47
- const awayOdd = firstBookmakerOdds.awayOdds;
48
- const drawOdd = isTwoPositionalSport ? 0 : firstBookmakerOdds.drawOdds;
49
-
50
- // Check if any bookmaker has odds of 0 or 0.0001
51
- const hasZeroOrOne = arrayOfBookmakers.some((bookmakerId) => {
52
- const line = oddsMap.get(bookmakerId);
53
- if (line) {
54
- return (
55
- line.homeOdds === 0 ||
56
- line.awayOdds === 0 ||
57
- (!isTwoPositionalSport && line.drawOdds === 0) ||
58
- line.homeOdds === 1 ||
59
- line.awayOdds === 1 ||
60
- (!isTwoPositionalSport && line.drawOdds === 1)
61
- );
62
- }
63
- return false; // fix for es-lint
64
- });
65
-
66
- if (hasZeroOrOne) {
67
- // If any bookmaker has zero odds, return zero odds
68
- return {
69
- homeOdds: 0,
70
- awayOdds: 0,
71
- drawOdds: 0,
72
- errorMessage: arrayOfBookmakers.length === 1 ? ZERO_ODDS_MESSAGE_SINGLE_BOOKMAKER : ZERO_ODDS_MESSAGE,
73
- // TODO: Return sportsbook name with zero odds
74
- };
75
- }
76
-
77
- if (arrayOfBookmakers.length == 1) {
78
- return {
79
- homeOdds: homeOdd,
80
- awayOdds: awayOdd,
81
- drawOdds: isTwoPositionalSport ? 0 : drawOdd,
82
- };
83
- }
84
-
85
- // If none of the bookmakers have zero odds, check implied odds percentage difference
86
- const hasLargeImpliedPercentageDifference = arrayOfBookmakers.slice(1).some((bookmakerId) => {
87
- const line = oddsMap.get(bookmakerId);
88
- if (line) {
89
- const otherHomeOdd = line.homeOdds;
90
- const otherAwayOdd = line.awayOdds;
91
- const otherDrawOdd = line.drawOdds;
92
-
93
- const homeOddsImplied = oddslib.from('decimal', homeOdd).to('impliedProbability');
94
-
95
- const awayOddsImplied = oddslib.from('decimal', awayOdd).to('impliedProbability');
96
-
97
- // Calculate implied odds for the "draw" if it's not a two-positions sport
98
- const drawOddsImplied = isTwoPositionalSport
99
- ? 0
100
- : oddslib.from('decimal', drawOdd).to('impliedProbability');
101
-
102
- const otherHomeOddImplied = oddslib.from('decimal', otherHomeOdd).to('impliedProbability');
103
-
104
- const otherAwayOddImplied = oddslib.from('decimal', otherAwayOdd).to('impliedProbability');
105
-
106
- // Calculate implied odds for the "draw" if it's not a two-positions sport
107
- const otherDrawOddImplied = isTwoPositionalSport
108
- ? 0
109
- : oddslib.from('decimal', otherDrawOdd).to('impliedProbability');
110
-
111
- // Calculate the percentage difference for implied odds
112
- const homeOddsDifference = calculateImpliedOddsDifference(homeOddsImplied, otherHomeOddImplied);
113
-
114
- const awayOddsDifference = calculateImpliedOddsDifference(awayOddsImplied, otherAwayOddImplied);
115
-
116
- // Check implied odds difference for the "draw" only if it's not a two-positions sport
117
- const drawOddsDifference = isTwoPositionalSport
118
- ? 0
119
- : calculateImpliedOddsDifference(drawOddsImplied, otherDrawOddImplied);
120
-
121
- // Check if the percentage difference exceeds the threshold
122
- if (
123
- (homeOddsDifference > maxImpliedPercentageDifference &&
124
- homeOddsImplied > minOddsForDiffChecking &&
125
- otherHomeOddImplied > minOddsForDiffChecking) ||
126
- (awayOddsDifference > maxImpliedPercentageDifference &&
127
- awayOddsImplied > minOddsForDiffChecking &&
128
- otherAwayOddImplied > minOddsForDiffChecking) ||
129
- (!isTwoPositionalSport &&
130
- drawOddsDifference > maxImpliedPercentageDifference &&
131
- drawOddsImplied > minOddsForDiffChecking &&
132
- otherDrawOddImplied > minOddsForDiffChecking)
133
- ) {
134
- return true;
135
- }
136
- }
137
- return false;
138
- });
139
-
140
- if (hasLargeImpliedPercentageDifference) {
141
- return {
142
- homeOdds: 0,
143
- awayOdds: 0,
144
- drawOdds: 0,
145
- errorMessage: DIFF_BETWEEN_BOOKMAKERS_MESSAGE,
146
- };
147
- }
148
-
149
- return {
150
- homeOdds: homeOdd,
151
- awayOdds: awayOdd,
152
- drawOdds: isTwoPositionalSport ? 0 : drawOdd,
153
- };
154
- };
155
-
156
- export const calculateImpliedOddsDifference = (impliedOddsA: number, impliedOddsB: number): number => {
157
- const percentageDifference = (Math.abs(impliedOddsA - impliedOddsB) / impliedOddsA) * 100;
158
- return percentageDifference;
159
- };
1
+ import * as oddslib from 'oddslib';
2
+ import { MAX_IMPLIED_PERCENTAGE_DIFF } from '../constants/common';
3
+ import {
4
+ DIFF_BETWEEN_BOOKMAKERS_MESSAGE,
5
+ NO_MATCHING_BOOKMAKERS_MESSAGE,
6
+ ZERO_ODDS_MESSAGE,
7
+ ZERO_ODDS_MESSAGE_SINGLE_BOOKMAKER,
8
+ } from '../constants/errors';
9
+ import { BookmakersConfig } from '../types/bookmakers';
10
+ import { OddsWithLeagueInfo } from '../types/odds';
11
+ import { LastPolledArray, LeagueConfigInfo } from '../types/sports';
12
+
13
+ export const getBookmakersArray = (
14
+ bookmakersData: BookmakersConfig[],
15
+ sportId: any,
16
+ backupLiveOddsProviders: string[]
17
+ ) => {
18
+ const sportBookmakersData = bookmakersData.find((data) => Number(data.sportId) === Number(sportId));
19
+ if (sportBookmakersData) {
20
+ if (sportBookmakersData.primaryBookmaker == '') {
21
+ return backupLiveOddsProviders;
22
+ }
23
+ const bookmakersArray: string[] = [];
24
+
25
+ sportBookmakersData.primaryBookmaker ? bookmakersArray.push(sportBookmakersData.primaryBookmaker) : '';
26
+ sportBookmakersData.secondaryBookmaker ? bookmakersArray.push(sportBookmakersData.secondaryBookmaker) : '';
27
+ sportBookmakersData.tertiaryBookmaker ? bookmakersArray.push(sportBookmakersData.tertiaryBookmaker) : '';
28
+
29
+ return bookmakersArray;
30
+ }
31
+ return backupLiveOddsProviders;
32
+ };
33
+
34
+ export const getBookmakersFromLeagueConfig = (sportId: string | number, leagueInfoArray: LeagueConfigInfo[]) => {
35
+ const uniqueBookmakers = [];
36
+
37
+ for (const leagueInfo of leagueInfoArray) {
38
+ if (Number(leagueInfo.sportId) === Number(sportId) && leagueInfo.enabled === 'true') {
39
+ const primary = leagueInfo.primaryBookmaker?.toLowerCase();
40
+ const secondary = leagueInfo.secondaryBookmaker?.toLowerCase();
41
+ if (primary) {
42
+ uniqueBookmakers.push(primary);
43
+ }
44
+ if (secondary && secondary !== primary) {
45
+ uniqueBookmakers.push(secondary);
46
+ }
47
+ break;
48
+ }
49
+ }
50
+
51
+ return uniqueBookmakers;
52
+ };
53
+
54
+ export const getBookmakersForLeague = (
55
+ sportId: string | number,
56
+ configPerMarket: LeagueConfigInfo[],
57
+ configPerLeague: BookmakersConfig[],
58
+ defaultBookmakers: string[],
59
+ maxNumOfBookmakers = 5
60
+ ) => {
61
+ // bookmakers defined per market for league
62
+ const bookmakersPerMarket = getBookmakersFromLeagueConfig(sportId, configPerMarket);
63
+ // bookmakers defined generally per league
64
+ const bookmakersPerLeague = getBookmakersArray(configPerLeague, sportId, defaultBookmakers);
65
+ // all unique bookmakers defined from both configs
66
+ const uniqueBookmakers = [...new Set([...bookmakersPerMarket, ...bookmakersPerLeague])];
67
+
68
+ const bookmakers = uniqueBookmakers.filter((s) => s.length).slice(0, maxNumOfBookmakers);
69
+
70
+ return bookmakers;
71
+ };
72
+
73
+ export const checkOddsFromBookmakers = (
74
+ oddsMap: Map<string, any>,
75
+ arrayOfBookmakers: string[],
76
+ isTwoPositionalSport: boolean,
77
+ maxImpliedPercentageDifference: number,
78
+ minOddsForDiffChecking: number
79
+ ) => {
80
+ // Main bookmaker odds
81
+ const firstBookmakerOdds = oddsMap.get(arrayOfBookmakers[0].toLowerCase());
82
+
83
+ if (!firstBookmakerOdds) {
84
+ // If no matching bookmakers are found, return zero odds
85
+ return {
86
+ homeOdds: 0,
87
+ awayOdds: 0,
88
+ drawOdds: 0,
89
+ errorMessage: NO_MATCHING_BOOKMAKERS_MESSAGE,
90
+ };
91
+ }
92
+
93
+ const homeOdd = firstBookmakerOdds.homeOdds;
94
+ const awayOdd = firstBookmakerOdds.awayOdds;
95
+ const drawOdd = isTwoPositionalSport ? 0 : firstBookmakerOdds.drawOdds;
96
+
97
+ // Check if any bookmaker has odds of 0 or 0.0001
98
+ const hasZeroOrOne = arrayOfBookmakers.some((bookmakerId) => {
99
+ const line = oddsMap.get(bookmakerId);
100
+ if (line) {
101
+ return (
102
+ line.homeOdds === 0 ||
103
+ line.awayOdds === 0 ||
104
+ (!isTwoPositionalSport && line.drawOdds === 0) ||
105
+ line.homeOdds === 1 ||
106
+ line.awayOdds === 1 ||
107
+ (!isTwoPositionalSport && line.drawOdds === 1)
108
+ );
109
+ }
110
+ return false; // fix for es-lint
111
+ });
112
+
113
+ if (hasZeroOrOne) {
114
+ // If any bookmaker has zero odds, return zero odds
115
+ return {
116
+ homeOdds: 0,
117
+ awayOdds: 0,
118
+ drawOdds: 0,
119
+ errorMessage: arrayOfBookmakers.length === 1 ? ZERO_ODDS_MESSAGE_SINGLE_BOOKMAKER : ZERO_ODDS_MESSAGE,
120
+ // TODO: Return sportsbook name with zero odds
121
+ };
122
+ }
123
+
124
+ if (arrayOfBookmakers.length == 1) {
125
+ return {
126
+ homeOdds: homeOdd,
127
+ awayOdds: awayOdd,
128
+ drawOdds: isTwoPositionalSport ? 0 : drawOdd,
129
+ };
130
+ }
131
+
132
+ // If none of the bookmakers have zero odds, check implied odds percentage difference
133
+ const hasLargeImpliedPercentageDifference = arrayOfBookmakers.slice(1).some((bookmakerId) => {
134
+ const line = oddsMap.get(bookmakerId);
135
+ if (line) {
136
+ const otherHomeOdd = line.homeOdds;
137
+ const otherAwayOdd = line.awayOdds;
138
+ const otherDrawOdd = line.drawOdds;
139
+
140
+ const homeOddsImplied = oddslib.from('decimal', homeOdd).to('impliedProbability');
141
+
142
+ const awayOddsImplied = oddslib.from('decimal', awayOdd).to('impliedProbability');
143
+
144
+ // Calculate implied odds for the "draw" if it's not a two-positions sport
145
+ const drawOddsImplied = isTwoPositionalSport
146
+ ? 0
147
+ : oddslib.from('decimal', drawOdd).to('impliedProbability');
148
+
149
+ const otherHomeOddImplied = oddslib.from('decimal', otherHomeOdd).to('impliedProbability');
150
+
151
+ const otherAwayOddImplied = oddslib.from('decimal', otherAwayOdd).to('impliedProbability');
152
+
153
+ // Calculate implied odds for the "draw" if it's not a two-positions sport
154
+ const otherDrawOddImplied = isTwoPositionalSport
155
+ ? 0
156
+ : oddslib.from('decimal', otherDrawOdd).to('impliedProbability');
157
+
158
+ // Calculate the percentage difference for implied odds
159
+ const homeOddsDifference = calculateImpliedOddsDifference(homeOddsImplied, otherHomeOddImplied);
160
+
161
+ const awayOddsDifference = calculateImpliedOddsDifference(awayOddsImplied, otherAwayOddImplied);
162
+
163
+ // Check implied odds difference for the "draw" only if it's not a two-positions sport
164
+ const drawOddsDifference = isTwoPositionalSport
165
+ ? 0
166
+ : calculateImpliedOddsDifference(drawOddsImplied, otherDrawOddImplied);
167
+
168
+ // Check if the percentage difference exceeds the threshold
169
+ if (
170
+ (homeOddsDifference > maxImpliedPercentageDifference &&
171
+ homeOddsImplied > minOddsForDiffChecking &&
172
+ otherHomeOddImplied > minOddsForDiffChecking) ||
173
+ (awayOddsDifference > maxImpliedPercentageDifference &&
174
+ awayOddsImplied > minOddsForDiffChecking &&
175
+ otherAwayOddImplied > minOddsForDiffChecking) ||
176
+ (!isTwoPositionalSport &&
177
+ drawOddsDifference > maxImpliedPercentageDifference &&
178
+ drawOddsImplied > minOddsForDiffChecking &&
179
+ otherDrawOddImplied > minOddsForDiffChecking)
180
+ ) {
181
+ return true;
182
+ }
183
+ }
184
+ return false;
185
+ });
186
+
187
+ if (hasLargeImpliedPercentageDifference) {
188
+ return {
189
+ homeOdds: 0,
190
+ awayOdds: 0,
191
+ drawOdds: 0,
192
+ errorMessage: DIFF_BETWEEN_BOOKMAKERS_MESSAGE,
193
+ };
194
+ }
195
+
196
+ return {
197
+ homeOdds: homeOdd,
198
+ awayOdds: awayOdd,
199
+ drawOdds: isTwoPositionalSport ? 0 : drawOdd,
200
+ };
201
+ };
202
+
203
+ export const checkOddsFromBookmakersForChildMarkets = (
204
+ odds: any,
205
+ leagueInfos: LeagueConfigInfo[],
206
+ oddsProviders: string[],
207
+ lastPolledMap: LastPolledArray,
208
+ MAX_ALLOWED_PROVIDER_DATA_STALE_DELAY: number
209
+ ): OddsWithLeagueInfo => {
210
+ const formattedOdds = Object.entries(odds as any).reduce((acc: any, [key, value]: [string, any]) => {
211
+ const [sportsBookName, marketName, points, selection, selectionLine] = key.split('_');
212
+ const info = leagueInfos.find((leagueInfo) => leagueInfo.marketName.toLowerCase() === marketName.toLowerCase());
213
+ if (info) {
214
+ const { primaryBookmaker, secondaryBookmaker } = getPrimaryAndSecondaryBookmakerForTypeId(
215
+ oddsProviders,
216
+ leagueInfos,
217
+ Number(info.typeId)
218
+ );
219
+
220
+ const isValidLastPolled = isLastPolledForBookmakersValid(
221
+ lastPolledMap,
222
+ MAX_ALLOWED_PROVIDER_DATA_STALE_DELAY,
223
+ primaryBookmaker,
224
+ secondaryBookmaker
225
+ );
226
+
227
+ if (isValidLastPolled) {
228
+ if (primaryBookmaker && !secondaryBookmaker) {
229
+ if (sportsBookName.toLowerCase() === primaryBookmaker.toLowerCase()) {
230
+ acc.push(value);
231
+ }
232
+ } else {
233
+ if (sportsBookName.toLowerCase() === primaryBookmaker) {
234
+ const secondaryBookmakerObject =
235
+ odds[
236
+ `${secondaryBookmaker}_${marketName.toLowerCase()}_${points}_${selection}_${selectionLine}`
237
+ ];
238
+ if (secondaryBookmakerObject) {
239
+ const primaryOdds = oddslib.from('decimal', value.price).to('impliedProbability');
240
+ const secondaryOdds = oddslib
241
+ .from('decimal', secondaryBookmakerObject.price)
242
+ .to('impliedProbability');
243
+
244
+ const homeOddsDifference = calculateImpliedOddsDifference(primaryOdds, secondaryOdds);
245
+ if (Number(homeOddsDifference) <= Number(MAX_IMPLIED_PERCENTAGE_DIFF)) {
246
+ acc.push(value);
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ return acc;
255
+ }, []);
256
+ return formattedOdds;
257
+ };
258
+
259
+ export const getPrimaryAndSecondaryBookmakerForTypeId = (
260
+ defaultProviders: string[],
261
+ leagueInfos: LeagueConfigInfo[], // LeagueConfigInfo for specific sport, not the entire list from csv
262
+ typeId: number
263
+ ): { primaryBookmaker: string; secondaryBookmaker: string | undefined } => {
264
+ const info = leagueInfos.find((leagueInfo) => Number(leagueInfo.typeId) === typeId);
265
+ let primaryBookmaker = defaultProviders[0].toLowerCase();
266
+ let secondaryBookmaker = defaultProviders[1] ? defaultProviders[1].toLowerCase() : undefined;
267
+ if (info) {
268
+ if (info.primaryBookmaker) {
269
+ primaryBookmaker = info.primaryBookmaker.toLowerCase();
270
+ secondaryBookmaker = info.secondaryBookmaker ? info.secondaryBookmaker.toLowerCase() : undefined;
271
+ }
272
+ }
273
+ return { primaryBookmaker, secondaryBookmaker };
274
+ };
275
+
276
+ export const isLastPolledForBookmakersValid = (
277
+ lastPolledMap: LastPolledArray,
278
+ MAX_ALLOWED_PROVIDER_DATA_STALE_DELAY: number,
279
+ primaryBookmaker: string,
280
+ secondaryBookmaker?: string
281
+ ): boolean => {
282
+ const lastPolledTimePrimary = lastPolledMap.find(
283
+ (entry) => entry.sportsbook.toLowerCase() === primaryBookmaker.toLowerCase()
284
+ )?.timestamp;
285
+ if (typeof lastPolledTimePrimary !== 'number') {
286
+ return false;
287
+ }
288
+
289
+ const now = new Date();
290
+ if (secondaryBookmaker) {
291
+ const lastPolledTimeSecondary = lastPolledMap.find(
292
+ (entry) => entry.sportsbook.toLowerCase() === secondaryBookmaker.toLowerCase()
293
+ )?.timestamp;
294
+
295
+ if (typeof lastPolledTimeSecondary !== 'number') {
296
+ return false;
297
+ }
298
+
299
+ const oddsDate = new Date(lastPolledTimeSecondary * 1000);
300
+ const timeDiff = now.getTime() - oddsDate.getTime();
301
+ if (timeDiff > MAX_ALLOWED_PROVIDER_DATA_STALE_DELAY) {
302
+ return false;
303
+ }
304
+ }
305
+
306
+ const oddsDate = new Date(lastPolledTimePrimary * 1000);
307
+ const timeDiff = now.getTime() - oddsDate.getTime();
308
+
309
+ return timeDiff < MAX_ALLOWED_PROVIDER_DATA_STALE_DELAY;
310
+ };
311
+
312
+ export const calculateImpliedOddsDifference = (impliedOddsA: number, impliedOddsB: number): number => {
313
+ const percentageDifference = (Math.abs(impliedOddsA - impliedOddsB) / impliedOddsA) * 100;
314
+ return percentageDifference;
315
+ };