overtime-live-trading-utils 2.1.45 → 3.0.0-rc.1

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 (43) 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 +225 -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/mock/OpticOddsMock/MockNBA.ts +17269 -0
  17. package/src/tests/mock/OpticOddsMock/MockRedisNba.ts +21 -0
  18. package/src/tests/unit/bookmakers.test.ts +149 -79
  19. package/src/tests/unit/markets.test.ts +177 -156
  20. package/src/tests/unit/odds.test.ts +104 -92
  21. package/src/tests/unit/resolution.test.ts +1489 -1489
  22. package/src/tests/unit/sports.test.ts +58 -58
  23. package/src/tests/unit/spread.test.ts +145 -131
  24. package/src/tests/utils/helper.ts +11 -0
  25. package/src/types/bookmakers.ts +7 -0
  26. package/src/types/missing-types.d.ts +2 -2
  27. package/src/types/odds.ts +80 -61
  28. package/src/types/resolution.ts +656 -660
  29. package/src/types/sports.ts +22 -19
  30. package/src/utils/bookmakers.ts +309 -159
  31. package/src/utils/constraints.ts +210 -210
  32. package/src/utils/gameMatching.ts +81 -81
  33. package/src/utils/markets.ts +120 -119
  34. package/src/utils/odds.ts +952 -918
  35. package/src/utils/opticOdds.ts +71 -71
  36. package/src/utils/resolution.ts +319 -319
  37. package/src/utils/sportPeriodMapping.ts +36 -36
  38. package/src/utils/sports.ts +51 -51
  39. package/src/utils/spread.ts +97 -97
  40. package/tsconfig.json +16 -16
  41. package/webpack.config.js +24 -24
  42. package/CLAUDE.md +0 -84
  43. package/resolution_live_markets.md +0 -356
package/src/utils/odds.ts CHANGED
@@ -1,918 +1,952 @@
1
- import * as oddslib from 'oddslib';
2
- import { MarketType, MarketTypeMap } from 'overtime-utils';
3
- import { DRAW, MIN_ODDS_FOR_DIFF_CHECKING, MONEYLINE_TYPE_ID, ZERO } from '../constants/common';
4
- import { NO_MARKETS_FOR_LEAGUE_ID } from '../constants/errors';
5
- import { MoneylineTypes } from '../enums/sports';
6
- import { HomeAwayTeams, Odds, OddsObject } from '../types/odds';
7
- import { ChildMarket, LeagueConfigInfo } from '../types/sports';
8
- import { checkOddsFromBookmakers } from './bookmakers';
9
- import { getLeagueInfo } from './sports';
10
- import { adjustSpreadOnOdds, getSpreadData } from './spread';
11
-
12
- /**
13
- * Converts a given odds value from one format to another.
14
- * Specifically, it converts from 'moneyline' to 'impliedProbability', handling special cases.
15
- *
16
- * @param {Number} odds - The odds value to convert.
17
- * @returns {Number} The converted odds value.
18
- */
19
- export const convertOddsToImpl = (odds: number): number => {
20
- return odds === ZERO ? 0 : getOddsFromTo('decimal', 'impliedProbability', odds);
21
- };
22
-
23
- /**
24
- * Converts odds from one format to another.
25
- * @param {String} from - The original odds format.
26
- * @param {String} to - The target odds format.
27
- * @param {Number} input - The odds value.
28
- * @returns {Number} The converted odds.
29
- */
30
- export const getOddsFromTo = (from: string, to: string, input: number): number => {
31
- try {
32
- return oddslib.from(from, input).to(to);
33
- } catch (error) {
34
- return 0;
35
- }
36
- };
37
-
38
- /**
39
- * Filters the odds array to find entries matching the specified market name and bookmaker.
40
- *
41
- * @param {Array} oddsArray - The array of odds objects.
42
- * @param {string} marketName - The market name to filter by.
43
- * @param {Array} liveOddsProviders - Odds providers for live odds
44
- * @param {Object} commonData - The common data object.
45
- * @param {boolean} isTwoPositionalSport - Indicates if the sport is a two positional sport.,
46
- * @returns {Map} The filtered map for odds per provider.
47
- */
48
- export const filterOddsByMarketNameTeamNameBookmaker = (
49
- oddsArray: Odds,
50
- marketName: MoneylineTypes,
51
- liveOddsProviders: any[],
52
- commonData: HomeAwayTeams,
53
- isTwoPositionalSport: boolean
54
- ) => {
55
- const linesMap = new Map<any, any>();
56
- liveOddsProviders.forEach((oddsProvider) => {
57
- let homeOdds = 0;
58
- const homeTeamOddsObject = oddsArray.filter((odd) => {
59
- return (
60
- odd &&
61
- odd.marketName.toLowerCase() === marketName.toLowerCase() &&
62
- odd.sportsBookName.toLowerCase() == oddsProvider.toLowerCase() &&
63
- odd.selection.toLowerCase() === commonData.homeTeam.toLowerCase()
64
- );
65
- });
66
- if (homeTeamOddsObject.length !== 0) {
67
- homeOdds = homeTeamOddsObject[0].price;
68
- }
69
-
70
- let awayOdds = 0;
71
- const awayTeamOddsObject = oddsArray.filter(
72
- (odd) =>
73
- odd &&
74
- odd.marketName.toLowerCase() === marketName.toLowerCase() &&
75
- odd.sportsBookName.toLowerCase() == oddsProvider.toLowerCase() &&
76
- odd.selection.toLowerCase() === commonData.awayTeam.toLowerCase()
77
- );
78
-
79
- if (awayTeamOddsObject.length !== 0) {
80
- awayOdds = awayTeamOddsObject[0].price;
81
- }
82
-
83
- let drawOdds = 0;
84
- if (!isTwoPositionalSport) {
85
- const drawOddsObject = oddsArray.filter(
86
- (odd) =>
87
- odd &&
88
- odd.marketName.toLowerCase() === marketName.toLowerCase() &&
89
- odd.sportsBookName.toLowerCase() == oddsProvider.toLowerCase() &&
90
- odd.selection.toLowerCase() === DRAW.toLowerCase()
91
- );
92
-
93
- if (drawOddsObject.length !== 0) {
94
- drawOdds = drawOddsObject[0].price;
95
- }
96
- }
97
-
98
- linesMap.set(oddsProvider.toLowerCase(), {
99
- homeOdds: homeOdds,
100
- awayOdds: awayOdds,
101
- drawOdds: drawOdds,
102
- });
103
- });
104
- return linesMap;
105
- };
106
-
107
- /**
108
- * Retrieves the parent odds for the given event.
109
- *
110
- * @param {boolean} isTwoPositionalSport - Indicates if the sport is a two positional sport.
111
- * @param {Array} sportSpreadData - Spread data specific to the sport.
112
- * @param {Array} liveOddsProviders - Odds providers for live odds
113
- * @param {Object} oddsApiObject - Odds data from the API.
114
- * @param {String} sportId - Sport ID API.
115
- * @param {Number} defaultSpreadForLiveMarkets - Default spread for live markets,
116
- * @param {Number} maxPercentageDiffBetwenOdds - Maximum allowed percentage difference between same position odds from different providers
117
- * @returns {Array} The parent odds for the event [homeOdds, awayOdds, drawOdds].
118
- */
119
- export const getParentOdds = (
120
- isTwoPositionalSport: boolean,
121
- sportSpreadData: any[],
122
- liveOddsProviders: any[],
123
- oddsApiObject: OddsObject,
124
- sportId: string,
125
- defaultSpreadForLiveMarkets: number,
126
- maxPercentageDiffBetwenOdds: number
127
- ) => {
128
- const commonData = { homeTeam: oddsApiObject.homeTeam, awayTeam: oddsApiObject.awayTeam };
129
-
130
- // EXTRACTING ODDS FROM THE RESPONSE PER MARKET NAME AND BOOKMAKER
131
- const moneylineOddsMap = filterOddsByMarketNameTeamNameBookmaker(
132
- oddsApiObject.odds,
133
- MoneylineTypes.MONEYLINE,
134
- liveOddsProviders,
135
- commonData,
136
- isTwoPositionalSport
137
- );
138
-
139
- // CHECKING AND COMPARING ODDS FOR THE GIVEN BOOKMAKERS
140
- const oddsObject = checkOddsFromBookmakers(
141
- moneylineOddsMap,
142
- liveOddsProviders,
143
- isTwoPositionalSport,
144
- maxPercentageDiffBetwenOdds,
145
- MIN_ODDS_FOR_DIFF_CHECKING
146
- );
147
-
148
- if (oddsObject.errorMessage) {
149
- return {
150
- odds: isTwoPositionalSport ? [0, 0] : [0, 0, 0],
151
- errorMessage: oddsObject.errorMessage,
152
- };
153
- }
154
- const primaryBookmakerOdds = isTwoPositionalSport
155
- ? [oddsObject.homeOdds, oddsObject.awayOdds]
156
- : [oddsObject.homeOdds, oddsObject.awayOdds, oddsObject.drawOdds];
157
-
158
- let parentOdds = primaryBookmakerOdds.map((odd) => convertOddsToImpl(odd));
159
- const spreadData = getSpreadData(sportSpreadData, sportId, MONEYLINE_TYPE_ID, defaultSpreadForLiveMarkets);
160
-
161
- if (spreadData !== null) {
162
- parentOdds = adjustSpreadOnOdds(parentOdds, spreadData.minSpread, spreadData.targetSpread);
163
- } else {
164
- // Use min spread by sport if available, otherwise use default min spread
165
- parentOdds = adjustSpreadOnOdds(parentOdds, defaultSpreadForLiveMarkets, 0);
166
- }
167
- return { odds: parentOdds };
168
- };
169
-
170
- /**
171
- * Creates child markets based on the given parameters.
172
- *
173
- * @param {Object} leagueId - leagueId AKA sportId
174
- * @param {Array} spreadDataForSport - Spread data for sport.
175
- * @param {Object} apiResponseWithOdds - API response from the provider
176
- * @param {Array} liveOddsProviders - Odds providers for live odds
177
- * @param {Number} defaultSpreadForLiveMarkets - Default spread for live markets
178
- * @param {Boolean} leagueMap - League Map info
179
- * @returns {Array} The child markets.
180
- */
181
- export const createChildMarkets: (
182
- apiResponseWithOdds: OddsObject,
183
- spreadDataForSport: any,
184
- leagueId: number,
185
- liveOddsProviders: any,
186
- defaultSpreadForLiveMarkets: any,
187
- leagueMap: any
188
- ) => ChildMarket[] = (
189
- apiResponseWithOdds,
190
- spreadDataForSport,
191
- leagueId,
192
- liveOddsProviders,
193
- defaultSpreadForLiveMarkets,
194
- leagueMap
195
- ) => {
196
- const [spreadOdds, totalOdds, moneylineOdds, correctScoreOdds, doubleChanceOdds, ggOdds, childMarkets]: any[] = [
197
- [],
198
- [],
199
- [],
200
- [],
201
- [],
202
- [],
203
- [],
204
- ];
205
- const leagueInfo = getLeagueInfo(leagueId, leagueMap);
206
- const commonData = {
207
- homeTeam: apiResponseWithOdds.homeTeam,
208
- awayTeam: apiResponseWithOdds.awayTeam,
209
- };
210
-
211
- if (leagueInfo.length > 0) {
212
- // TODO ADD ODDS COMPARISON BETWEEN BOOKMAKERS
213
- const allChildOdds = filterOddsByMarketNameBookmaker(
214
- apiResponseWithOdds.odds,
215
- leagueInfo,
216
- liveOddsProviders[0]
217
- );
218
-
219
- allChildOdds.forEach((odd) => {
220
- if (odd.type === 'Total') {
221
- if (Math.abs(Number(odd.points) % 1) === 0.5) totalOdds.push(odd);
222
- } else if (odd.type === 'Spread') {
223
- if (Math.abs(Number(odd.points) % 1) === 0.5) spreadOdds.push(odd);
224
- } else if (odd.type === 'Moneyline') {
225
- moneylineOdds.push(odd);
226
- } else if (odd.type === 'Correct Score') {
227
- correctScoreOdds.push(odd);
228
- } else if (odd.type === 'Double Chance') {
229
- doubleChanceOdds.push(odd);
230
- } else if (odd.type === 'Both Teams To Score') {
231
- ggOdds.push(odd);
232
- }
233
- });
234
-
235
- const homeAwayFormattedOdds = [
236
- ...groupAndFormatSpreadOdds(spreadOdds, commonData),
237
- ...groupAndFormatTotalOdds(totalOdds, commonData),
238
- ...groupAndFormatMoneylineOdds(moneylineOdds, commonData),
239
- ...groupAndFormatGGOdds(ggOdds),
240
- ...groupAndFormatDoubleChanceOdds(doubleChanceOdds, commonData),
241
- ];
242
- const otherFormattedOdds = [...groupAndFormatCorrectScoreOdds(correctScoreOdds, commonData)];
243
-
244
- const homeAwayOddsWithSpreadAdjusted = adjustSpreadOnChildOdds(
245
- homeAwayFormattedOdds,
246
- spreadDataForSport,
247
- defaultSpreadForLiveMarkets
248
- );
249
-
250
- homeAwayOddsWithSpreadAdjusted.forEach((data) => {
251
- const childMarket = {
252
- leagueId: Number(data.sportId),
253
- typeId: Number(data.typeId),
254
- type: MarketTypeMap[data.typeId as MarketType]?.key || '',
255
- line: Number(data.line || 0),
256
- odds: data.odds,
257
- };
258
- const leagueInfoByTypeId = leagueInfo.find((league) => Number(league.typeId) === Number(data.typeId));
259
- const minOdds = leagueInfoByTypeId?.minOdds;
260
- const maxOdds = leagueInfoByTypeId?.maxOdds;
261
- if (
262
- !(
263
- minOdds &&
264
- maxOdds &&
265
- (data.odds[0] >= minOdds ||
266
- data.odds[0] <= maxOdds ||
267
- data.odds[1] >= minOdds ||
268
- data.odds[1] <= maxOdds)
269
- )
270
- ) {
271
- childMarkets.push(childMarket);
272
- }
273
- });
274
-
275
- otherFormattedOdds.forEach((data) => {
276
- const leagueInfoByTypeId = leagueInfo.find((league) => Number(league.typeId) === Number(data.typeId));
277
- const minOdds = leagueInfoByTypeId?.minOdds;
278
- const maxOdds = leagueInfoByTypeId?.maxOdds;
279
-
280
- const childMarket = {
281
- leagueId: Number(data.sportId),
282
- typeId: Number(data.typeId),
283
- type: MarketTypeMap[data.typeId as MarketType]?.key || '',
284
- line: Number(data.line || 0),
285
- odds: data.odds.map((odd: any) => {
286
- const impliedOdds = convertOddsToImpl(odd) || ZERO;
287
- return !minOdds || !maxOdds || impliedOdds >= minOdds || impliedOdds <= maxOdds ? 0 : impliedOdds;
288
- }),
289
- positionNames: data.positionNames,
290
- };
291
- childMarkets.push(childMarket);
292
- });
293
- } else {
294
- console.warn(`${NO_MARKETS_FOR_LEAGUE_ID}: ${Number(leagueId)}`);
295
- }
296
-
297
- return childMarkets;
298
- };
299
-
300
- /**
301
- * Filters the odds array to find entries matching the specified market name.
302
- *
303
- * @param {Array} oddsArray - The array of odds objects.
304
- * @param {string} leagueInfos - The market names to filter by.
305
- * @param {string} oddsProvider - The main odds provider to filter by.
306
- * @returns {Array} The filtered odds array.
307
- */
308
- export const filterOddsByMarketNameBookmaker = (
309
- oddsArray: Odds,
310
- leagueInfos: LeagueConfigInfo[],
311
- oddsProvider: string
312
- ): any[] => {
313
- const allChildMarketsTypes = leagueInfos
314
- .filter(
315
- (leagueInfo) =>
316
- leagueInfo.marketName.toLowerCase() !== MoneylineTypes.MONEYLINE.toLowerCase() &&
317
- leagueInfo.enabled === 'true'
318
- )
319
- .map((leagueInfo) => leagueInfo.marketName.toLowerCase());
320
- return oddsArray
321
- .filter(
322
- (odd) =>
323
- allChildMarketsTypes.includes(odd.marketName.toLowerCase()) &&
324
- odd.sportsBookName.toLowerCase() == oddsProvider.toLowerCase()
325
- )
326
- .map((odd) => {
327
- return {
328
- ...odd,
329
- ...leagueInfos.find(
330
- (leagueInfo) => leagueInfo.marketName.toLowerCase() === odd.marketName.toLowerCase()
331
- ), // using .find() for team totals means that we will always assign 10017 as typeID at this point
332
- };
333
- });
334
- };
335
-
336
- /**
337
- * Groups spread odds by their lines and formats the result.
338
- *
339
- * @param {Array} oddsArray - The input array of odds objects.
340
- * @param {Object} commonData - The common data object containing homeTeam information.
341
- * @returns {Array} The grouped and formatted spread odds.
342
- */
343
- export const groupAndFormatSpreadOdds = (oddsArray: any[], commonData: HomeAwayTeams) => {
344
- // Group odds by their selection points and selection
345
- const groupedOdds = oddsArray.reduce((acc: any, odd: any) => {
346
- const { points, marketName, price, selection, typeId, sportId, type } = odd;
347
- const isHomeTeam = selection === commonData.homeTeam;
348
-
349
- const key = `${marketName}_${isHomeTeam ? points : -points}`;
350
-
351
- if (!acc[key]) {
352
- acc[key] = { home: null, away: null, typeId: null, sportId: null };
353
- }
354
-
355
- if (isHomeTeam) {
356
- acc[key].home = price;
357
- } else {
358
- acc[key].away = price;
359
- }
360
-
361
- acc[key].typeId = typeId;
362
- acc[key].type = type;
363
- acc[key].sportId = sportId;
364
-
365
- return acc;
366
- }, {}) as any;
367
- // Format the grouped odds into the desired output
368
- const formattedOdds = Object.entries(groupedOdds as any).reduce((acc: any, [key, value]) => {
369
- const [_marketName, lineFloat] = key.split('_');
370
- const line = parseFloat(lineFloat);
371
- if ((value as any).home !== null && (value as any).away !== null) {
372
- acc.push({
373
- line: line as any,
374
- odds: [(value as any).home, (value as any).away],
375
- typeId: (value as any).typeId,
376
- sportId: (value as any).sportId,
377
- type: (value as any).type,
378
- });
379
- }
380
- return acc;
381
- }, []);
382
-
383
- return formattedOdds;
384
- };
385
-
386
- /**
387
- * Groups odds by selection and points over/under.
388
- *
389
- * @param {Array} oddsArray - The array of odds objects.
390
- * @returns {Object} The grouped odds.
391
- */
392
- export const groupAndFormatTotalOdds = (oddsArray: any[], commonData: HomeAwayTeams) => {
393
- // Group odds by their selection points and selection
394
- const groupedOdds = oddsArray.reduce((acc, odd) => {
395
- if (odd) {
396
- const key = `${odd.marketName}_${odd.selection}_${odd.points}`;
397
- if (!acc[key]) {
398
- acc[key] = { over: null, under: null };
399
- }
400
- if (odd.selectionLine === 'over') {
401
- acc[key].over = odd.price;
402
- } else if (odd.selectionLine === 'under') {
403
- acc[key].under = odd.price;
404
- }
405
-
406
- acc[key].typeId = odd.typeId;
407
- acc[key].type = odd.type;
408
- acc[key].sportId = odd.sportId;
409
- }
410
-
411
- return acc;
412
- }, {});
413
-
414
- // Format the grouped odds into the desired output
415
- const formattedOdds = Object.entries(groupedOdds as any).reduce((acc: any, [key, value]) => {
416
- const [_marketName, selection, selectionLine] = key.split('_');
417
- const line = parseFloat(selectionLine);
418
-
419
- // if we have away team in total odds we know the market is team total and we need to increase typeId by one.
420
- // if this is false typeId is already mapped correctly
421
- const isAwayTeam = selection === commonData.awayTeam;
422
- if ((value as any).over !== null && (value as any).under !== null) {
423
- acc.push({
424
- line: line as any,
425
- odds: [(value as any).over, (value as any).under],
426
- typeId: !isAwayTeam ? (value as any).typeId : Number((value as any).typeId) + 1,
427
- sportId: (value as any).sportId,
428
- type: (value as any).type,
429
- });
430
- }
431
- return acc;
432
- }, []);
433
-
434
- return formattedOdds;
435
- };
436
-
437
- /**
438
- * Groups moneyline odds by their lines and formats the result.
439
- *
440
- * @param {Array} oddsArray - The input array of odds objects.
441
- * @param {Object} commonData - The common data object containing homeTeam information.
442
- * @returns {Array} The grouped and formatted moneyline odds.
443
- */
444
- export const groupAndFormatMoneylineOdds = (oddsArray: any[], commonData: HomeAwayTeams) => {
445
- // Group odds by their selection points and selection
446
- const groupedOdds = oddsArray.reduce((acc: any, odd: any) => {
447
- const { price, selection, typeId, sportId, type } = odd;
448
- const key = typeId;
449
-
450
- if (!acc[key]) {
451
- acc[key] = { home: null, away: null, draw: null, typeId: null, sportId: null };
452
- }
453
-
454
- if (selection.toLowerCase() === commonData.homeTeam.toLowerCase()) acc[key].home = price;
455
- else if (selection.toLowerCase() === commonData.awayTeam.toLowerCase()) acc[key].away = price;
456
- else if (selection.toLowerCase() === DRAW.toLowerCase()) acc[key].draw = price;
457
-
458
- acc[key].typeId = typeId;
459
- acc[key].type = type;
460
- acc[key].sportId = sportId;
461
-
462
- return acc;
463
- }, {}) as any;
464
-
465
- // Format the grouped odds into the desired output
466
- const formattedOdds = Object.entries(groupedOdds as any).reduce((acc: any, [_key, value]) => {
467
- if ((value as any).home !== null && (value as any).away !== null) {
468
- acc.push({
469
- odds: (value as any).draw
470
- ? [(value as any).home, (value as any).away, (value as any).draw]
471
- : [(value as any).home, (value as any).away],
472
- typeId: (value as any).typeId,
473
- sportId: (value as any).sportId,
474
- type: (value as any).type,
475
- });
476
- }
477
- return acc;
478
- }, []);
479
-
480
- return formattedOdds;
481
- };
482
-
483
- /**w
484
- * Groups GG (Both Teams to Score) odds by their lines and formats the result.
485
- *
486
- * @param {Array} oddsArray - The input array of odds objects.
487
- * @returns {Array} The grouped and formatted moneyline odds.
488
- */
489
- export const groupAndFormatGGOdds = (oddsArray: any[]) => {
490
- const groupedOdds = oddsArray.reduce((acc: any, odd: any) => {
491
- const { price, selection, typeId, sportId, type } = odd;
492
- const key = typeId;
493
-
494
- if (!acc[key]) {
495
- acc[key] = { home: null, away: null, draw: null, typeId: null, sportId: null };
496
- }
497
-
498
- if (selection.toLowerCase() === 'yes') acc[key].home = price;
499
- else if (selection.toLowerCase() === 'no') acc[key].away = price;
500
-
501
- acc[key].typeId = typeId;
502
- acc[key].type = type;
503
- acc[key].sportId = sportId;
504
-
505
- return acc;
506
- }, {}) as any;
507
-
508
- // Format the grouped odds into the desired output
509
- const formattedOdds = Object.entries(groupedOdds as any).reduce((acc: any, [_key, value]) => {
510
- if ((value as any).home !== null && (value as any).away !== null) {
511
- acc.push({
512
- odds: (value as any).draw
513
- ? [(value as any).home, (value as any).away, (value as any).draw]
514
- : [(value as any).home, (value as any).away],
515
- typeId: (value as any).typeId,
516
- sportId: (value as any).sportId,
517
- type: (value as any).type,
518
- });
519
- }
520
- return acc;
521
- }, []);
522
-
523
- return formattedOdds;
524
- };
525
-
526
- /**
527
- * Normalize a score line like "A:B" to a map-friendly key "A_B" with
528
- * numeric normalization (removing any leading zeros).
529
- *
530
- * Behavior:
531
- * - Accepts optional whitespace around the colon (e.g., "03 : 10").
532
- * - Parses both sides as numbers to strip leading zeros:
533
- * "13:09" "13_9", "13:00" "13_0", "03:10" → "3_10".
534
- * - If the input does NOT match "<digits> : <digits>", falls back to a simple
535
- * string replace of ":" with "_" without numeric parsing.
536
- *
537
- * @param {string|number} line
538
- * A score string (e.g., "13:09") or any value coercible to string.
539
- *
540
- * @returns {string}
541
- * The normalized key (e.g., "13_9"). If not a digit:digit pattern, returns
542
- * the string with ":" replaced by "_".
543
- */
544
- function normalizeScoreLine(line: any) {
545
- const m = String(line).match(/(\d+)\s*:\s*(\d+)/);
546
- return m ? `${Number(m[1])}_${Number(m[2])}` : String(line).replace(':', '_');
547
- }
548
-
549
- /**
550
- * Groups correct score odds by their lines and formats the result.
551
- *
552
- * @param {Array} oddsArray - The input array of odds objects.
553
- * @param {Object} commonData - The common data object containing homeTeam and awayTeam information.
554
- * @returns {Array} The grouped and formatted correct score odds.
555
- */
556
- export const groupAndFormatCorrectScoreOdds = (oddsArray: any[], commonData: HomeAwayTeams): any[] => {
557
- const homeTeamKey = commonData.homeTeam.toLowerCase().replace(/\s+/g, '_');
558
- const awayTeamKey = commonData.awayTeam.toLowerCase().replace(/\s+/g, '_');
559
-
560
- // Group odds by typeId first
561
- const oddsByTypeId = oddsArray.reduce((acc: any, odd: any) => {
562
- const typeId = odd.typeId || 10100;
563
- if (!acc[typeId]) {
564
- acc[typeId] = [];
565
- }
566
- acc[typeId].push(odd);
567
- return acc;
568
- }, {});
569
-
570
- // Create market objects for each unique typeId
571
- const marketObjects: any[] = [];
572
-
573
- Object.entries(oddsByTypeId).forEach(([typeId, odds]: [string, any]) => {
574
- const oddsMap = {
575
- draw_0_0: 0,
576
- draw_1_1: 0,
577
- draw_2_2: 0,
578
- draw_3_3: 0,
579
- draw_4_4: 0,
580
- [`${homeTeamKey}_1_0`]: 0,
581
- [`${homeTeamKey}_2_0`]: 0,
582
- [`${homeTeamKey}_2_1`]: 0,
583
- [`${homeTeamKey}_3_0`]: 0,
584
- [`${homeTeamKey}_3_1`]: 0,
585
- [`${homeTeamKey}_3_2`]: 0,
586
- [`${homeTeamKey}_4_0`]: 0,
587
- [`${homeTeamKey}_4_1`]: 0,
588
- [`${homeTeamKey}_4_2`]: 0,
589
- [`${homeTeamKey}_4_3`]: 0,
590
- [`${awayTeamKey}_1_0`]: 0,
591
- [`${awayTeamKey}_2_0`]: 0,
592
- [`${awayTeamKey}_2_1`]: 0,
593
- [`${awayTeamKey}_3_0`]: 0,
594
- [`${awayTeamKey}_3_1`]: 0,
595
- [`${awayTeamKey}_3_2`]: 0,
596
- [`${awayTeamKey}_4_0`]: 0,
597
- [`${awayTeamKey}_4_1`]: 0,
598
- [`${awayTeamKey}_4_2`]: 0,
599
- [`${awayTeamKey}_4_3`]: 0,
600
- draw_5_5: 0,
601
- draw_6_6: 0,
602
- draw_7_7: 0,
603
- draw_8_8: 0,
604
- draw_9_9: 0,
605
- draw_10_10: 0,
606
- draw_11_11: 0,
607
- draw_12_12: 0,
608
- draw_13_13: 0,
609
- draw_14_14: 0,
610
- [`${homeTeamKey}_5_0`]: 0,
611
- [`${homeTeamKey}_5_1`]: 0,
612
- [`${homeTeamKey}_5_2`]: 0,
613
- [`${homeTeamKey}_5_3`]: 0,
614
- [`${homeTeamKey}_5_4`]: 0,
615
- [`${homeTeamKey}_6_0`]: 0,
616
- [`${homeTeamKey}_6_1`]: 0,
617
- [`${homeTeamKey}_6_2`]: 0,
618
- [`${homeTeamKey}_6_3`]: 0,
619
- [`${homeTeamKey}_6_4`]: 0,
620
- [`${homeTeamKey}_6_5`]: 0,
621
- [`${homeTeamKey}_7_0`]: 0,
622
- [`${homeTeamKey}_7_1`]: 0,
623
- [`${homeTeamKey}_7_2`]: 0,
624
- [`${homeTeamKey}_7_3`]: 0,
625
- [`${homeTeamKey}_7_4`]: 0,
626
- [`${homeTeamKey}_7_5`]: 0,
627
- [`${homeTeamKey}_7_6`]: 0,
628
- [`${homeTeamKey}_8_0`]: 0,
629
- [`${homeTeamKey}_8_1`]: 0,
630
- [`${homeTeamKey}_8_2`]: 0,
631
- [`${homeTeamKey}_8_3`]: 0,
632
- [`${homeTeamKey}_8_4`]: 0,
633
- [`${homeTeamKey}_8_5`]: 0,
634
- [`${homeTeamKey}_8_6`]: 0,
635
- [`${homeTeamKey}_8_7`]: 0,
636
- [`${homeTeamKey}_9_0`]: 0,
637
- [`${homeTeamKey}_9_1`]: 0,
638
- [`${homeTeamKey}_9_2`]: 0,
639
- [`${homeTeamKey}_9_3`]: 0,
640
- [`${homeTeamKey}_9_4`]: 0,
641
- [`${homeTeamKey}_9_5`]: 0,
642
- [`${homeTeamKey}_9_6`]: 0,
643
- [`${homeTeamKey}_9_7`]: 0,
644
- [`${homeTeamKey}_9_8`]: 0,
645
- [`${homeTeamKey}_10_0`]: 0,
646
- [`${homeTeamKey}_10_1`]: 0,
647
- [`${homeTeamKey}_10_2`]: 0,
648
- [`${homeTeamKey}_10_3`]: 0,
649
- [`${homeTeamKey}_10_4`]: 0,
650
- [`${homeTeamKey}_10_5`]: 0,
651
- [`${homeTeamKey}_10_6`]: 0,
652
- [`${homeTeamKey}_10_7`]: 0,
653
- [`${homeTeamKey}_10_8`]: 0,
654
- [`${homeTeamKey}_10_9`]: 0,
655
- [`${homeTeamKey}_11_0`]: 0,
656
- [`${homeTeamKey}_11_1`]: 0,
657
- [`${homeTeamKey}_11_2`]: 0,
658
- [`${homeTeamKey}_11_3`]: 0,
659
- [`${homeTeamKey}_11_4`]: 0,
660
- [`${homeTeamKey}_11_5`]: 0,
661
- [`${homeTeamKey}_11_6`]: 0,
662
- [`${homeTeamKey}_11_7`]: 0,
663
- [`${homeTeamKey}_11_8`]: 0,
664
- [`${homeTeamKey}_11_9`]: 0,
665
- [`${homeTeamKey}_11_10`]: 0,
666
- [`${homeTeamKey}_12_0`]: 0,
667
- [`${homeTeamKey}_12_1`]: 0,
668
- [`${homeTeamKey}_12_2`]: 0,
669
- [`${homeTeamKey}_12_3`]: 0,
670
- [`${homeTeamKey}_12_4`]: 0,
671
- [`${homeTeamKey}_12_5`]: 0,
672
- [`${homeTeamKey}_12_6`]: 0,
673
- [`${homeTeamKey}_12_7`]: 0,
674
- [`${homeTeamKey}_12_8`]: 0,
675
- [`${homeTeamKey}_12_9`]: 0,
676
- [`${homeTeamKey}_12_10`]: 0,
677
- [`${homeTeamKey}_12_11`]: 0,
678
- [`${homeTeamKey}_13_0`]: 0,
679
- [`${homeTeamKey}_13_1`]: 0,
680
- [`${homeTeamKey}_13_2`]: 0,
681
- [`${homeTeamKey}_13_3`]: 0,
682
- [`${homeTeamKey}_13_4`]: 0,
683
- [`${homeTeamKey}_13_5`]: 0,
684
- [`${homeTeamKey}_13_6`]: 0,
685
- [`${homeTeamKey}_13_7`]: 0,
686
- [`${homeTeamKey}_13_8`]: 0,
687
- [`${homeTeamKey}_13_9`]: 0,
688
- [`${homeTeamKey}_13_10`]: 0,
689
- [`${homeTeamKey}_13_11`]: 0,
690
- [`${homeTeamKey}_13_12`]: 0,
691
- [`${homeTeamKey}_14_0`]: 0,
692
- [`${homeTeamKey}_14_1`]: 0,
693
- [`${homeTeamKey}_14_2`]: 0,
694
- [`${homeTeamKey}_14_3`]: 0,
695
- [`${homeTeamKey}_14_4`]: 0,
696
- [`${homeTeamKey}_14_5`]: 0,
697
- [`${homeTeamKey}_14_6`]: 0,
698
- [`${homeTeamKey}_14_7`]: 0,
699
- [`${homeTeamKey}_14_8`]: 0,
700
- [`${homeTeamKey}_14_9`]: 0,
701
- [`${homeTeamKey}_14_10`]: 0,
702
- [`${homeTeamKey}_14_11`]: 0,
703
- [`${homeTeamKey}_14_12`]: 0,
704
- [`${homeTeamKey}_14_13`]: 0,
705
- [`${awayTeamKey}_5_0`]: 0,
706
- [`${awayTeamKey}_5_1`]: 0,
707
- [`${awayTeamKey}_5_2`]: 0,
708
- [`${awayTeamKey}_5_3`]: 0,
709
- [`${awayTeamKey}_5_4`]: 0,
710
- [`${awayTeamKey}_6_0`]: 0,
711
- [`${awayTeamKey}_6_1`]: 0,
712
- [`${awayTeamKey}_6_2`]: 0,
713
- [`${awayTeamKey}_6_3`]: 0,
714
- [`${awayTeamKey}_6_4`]: 0,
715
- [`${awayTeamKey}_6_5`]: 0,
716
- [`${awayTeamKey}_7_0`]: 0,
717
- [`${awayTeamKey}_7_1`]: 0,
718
- [`${awayTeamKey}_7_2`]: 0,
719
- [`${awayTeamKey}_7_3`]: 0,
720
- [`${awayTeamKey}_7_4`]: 0,
721
- [`${awayTeamKey}_7_5`]: 0,
722
- [`${awayTeamKey}_7_6`]: 0,
723
- [`${awayTeamKey}_8_0`]: 0,
724
- [`${awayTeamKey}_8_1`]: 0,
725
- [`${awayTeamKey}_8_2`]: 0,
726
- [`${awayTeamKey}_8_3`]: 0,
727
- [`${awayTeamKey}_8_4`]: 0,
728
- [`${awayTeamKey}_8_5`]: 0,
729
- [`${awayTeamKey}_8_6`]: 0,
730
- [`${awayTeamKey}_8_7`]: 0,
731
- [`${awayTeamKey}_9_0`]: 0,
732
- [`${awayTeamKey}_9_1`]: 0,
733
- [`${awayTeamKey}_9_2`]: 0,
734
- [`${awayTeamKey}_9_3`]: 0,
735
- [`${awayTeamKey}_9_4`]: 0,
736
- [`${awayTeamKey}_9_5`]: 0,
737
- [`${awayTeamKey}_9_6`]: 0,
738
- [`${awayTeamKey}_9_7`]: 0,
739
- [`${awayTeamKey}_9_8`]: 0,
740
- [`${awayTeamKey}_10_0`]: 0,
741
- [`${awayTeamKey}_10_1`]: 0,
742
- [`${awayTeamKey}_10_2`]: 0,
743
- [`${awayTeamKey}_10_3`]: 0,
744
- [`${awayTeamKey}_10_4`]: 0,
745
- [`${awayTeamKey}_10_5`]: 0,
746
- [`${awayTeamKey}_10_6`]: 0,
747
- [`${awayTeamKey}_10_7`]: 0,
748
- [`${awayTeamKey}_10_8`]: 0,
749
- [`${awayTeamKey}_10_9`]: 0,
750
- [`${awayTeamKey}_11_0`]: 0,
751
- [`${awayTeamKey}_11_1`]: 0,
752
- [`${awayTeamKey}_11_2`]: 0,
753
- [`${awayTeamKey}_11_3`]: 0,
754
- [`${awayTeamKey}_11_4`]: 0,
755
- [`${awayTeamKey}_11_5`]: 0,
756
- [`${awayTeamKey}_11_6`]: 0,
757
- [`${awayTeamKey}_11_7`]: 0,
758
- [`${awayTeamKey}_11_8`]: 0,
759
- [`${awayTeamKey}_11_9`]: 0,
760
- [`${awayTeamKey}_11_10`]: 0,
761
- [`${awayTeamKey}_12_0`]: 0,
762
- [`${awayTeamKey}_12_1`]: 0,
763
- [`${awayTeamKey}_12_2`]: 0,
764
- [`${awayTeamKey}_12_3`]: 0,
765
- [`${awayTeamKey}_12_4`]: 0,
766
- [`${awayTeamKey}_12_5`]: 0,
767
- [`${awayTeamKey}_12_6`]: 0,
768
- [`${awayTeamKey}_12_7`]: 0,
769
- [`${awayTeamKey}_12_8`]: 0,
770
- [`${awayTeamKey}_12_9`]: 0,
771
- [`${awayTeamKey}_12_10`]: 0,
772
- [`${awayTeamKey}_12_11`]: 0,
773
- [`${awayTeamKey}_13_0`]: 0,
774
- [`${awayTeamKey}_13_1`]: 0,
775
- [`${awayTeamKey}_13_2`]: 0,
776
- [`${awayTeamKey}_13_3`]: 0,
777
- [`${awayTeamKey}_13_4`]: 0,
778
- [`${awayTeamKey}_13_5`]: 0,
779
- [`${awayTeamKey}_13_6`]: 0,
780
- [`${awayTeamKey}_13_7`]: 0,
781
- [`${awayTeamKey}_13_8`]: 0,
782
- [`${awayTeamKey}_13_9`]: 0,
783
- [`${awayTeamKey}_13_10`]: 0,
784
- [`${awayTeamKey}_13_11`]: 0,
785
- [`${awayTeamKey}_13_12`]: 0,
786
- [`${awayTeamKey}_14_0`]: 0,
787
- [`${awayTeamKey}_14_1`]: 0,
788
- [`${awayTeamKey}_14_2`]: 0,
789
- [`${awayTeamKey}_14_3`]: 0,
790
- [`${awayTeamKey}_14_4`]: 0,
791
- [`${awayTeamKey}_14_5`]: 0,
792
- [`${awayTeamKey}_14_6`]: 0,
793
- [`${awayTeamKey}_14_7`]: 0,
794
- [`${awayTeamKey}_14_8`]: 0,
795
- [`${awayTeamKey}_14_9`]: 0,
796
- [`${awayTeamKey}_14_10`]: 0,
797
- [`${awayTeamKey}_14_11`]: 0,
798
- [`${awayTeamKey}_14_12`]: 0,
799
- [`${awayTeamKey}_14_13`]: 0,
800
- other: 0,
801
- };
802
-
803
- // Populate the oddsMap with the odds from this typeId's odds
804
- odds.forEach((odd: any) => {
805
- const normalizedSelection = `${odd.selection.toLowerCase().replace(/\s+/g, '_')}_${normalizeScoreLine(
806
- odd.selectionLine
807
- )}`;
808
- if (oddsMap.hasOwnProperty(normalizedSelection)) {
809
- oddsMap[normalizedSelection] = odd.price;
810
- }
811
- });
812
-
813
- const allOddsAreZero = Object.values(oddsMap).every((odd) => odd === ZERO);
814
-
815
- if (!allOddsAreZero) {
816
- const positionNames = Object.keys(oddsMap);
817
-
818
- // Create a market object for this typeId
819
- const marketObject = {
820
- homeTeam: commonData.homeTeam,
821
- awayTeam: commonData.awayTeam,
822
- line: 0,
823
- positionNames: positionNames,
824
- odds: Object.values(oddsMap),
825
- type: odds?.[0]?.type ? odds[0].type : 'Correct Score',
826
- typeId: Number(typeId),
827
- sportId: odds?.[0]?.sportId ? odds[0].sportId : undefined,
828
- };
829
- marketObjects.push(marketObject);
830
- }
831
- });
832
-
833
- return marketObjects;
834
- };
835
-
836
- /**
837
- * Groups double chance odds by their lines and formats the result.
838
- *
839
- * @param {Array} oddsArray - The input array of odds objects.
840
- * @param {Object} commonData - The common data object containing homeTeam and awayTeam information.
841
- * @returns {Array} The grouped and formatted correct score odds.
842
- */
843
- export const groupAndFormatDoubleChanceOdds = (oddsArray: any[], commonData: HomeAwayTeams) => {
844
- let probability1X = 0;
845
- let probability12 = 0;
846
- let probabilityX2 = 0;
847
-
848
- let sportId;
849
-
850
- oddsArray.forEach((odd) => {
851
- if (odd.sportId) {
852
- sportId = odd.sportId;
853
- }
854
- if (odd.selection.includes(commonData.homeTeam)) {
855
- if (odd.selection.includes(commonData.awayTeam)) {
856
- probability12 = odd.price;
857
- } else {
858
- probability1X = odd.price;
859
- }
860
- } else if (odd.selection.includes(commonData.awayTeam)) {
861
- probabilityX2 = odd.price;
862
- }
863
- });
864
-
865
- if ([probability1X, probability12, probabilityX2].every((odd) => odd === ZERO)) {
866
- return [];
867
- }
868
- // Create the market object
869
- const marketObject = {
870
- homeTeam: commonData.homeTeam,
871
- awayTeam: commonData.awayTeam,
872
- line: 0,
873
- odds: [probability1X, probability12, probabilityX2],
874
- type: 'Double Chance',
875
- typeId: 10003,
876
- sportId: sportId,
877
- };
878
- return [marketObject];
879
- };
880
-
881
- // used for home/away markets
882
- export const adjustSpreadOnChildOdds = (
883
- iterableGroupedOdds: any[],
884
- spreadDataForSport: any,
885
- defaultSpreadForLiveMarkets: any
886
- ) => {
887
- const result: any[] = [];
888
- iterableGroupedOdds.forEach((data) => {
889
- const hasDrawOdds = data.odds.length === 3;
890
- const homeTeamOdds = convertOddsToImpl(data.odds[0]) || ZERO;
891
- const awayTeamOdds = convertOddsToImpl(data.odds[1]) || ZERO;
892
- const drawOdds = convertOddsToImpl(data.odds[2]) || ZERO;
893
- const odds = hasDrawOdds ? [homeTeamOdds, awayTeamOdds, drawOdds] : [homeTeamOdds, awayTeamOdds];
894
-
895
- const isZeroOddsChild = homeTeamOdds === ZERO || awayTeamOdds === ZERO || (hasDrawOdds && drawOdds === ZERO);
896
- if (!isZeroOddsChild) {
897
- const spreadData = getSpreadData(
898
- spreadDataForSport,
899
- data.sportId,
900
- data.typeId,
901
- defaultSpreadForLiveMarkets
902
- );
903
-
904
- let adjustedOdds;
905
- if (spreadData !== null) {
906
- adjustedOdds = adjustSpreadOnOdds(odds, spreadData.minSpread, spreadData.targetSpread);
907
- } else {
908
- adjustedOdds = adjustSpreadOnOdds(odds, defaultSpreadForLiveMarkets, 0);
909
- }
910
-
911
- result.push({
912
- ...data,
913
- odds: adjustedOdds,
914
- });
915
- }
916
- });
917
- return result;
918
- };
1
+ import * as oddslib from 'oddslib';
2
+ import { MarketType, MarketTypeMap } from 'overtime-utils';
3
+ import { DRAW, MIN_ODDS_FOR_DIFF_CHECKING, MONEYLINE_TYPE_ID, ZERO } from '../constants/common';
4
+ import { LAST_POLLED_TOO_OLD, NO_MARKETS_FOR_LEAGUE_ID } from '../constants/errors';
5
+ import { MoneylineTypes } from '../enums/sports';
6
+ import { HomeAwayTeams, Odds, OddsObject } from '../types/odds';
7
+ import { ChildMarket, LastPolledArray, LeagueConfigInfo } from '../types/sports';
8
+ import {
9
+ checkOddsFromBookmakers,
10
+ checkOddsFromBookmakersForChildMarkets,
11
+ getPrimaryAndSecondaryBookmakerForTypeId,
12
+ isLastPolledForBookmakersValid,
13
+ } from './bookmakers';
14
+ import { getLeagueInfo } from './sports';
15
+ import { adjustSpreadOnOdds, getSpreadData } from './spread';
16
+
17
+ /**
18
+ * Converts a given odds value from one format to another.
19
+ * Specifically, it converts from 'moneyline' to 'impliedProbability', handling special cases.
20
+ *
21
+ * @param {Number} odds - The odds value to convert.
22
+ * @returns {Number} The converted odds value.
23
+ */
24
+ export const convertOddsToImpl = (odds: number): number => {
25
+ return odds === ZERO ? 0 : getOddsFromTo('decimal', 'impliedProbability', odds);
26
+ };
27
+
28
+ /**
29
+ * Converts odds from one format to another.
30
+ * @param {String} from - The original odds format.
31
+ * @param {String} to - The target odds format.
32
+ * @param {Number} input - The odds value.
33
+ * @returns {Number} The converted odds.
34
+ */
35
+ export const getOddsFromTo = (from: string, to: string, input: number): number => {
36
+ try {
37
+ return oddslib.from(from, input).to(to);
38
+ } catch (error) {
39
+ return 0;
40
+ }
41
+ };
42
+
43
+ /**
44
+ * Filters the odds array to find entries matching the specified market name and bookmaker.
45
+ *
46
+ * @param {Array} oddsArray - The array of odds objects.
47
+ * @param {string} marketName - The market name to filter by.
48
+ * @param {Array} liveOddsProviders - Odds providers for live odds
49
+ * @param {Object} commonData - The common data object.
50
+ * @param {boolean} isTwoPositionalSport - Indicates if the sport is a two positional sport.,
51
+ * @returns {Map} The filtered map for odds per provider.
52
+ */
53
+ export const filterOddsByMarketNameTeamNameBookmaker = (
54
+ oddsArray: Odds,
55
+ marketName: MoneylineTypes,
56
+ liveOddsProviders: any[],
57
+ commonData: HomeAwayTeams,
58
+ isTwoPositionalSport: boolean
59
+ ) => {
60
+ const linesMap = new Map<any, any>();
61
+ liveOddsProviders.forEach((oddsProvider) => {
62
+ let homeOdds = 0;
63
+ const homeTeamOddsObject = oddsArray.filter((odd) => {
64
+ return (
65
+ odd &&
66
+ odd.marketName.toLowerCase() === marketName.toLowerCase() &&
67
+ odd.sportsBookName.toLowerCase() == oddsProvider.toLowerCase() &&
68
+ odd.selection.toLowerCase() === commonData.homeTeam.toLowerCase()
69
+ );
70
+ });
71
+ if (homeTeamOddsObject.length !== 0) {
72
+ homeOdds = homeTeamOddsObject[0].price;
73
+ }
74
+
75
+ let awayOdds = 0;
76
+ const awayTeamOddsObject = oddsArray.filter(
77
+ (odd) =>
78
+ odd &&
79
+ odd.marketName.toLowerCase() === marketName.toLowerCase() &&
80
+ odd.sportsBookName.toLowerCase() == oddsProvider.toLowerCase() &&
81
+ odd.selection.toLowerCase() === commonData.awayTeam.toLowerCase()
82
+ );
83
+
84
+ if (awayTeamOddsObject.length !== 0) {
85
+ awayOdds = awayTeamOddsObject[0].price;
86
+ }
87
+
88
+ let drawOdds = 0;
89
+ if (!isTwoPositionalSport) {
90
+ const drawOddsObject = oddsArray.filter(
91
+ (odd) =>
92
+ odd &&
93
+ odd.marketName.toLowerCase() === marketName.toLowerCase() &&
94
+ odd.sportsBookName.toLowerCase() == oddsProvider.toLowerCase() &&
95
+ odd.selection.toLowerCase() === DRAW.toLowerCase()
96
+ );
97
+
98
+ if (drawOddsObject.length !== 0) {
99
+ drawOdds = drawOddsObject[0].price;
100
+ }
101
+ }
102
+
103
+ linesMap.set(oddsProvider.toLowerCase(), {
104
+ homeOdds: homeOdds,
105
+ awayOdds: awayOdds,
106
+ drawOdds: drawOdds,
107
+ });
108
+ });
109
+ return linesMap;
110
+ };
111
+
112
+ /**
113
+ * Retrieves the parent odds for the given event.
114
+ *
115
+ * @param {boolean} isTwoPositionalSport - Indicates if the sport is a two positional sport.
116
+ * @param {Array} sportSpreadData - Spread data specific to the sport.
117
+ * @param {Array} liveOddsProviders - Odds providers for live odds
118
+ * @param {Object} oddsApiObject - Odds data from the API.
119
+ * @param {String} sportId - Sport ID API.
120
+ * @param {Number} defaultSpreadForLiveMarkets - Default spread for live markets,
121
+ * @param {Number} maxPercentageDiffBetwenOdds - Maximum allowed percentage difference between same position odds from different providers
122
+ * @returns {Array} The parent odds for the event [homeOdds, awayOdds, drawOdds].
123
+ */
124
+ export const getParentOdds = (
125
+ isTwoPositionalSport: boolean,
126
+ sportSpreadData: any[],
127
+ liveOddsProviders: any[],
128
+ oddsApiObject: OddsObject,
129
+ sportId: string,
130
+ defaultSpreadForLiveMarkets: number,
131
+ maxPercentageDiffBetwenOdds: number,
132
+ leagueInfo: LeagueConfigInfo[],
133
+ lastPolledData: LastPolledArray,
134
+ maxAllowedProviderDataStaleDelay: number
135
+ ) => {
136
+ const commonData = { homeTeam: oddsApiObject.homeTeam, awayTeam: oddsApiObject.awayTeam };
137
+
138
+ const bookmakers = getPrimaryAndSecondaryBookmakerForTypeId(
139
+ liveOddsProviders,
140
+ leagueInfo,
141
+ 0 // typeId for moneyline
142
+ );
143
+
144
+ const isValidLastPolled = isLastPolledForBookmakersValid(
145
+ lastPolledData,
146
+ maxAllowedProviderDataStaleDelay,
147
+ bookmakers
148
+ );
149
+
150
+ if (!isValidLastPolled) {
151
+ return {
152
+ odds: isTwoPositionalSport ? [0, 0] : [0, 0, 0],
153
+ errorMessage: LAST_POLLED_TOO_OLD,
154
+ };
155
+ }
156
+
157
+ // EXTRACTING ODDS FROM THE RESPONSE PER MARKET NAME AND BOOKMAKER
158
+ const moneylineOddsMap = filterOddsByMarketNameTeamNameBookmaker(
159
+ oddsApiObject.odds,
160
+ MoneylineTypes.MONEYLINE,
161
+ bookmakers,
162
+ commonData,
163
+ isTwoPositionalSport
164
+ );
165
+
166
+ // CHECKING AND COMPARING ODDS FOR THE GIVEN BOOKMAKERS
167
+ const oddsObject = checkOddsFromBookmakers(
168
+ moneylineOddsMap,
169
+ bookmakers,
170
+ isTwoPositionalSport,
171
+ maxPercentageDiffBetwenOdds,
172
+ MIN_ODDS_FOR_DIFF_CHECKING
173
+ );
174
+
175
+ if (oddsObject.errorMessage) {
176
+ return {
177
+ odds: isTwoPositionalSport ? [0, 0] : [0, 0, 0],
178
+ errorMessage: oddsObject.errorMessage,
179
+ };
180
+ }
181
+ const primaryBookmakerOdds = isTwoPositionalSport
182
+ ? [oddsObject.homeOdds, oddsObject.awayOdds]
183
+ : [oddsObject.homeOdds, oddsObject.awayOdds, oddsObject.drawOdds];
184
+
185
+ let parentOdds = primaryBookmakerOdds.map((odd) => convertOddsToImpl(odd));
186
+ const spreadData = getSpreadData(sportSpreadData, sportId, MONEYLINE_TYPE_ID, defaultSpreadForLiveMarkets);
187
+
188
+ if (spreadData !== null) {
189
+ parentOdds = adjustSpreadOnOdds(parentOdds, spreadData.minSpread, spreadData.targetSpread);
190
+ } else {
191
+ // Use min spread by sport if available, otherwise use default min spread
192
+ parentOdds = adjustSpreadOnOdds(parentOdds, defaultSpreadForLiveMarkets, 0);
193
+ }
194
+ return { odds: parentOdds };
195
+ };
196
+
197
+ /**
198
+ * Creates child markets based on the given parameters.
199
+ *
200
+ * @param {Object} leagueId - leagueId AKA sportId
201
+ * @param {Array} spreadDataForSport - Spread data for sport.
202
+ * @param {Object} apiResponseWithOdds - API response from the provider
203
+ * @param {Array} liveOddsProviders - Odds providers for live odds
204
+ * @param {Number} defaultSpreadForLiveMarkets - Default spread for live markets
205
+ * @param {Boolean} leagueMap - League Map info
206
+ * @returns {Array} The child markets.
207
+ */
208
+ export const createChildMarkets: (
209
+ apiResponseWithOdds: OddsObject,
210
+ spreadDataForSport: any,
211
+ leagueId: number,
212
+ liveOddsProviders: any,
213
+ defaultSpreadForLiveMarkets: any,
214
+ leagueMap: any,
215
+ lastPolledData: LastPolledArray,
216
+ maxAllowedProviderDataStaleDelay: number,
217
+ maxImpliedPercentageDifference: number
218
+ ) => ChildMarket[] = (
219
+ apiResponseWithOdds,
220
+ spreadDataForSport,
221
+ leagueId,
222
+ liveOddsProviders,
223
+ defaultSpreadForLiveMarkets,
224
+ leagueMap,
225
+ lastPolledData,
226
+ maxAllowedProviderDataStaleDelay,
227
+ maxImpliedPercentageDifference
228
+ ) => {
229
+ const [spreadOdds, totalOdds, moneylineOdds, correctScoreOdds, doubleChanceOdds, ggOdds, childMarkets]: any[] = [
230
+ [],
231
+ [],
232
+ [],
233
+ [],
234
+ [],
235
+ [],
236
+ [],
237
+ ];
238
+ const leagueInfo = getLeagueInfo(leagueId, leagueMap);
239
+ const commonData = {
240
+ homeTeam: apiResponseWithOdds.homeTeam,
241
+ awayTeam: apiResponseWithOdds.awayTeam,
242
+ };
243
+
244
+ if (leagueInfo.length > 0) {
245
+ const allChildOdds = filterOddsByMarketNameBookmaker(apiResponseWithOdds.odds, leagueInfo);
246
+ const checkedChildOdds = checkOddsFromBookmakersForChildMarkets(
247
+ allChildOdds,
248
+ leagueInfo,
249
+ liveOddsProviders,
250
+ lastPolledData,
251
+ maxAllowedProviderDataStaleDelay,
252
+ maxImpliedPercentageDifference
253
+ );
254
+ checkedChildOdds.forEach((odd) => {
255
+ if (odd.type === 'Total') {
256
+ if (Math.abs(Number(odd.points) % 1) === 0.5) totalOdds.push(odd);
257
+ } else if (odd.type === 'Spread') {
258
+ if (Math.abs(Number(odd.points) % 1) === 0.5) spreadOdds.push(odd);
259
+ } else if (odd.type === 'Moneyline') {
260
+ moneylineOdds.push(odd);
261
+ } else if (odd.type === 'Correct Score') {
262
+ correctScoreOdds.push(odd);
263
+ } else if (odd.type === 'Double Chance') {
264
+ doubleChanceOdds.push(odd);
265
+ } else if (odd.type === 'Both Teams To Score') {
266
+ ggOdds.push(odd);
267
+ }
268
+ });
269
+
270
+ const homeAwayFormattedOdds = [
271
+ ...groupAndFormatSpreadOdds(spreadOdds, commonData),
272
+ ...groupAndFormatTotalOdds(totalOdds, commonData),
273
+ ...groupAndFormatMoneylineOdds(moneylineOdds, commonData),
274
+ ...groupAndFormatGGOdds(ggOdds),
275
+ ...groupAndFormatDoubleChanceOdds(doubleChanceOdds, commonData),
276
+ ];
277
+ const otherFormattedOdds = [...groupAndFormatCorrectScoreOdds(correctScoreOdds, commonData)];
278
+
279
+ // odds are converted to implied probability inside adjustSpreadOnChildOdds
280
+ const homeAwayOddsWithSpreadAdjusted = adjustSpreadOnChildOdds(
281
+ homeAwayFormattedOdds,
282
+ spreadDataForSport,
283
+ defaultSpreadForLiveMarkets
284
+ );
285
+
286
+ homeAwayOddsWithSpreadAdjusted.forEach((data) => {
287
+ const childMarket = {
288
+ leagueId: Number(data.sportId),
289
+ typeId: Number(data.typeId),
290
+ type: MarketTypeMap[data.typeId as MarketType]?.key || '',
291
+ line: Number(data.line || 0),
292
+ odds: data.odds,
293
+ };
294
+ const leagueInfoByTypeId = leagueInfo.find((league) => Number(league.typeId) === Number(data.typeId));
295
+ const minOdds = leagueInfoByTypeId?.minOdds; // minimum odds configured for child market (e.g. 0.95 implied probability)
296
+ const maxOdds = leagueInfoByTypeId?.maxOdds; // maximum odds configured for child market (e.g. 0.05 implied probability)
297
+
298
+ if (minOdds && maxOdds) {
299
+ let conditionToAddChildMarket = true;
300
+ data.odds.forEach((odd: number) => {
301
+ if (odd >= minOdds || odd <= maxOdds) {
302
+ conditionToAddChildMarket = false;
303
+ }
304
+ });
305
+ if (conditionToAddChildMarket) {
306
+ childMarkets.push(childMarket);
307
+ }
308
+ } else {
309
+ childMarkets.push(childMarket);
310
+ }
311
+ });
312
+
313
+ otherFormattedOdds.forEach((data) => {
314
+ const leagueInfoByTypeId = leagueInfo.find((league) => Number(league.typeId) === Number(data.typeId));
315
+ const minOdds = leagueInfoByTypeId?.minOdds;
316
+ const maxOdds = leagueInfoByTypeId?.maxOdds;
317
+
318
+ const childMarket = {
319
+ leagueId: Number(data.sportId),
320
+ typeId: Number(data.typeId),
321
+ type: MarketTypeMap[data.typeId as MarketType]?.key || '',
322
+ line: Number(data.line || 0),
323
+ odds: data.odds.map((odd: any) => {
324
+ const impliedOdds = convertOddsToImpl(odd) || ZERO;
325
+ return !minOdds || !maxOdds || impliedOdds >= minOdds || impliedOdds <= maxOdds ? 0 : impliedOdds;
326
+ }),
327
+ positionNames: data.positionNames,
328
+ };
329
+ childMarkets.push(childMarket);
330
+ });
331
+ } else {
332
+ console.warn(`${NO_MARKETS_FOR_LEAGUE_ID}: ${Number(leagueId)}`);
333
+ }
334
+
335
+ return childMarkets;
336
+ };
337
+
338
+ /**
339
+ * Filters the odds array to find entries matching the specified market name.
340
+ *
341
+ * @param {Array} oddsArray - The array of odds objects.
342
+ * @param {string} leagueInfos - The market names to filter by.
343
+ * @param {string} oddsProvider - The main odds provider to filter by.
344
+ * @returns {Array} The filtered odds array.
345
+ */
346
+ export const filterOddsByMarketNameBookmaker = (oddsArray: Odds, leagueInfos: LeagueConfigInfo[]): any => {
347
+ const allChildMarketsTypes = leagueInfos
348
+ .filter(
349
+ (leagueInfo) =>
350
+ leagueInfo.marketName.toLowerCase() !== MoneylineTypes.MONEYLINE.toLowerCase() &&
351
+ leagueInfo.enabled === 'true'
352
+ )
353
+ .map((leagueInfo) => leagueInfo.marketName.toLowerCase());
354
+ return oddsArray.reduce((acc: any, odd: any) => {
355
+ if (allChildMarketsTypes.includes(odd.marketName.toLowerCase())) {
356
+ const { points, marketName, selection, selectionLine, sportsBookName } = odd;
357
+ const key = `${sportsBookName.toLowerCase()}_${marketName.toLowerCase()}_${points}_${selection}_${selectionLine}`;
358
+ acc[key] = {
359
+ ...odd,
360
+ ...leagueInfos.find(
361
+ (leagueInfo) => leagueInfo.marketName.toLowerCase() === odd.marketName.toLowerCase()
362
+ ), // using .find() for team totals means that we will always assign 10017 as typeID at this point
363
+ };
364
+ }
365
+
366
+ return acc;
367
+ }, {}) as any;
368
+ };
369
+
370
+ /**
371
+ * Groups spread odds by their lines and formats the result.
372
+ *
373
+ * @param {Array} oddsArray - The input array of odds objects.
374
+ * @param {Object} commonData - The common data object containing homeTeam information.
375
+ * @returns {Array} The grouped and formatted spread odds.
376
+ */
377
+ export const groupAndFormatSpreadOdds = (oddsArray: any[], commonData: HomeAwayTeams) => {
378
+ // Group odds by their selection points and selection
379
+ const groupedOdds = oddsArray.reduce((acc: any, odd: any) => {
380
+ const { points, marketName, price, selection, typeId, sportId, type } = odd;
381
+ const isHomeTeam = selection === commonData.homeTeam;
382
+
383
+ const key = `${marketName}_${isHomeTeam ? points : -points}`;
384
+
385
+ if (!acc[key]) {
386
+ acc[key] = { home: null, away: null, typeId: null, sportId: null };
387
+ }
388
+
389
+ if (isHomeTeam) {
390
+ acc[key].home = price;
391
+ } else {
392
+ acc[key].away = price;
393
+ }
394
+
395
+ acc[key].typeId = typeId;
396
+ acc[key].type = type;
397
+ acc[key].sportId = sportId;
398
+
399
+ return acc;
400
+ }, {}) as any;
401
+ // Format the grouped odds into the desired output
402
+ const formattedOdds = Object.entries(groupedOdds as any).reduce((acc: any, [key, value]) => {
403
+ const [_marketName, lineFloat] = key.split('_');
404
+ const line = parseFloat(lineFloat);
405
+ if ((value as any).home !== null && (value as any).away !== null) {
406
+ acc.push({
407
+ line: line as any,
408
+ odds: [(value as any).home, (value as any).away],
409
+ typeId: (value as any).typeId,
410
+ sportId: (value as any).sportId,
411
+ type: (value as any).type,
412
+ });
413
+ }
414
+ return acc;
415
+ }, []);
416
+
417
+ return formattedOdds;
418
+ };
419
+
420
+ /**
421
+ * Groups odds by selection and points over/under.
422
+ *
423
+ * @param {Array} oddsArray - The array of odds objects.
424
+ * @returns {Object} The grouped odds.
425
+ */
426
+ export const groupAndFormatTotalOdds = (oddsArray: any[], commonData: HomeAwayTeams) => {
427
+ // Group odds by their selection points and selection
428
+ const groupedOdds = oddsArray.reduce((acc, odd) => {
429
+ if (odd) {
430
+ const key = `${odd.marketName}_${odd.selection}_${odd.points}`;
431
+ if (!acc[key]) {
432
+ acc[key] = { over: null, under: null };
433
+ }
434
+ if (odd.selectionLine === 'over') {
435
+ acc[key].over = odd.price;
436
+ } else if (odd.selectionLine === 'under') {
437
+ acc[key].under = odd.price;
438
+ }
439
+
440
+ acc[key].typeId = odd.typeId;
441
+ acc[key].type = odd.type;
442
+ acc[key].sportId = odd.sportId;
443
+ }
444
+
445
+ return acc;
446
+ }, {});
447
+
448
+ // Format the grouped odds into the desired output
449
+ const formattedOdds = Object.entries(groupedOdds as any).reduce((acc: any, [key, value]) => {
450
+ const [_marketName, selection, selectionLine] = key.split('_');
451
+ const line = parseFloat(selectionLine);
452
+
453
+ // if we have away team in total odds we know the market is team total and we need to increase typeId by one.
454
+ // if this is false typeId is already mapped correctly
455
+ const isAwayTeam = selection === commonData.awayTeam;
456
+ if ((value as any).over !== null && (value as any).under !== null) {
457
+ acc.push({
458
+ line: line as any,
459
+ odds: [(value as any).over, (value as any).under],
460
+ typeId: !isAwayTeam ? (value as any).typeId : Number((value as any).typeId) + 1,
461
+ sportId: (value as any).sportId,
462
+ type: (value as any).type,
463
+ });
464
+ }
465
+ return acc;
466
+ }, []);
467
+
468
+ return formattedOdds;
469
+ };
470
+
471
+ /**
472
+ * Groups moneyline odds by their lines and formats the result.
473
+ *
474
+ * @param {Array} oddsArray - The input array of odds objects.
475
+ * @param {Object} commonData - The common data object containing homeTeam information.
476
+ * @returns {Array} The grouped and formatted moneyline odds.
477
+ */
478
+ export const groupAndFormatMoneylineOdds = (oddsArray: any[], commonData: HomeAwayTeams) => {
479
+ // Group odds by their selection points and selection
480
+ const groupedOdds = oddsArray.reduce((acc: any, odd: any) => {
481
+ const { price, selection, typeId, sportId, type } = odd;
482
+ const key = typeId;
483
+
484
+ if (!acc[key]) {
485
+ acc[key] = { home: null, away: null, draw: null, typeId: null, sportId: null };
486
+ }
487
+
488
+ if (selection.toLowerCase() === commonData.homeTeam.toLowerCase()) acc[key].home = price;
489
+ else if (selection.toLowerCase() === commonData.awayTeam.toLowerCase()) acc[key].away = price;
490
+ else if (selection.toLowerCase() === DRAW.toLowerCase()) acc[key].draw = price;
491
+
492
+ acc[key].typeId = typeId;
493
+ acc[key].type = type;
494
+ acc[key].sportId = sportId;
495
+
496
+ return acc;
497
+ }, {}) as any;
498
+
499
+ // Format the grouped odds into the desired output
500
+ const formattedOdds = Object.entries(groupedOdds as any).reduce((acc: any, [_key, value]) => {
501
+ if ((value as any).home !== null && (value as any).away !== null) {
502
+ acc.push({
503
+ odds: (value as any).draw
504
+ ? [(value as any).home, (value as any).away, (value as any).draw]
505
+ : [(value as any).home, (value as any).away],
506
+ typeId: (value as any).typeId,
507
+ sportId: (value as any).sportId,
508
+ type: (value as any).type,
509
+ });
510
+ }
511
+ return acc;
512
+ }, []);
513
+
514
+ return formattedOdds;
515
+ };
516
+
517
+ /**w
518
+ * Groups GG (Both Teams to Score) odds by their lines and formats the result.
519
+ *
520
+ * @param {Array} oddsArray - The input array of odds objects.
521
+ * @returns {Array} The grouped and formatted moneyline odds.
522
+ */
523
+ export const groupAndFormatGGOdds = (oddsArray: any[]) => {
524
+ const groupedOdds = oddsArray.reduce((acc: any, odd: any) => {
525
+ const { price, selection, typeId, sportId, type } = odd;
526
+ const key = typeId;
527
+
528
+ if (!acc[key]) {
529
+ acc[key] = { home: null, away: null, draw: null, typeId: null, sportId: null };
530
+ }
531
+
532
+ if (selection.toLowerCase() === 'yes') acc[key].home = price;
533
+ else if (selection.toLowerCase() === 'no') acc[key].away = price;
534
+
535
+ acc[key].typeId = typeId;
536
+ acc[key].type = type;
537
+ acc[key].sportId = sportId;
538
+
539
+ return acc;
540
+ }, {}) as any;
541
+
542
+ // Format the grouped odds into the desired output
543
+ const formattedOdds = Object.entries(groupedOdds as any).reduce((acc: any, [_key, value]) => {
544
+ if ((value as any).home !== null && (value as any).away !== null) {
545
+ acc.push({
546
+ odds: (value as any).draw
547
+ ? [(value as any).home, (value as any).away, (value as any).draw]
548
+ : [(value as any).home, (value as any).away],
549
+ typeId: (value as any).typeId,
550
+ sportId: (value as any).sportId,
551
+ type: (value as any).type,
552
+ });
553
+ }
554
+ return acc;
555
+ }, []);
556
+
557
+ return formattedOdds;
558
+ };
559
+
560
+ /**
561
+ * Normalize a score line like "A:B" to a map-friendly key "A_B" with
562
+ * numeric normalization (removing any leading zeros).
563
+ *
564
+ * Behavior:
565
+ * - Accepts optional whitespace around the colon (e.g., "03 : 10").
566
+ * - Parses both sides as numbers to strip leading zeros:
567
+ * "13:09" → "13_9", "13:00" → "13_0", "03:10" → "3_10".
568
+ * - If the input does NOT match "<digits> : <digits>", falls back to a simple
569
+ * string replace of ":" with "_" without numeric parsing.
570
+ *
571
+ * @param {string|number} line
572
+ * A score string (e.g., "13:09") or any value coercible to string.
573
+ *
574
+ * @returns {string}
575
+ * The normalized key (e.g., "13_9"). If not a digit:digit pattern, returns
576
+ * the string with ":" replaced by "_".
577
+ */
578
+ function normalizeScoreLine(line: any) {
579
+ const m = String(line).match(/(\d+)\s*:\s*(\d+)/);
580
+ return m ? `${Number(m[1])}_${Number(m[2])}` : String(line).replace(':', '_');
581
+ }
582
+
583
+ /**
584
+ * Groups correct score odds by their lines and formats the result.
585
+ *
586
+ * @param {Array} oddsArray - The input array of odds objects.
587
+ * @param {Object} commonData - The common data object containing homeTeam and awayTeam information.
588
+ * @returns {Array} The grouped and formatted correct score odds.
589
+ */
590
+ export const groupAndFormatCorrectScoreOdds = (oddsArray: any[], commonData: HomeAwayTeams): any[] => {
591
+ const homeTeamKey = commonData.homeTeam.toLowerCase().replace(/\s+/g, '_');
592
+ const awayTeamKey = commonData.awayTeam.toLowerCase().replace(/\s+/g, '_');
593
+
594
+ // Group odds by typeId first
595
+ const oddsByTypeId = oddsArray.reduce((acc: any, odd: any) => {
596
+ const typeId = odd.typeId || 10100;
597
+ if (!acc[typeId]) {
598
+ acc[typeId] = [];
599
+ }
600
+ acc[typeId].push(odd);
601
+ return acc;
602
+ }, {});
603
+
604
+ // Create market objects for each unique typeId
605
+ const marketObjects: any[] = [];
606
+
607
+ Object.entries(oddsByTypeId).forEach(([typeId, odds]: [string, any]) => {
608
+ const oddsMap = {
609
+ draw_0_0: 0,
610
+ draw_1_1: 0,
611
+ draw_2_2: 0,
612
+ draw_3_3: 0,
613
+ draw_4_4: 0,
614
+ [`${homeTeamKey}_1_0`]: 0,
615
+ [`${homeTeamKey}_2_0`]: 0,
616
+ [`${homeTeamKey}_2_1`]: 0,
617
+ [`${homeTeamKey}_3_0`]: 0,
618
+ [`${homeTeamKey}_3_1`]: 0,
619
+ [`${homeTeamKey}_3_2`]: 0,
620
+ [`${homeTeamKey}_4_0`]: 0,
621
+ [`${homeTeamKey}_4_1`]: 0,
622
+ [`${homeTeamKey}_4_2`]: 0,
623
+ [`${homeTeamKey}_4_3`]: 0,
624
+ [`${awayTeamKey}_1_0`]: 0,
625
+ [`${awayTeamKey}_2_0`]: 0,
626
+ [`${awayTeamKey}_2_1`]: 0,
627
+ [`${awayTeamKey}_3_0`]: 0,
628
+ [`${awayTeamKey}_3_1`]: 0,
629
+ [`${awayTeamKey}_3_2`]: 0,
630
+ [`${awayTeamKey}_4_0`]: 0,
631
+ [`${awayTeamKey}_4_1`]: 0,
632
+ [`${awayTeamKey}_4_2`]: 0,
633
+ [`${awayTeamKey}_4_3`]: 0,
634
+ draw_5_5: 0,
635
+ draw_6_6: 0,
636
+ draw_7_7: 0,
637
+ draw_8_8: 0,
638
+ draw_9_9: 0,
639
+ draw_10_10: 0,
640
+ draw_11_11: 0,
641
+ draw_12_12: 0,
642
+ draw_13_13: 0,
643
+ draw_14_14: 0,
644
+ [`${homeTeamKey}_5_0`]: 0,
645
+ [`${homeTeamKey}_5_1`]: 0,
646
+ [`${homeTeamKey}_5_2`]: 0,
647
+ [`${homeTeamKey}_5_3`]: 0,
648
+ [`${homeTeamKey}_5_4`]: 0,
649
+ [`${homeTeamKey}_6_0`]: 0,
650
+ [`${homeTeamKey}_6_1`]: 0,
651
+ [`${homeTeamKey}_6_2`]: 0,
652
+ [`${homeTeamKey}_6_3`]: 0,
653
+ [`${homeTeamKey}_6_4`]: 0,
654
+ [`${homeTeamKey}_6_5`]: 0,
655
+ [`${homeTeamKey}_7_0`]: 0,
656
+ [`${homeTeamKey}_7_1`]: 0,
657
+ [`${homeTeamKey}_7_2`]: 0,
658
+ [`${homeTeamKey}_7_3`]: 0,
659
+ [`${homeTeamKey}_7_4`]: 0,
660
+ [`${homeTeamKey}_7_5`]: 0,
661
+ [`${homeTeamKey}_7_6`]: 0,
662
+ [`${homeTeamKey}_8_0`]: 0,
663
+ [`${homeTeamKey}_8_1`]: 0,
664
+ [`${homeTeamKey}_8_2`]: 0,
665
+ [`${homeTeamKey}_8_3`]: 0,
666
+ [`${homeTeamKey}_8_4`]: 0,
667
+ [`${homeTeamKey}_8_5`]: 0,
668
+ [`${homeTeamKey}_8_6`]: 0,
669
+ [`${homeTeamKey}_8_7`]: 0,
670
+ [`${homeTeamKey}_9_0`]: 0,
671
+ [`${homeTeamKey}_9_1`]: 0,
672
+ [`${homeTeamKey}_9_2`]: 0,
673
+ [`${homeTeamKey}_9_3`]: 0,
674
+ [`${homeTeamKey}_9_4`]: 0,
675
+ [`${homeTeamKey}_9_5`]: 0,
676
+ [`${homeTeamKey}_9_6`]: 0,
677
+ [`${homeTeamKey}_9_7`]: 0,
678
+ [`${homeTeamKey}_9_8`]: 0,
679
+ [`${homeTeamKey}_10_0`]: 0,
680
+ [`${homeTeamKey}_10_1`]: 0,
681
+ [`${homeTeamKey}_10_2`]: 0,
682
+ [`${homeTeamKey}_10_3`]: 0,
683
+ [`${homeTeamKey}_10_4`]: 0,
684
+ [`${homeTeamKey}_10_5`]: 0,
685
+ [`${homeTeamKey}_10_6`]: 0,
686
+ [`${homeTeamKey}_10_7`]: 0,
687
+ [`${homeTeamKey}_10_8`]: 0,
688
+ [`${homeTeamKey}_10_9`]: 0,
689
+ [`${homeTeamKey}_11_0`]: 0,
690
+ [`${homeTeamKey}_11_1`]: 0,
691
+ [`${homeTeamKey}_11_2`]: 0,
692
+ [`${homeTeamKey}_11_3`]: 0,
693
+ [`${homeTeamKey}_11_4`]: 0,
694
+ [`${homeTeamKey}_11_5`]: 0,
695
+ [`${homeTeamKey}_11_6`]: 0,
696
+ [`${homeTeamKey}_11_7`]: 0,
697
+ [`${homeTeamKey}_11_8`]: 0,
698
+ [`${homeTeamKey}_11_9`]: 0,
699
+ [`${homeTeamKey}_11_10`]: 0,
700
+ [`${homeTeamKey}_12_0`]: 0,
701
+ [`${homeTeamKey}_12_1`]: 0,
702
+ [`${homeTeamKey}_12_2`]: 0,
703
+ [`${homeTeamKey}_12_3`]: 0,
704
+ [`${homeTeamKey}_12_4`]: 0,
705
+ [`${homeTeamKey}_12_5`]: 0,
706
+ [`${homeTeamKey}_12_6`]: 0,
707
+ [`${homeTeamKey}_12_7`]: 0,
708
+ [`${homeTeamKey}_12_8`]: 0,
709
+ [`${homeTeamKey}_12_9`]: 0,
710
+ [`${homeTeamKey}_12_10`]: 0,
711
+ [`${homeTeamKey}_12_11`]: 0,
712
+ [`${homeTeamKey}_13_0`]: 0,
713
+ [`${homeTeamKey}_13_1`]: 0,
714
+ [`${homeTeamKey}_13_2`]: 0,
715
+ [`${homeTeamKey}_13_3`]: 0,
716
+ [`${homeTeamKey}_13_4`]: 0,
717
+ [`${homeTeamKey}_13_5`]: 0,
718
+ [`${homeTeamKey}_13_6`]: 0,
719
+ [`${homeTeamKey}_13_7`]: 0,
720
+ [`${homeTeamKey}_13_8`]: 0,
721
+ [`${homeTeamKey}_13_9`]: 0,
722
+ [`${homeTeamKey}_13_10`]: 0,
723
+ [`${homeTeamKey}_13_11`]: 0,
724
+ [`${homeTeamKey}_13_12`]: 0,
725
+ [`${homeTeamKey}_14_0`]: 0,
726
+ [`${homeTeamKey}_14_1`]: 0,
727
+ [`${homeTeamKey}_14_2`]: 0,
728
+ [`${homeTeamKey}_14_3`]: 0,
729
+ [`${homeTeamKey}_14_4`]: 0,
730
+ [`${homeTeamKey}_14_5`]: 0,
731
+ [`${homeTeamKey}_14_6`]: 0,
732
+ [`${homeTeamKey}_14_7`]: 0,
733
+ [`${homeTeamKey}_14_8`]: 0,
734
+ [`${homeTeamKey}_14_9`]: 0,
735
+ [`${homeTeamKey}_14_10`]: 0,
736
+ [`${homeTeamKey}_14_11`]: 0,
737
+ [`${homeTeamKey}_14_12`]: 0,
738
+ [`${homeTeamKey}_14_13`]: 0,
739
+ [`${awayTeamKey}_5_0`]: 0,
740
+ [`${awayTeamKey}_5_1`]: 0,
741
+ [`${awayTeamKey}_5_2`]: 0,
742
+ [`${awayTeamKey}_5_3`]: 0,
743
+ [`${awayTeamKey}_5_4`]: 0,
744
+ [`${awayTeamKey}_6_0`]: 0,
745
+ [`${awayTeamKey}_6_1`]: 0,
746
+ [`${awayTeamKey}_6_2`]: 0,
747
+ [`${awayTeamKey}_6_3`]: 0,
748
+ [`${awayTeamKey}_6_4`]: 0,
749
+ [`${awayTeamKey}_6_5`]: 0,
750
+ [`${awayTeamKey}_7_0`]: 0,
751
+ [`${awayTeamKey}_7_1`]: 0,
752
+ [`${awayTeamKey}_7_2`]: 0,
753
+ [`${awayTeamKey}_7_3`]: 0,
754
+ [`${awayTeamKey}_7_4`]: 0,
755
+ [`${awayTeamKey}_7_5`]: 0,
756
+ [`${awayTeamKey}_7_6`]: 0,
757
+ [`${awayTeamKey}_8_0`]: 0,
758
+ [`${awayTeamKey}_8_1`]: 0,
759
+ [`${awayTeamKey}_8_2`]: 0,
760
+ [`${awayTeamKey}_8_3`]: 0,
761
+ [`${awayTeamKey}_8_4`]: 0,
762
+ [`${awayTeamKey}_8_5`]: 0,
763
+ [`${awayTeamKey}_8_6`]: 0,
764
+ [`${awayTeamKey}_8_7`]: 0,
765
+ [`${awayTeamKey}_9_0`]: 0,
766
+ [`${awayTeamKey}_9_1`]: 0,
767
+ [`${awayTeamKey}_9_2`]: 0,
768
+ [`${awayTeamKey}_9_3`]: 0,
769
+ [`${awayTeamKey}_9_4`]: 0,
770
+ [`${awayTeamKey}_9_5`]: 0,
771
+ [`${awayTeamKey}_9_6`]: 0,
772
+ [`${awayTeamKey}_9_7`]: 0,
773
+ [`${awayTeamKey}_9_8`]: 0,
774
+ [`${awayTeamKey}_10_0`]: 0,
775
+ [`${awayTeamKey}_10_1`]: 0,
776
+ [`${awayTeamKey}_10_2`]: 0,
777
+ [`${awayTeamKey}_10_3`]: 0,
778
+ [`${awayTeamKey}_10_4`]: 0,
779
+ [`${awayTeamKey}_10_5`]: 0,
780
+ [`${awayTeamKey}_10_6`]: 0,
781
+ [`${awayTeamKey}_10_7`]: 0,
782
+ [`${awayTeamKey}_10_8`]: 0,
783
+ [`${awayTeamKey}_10_9`]: 0,
784
+ [`${awayTeamKey}_11_0`]: 0,
785
+ [`${awayTeamKey}_11_1`]: 0,
786
+ [`${awayTeamKey}_11_2`]: 0,
787
+ [`${awayTeamKey}_11_3`]: 0,
788
+ [`${awayTeamKey}_11_4`]: 0,
789
+ [`${awayTeamKey}_11_5`]: 0,
790
+ [`${awayTeamKey}_11_6`]: 0,
791
+ [`${awayTeamKey}_11_7`]: 0,
792
+ [`${awayTeamKey}_11_8`]: 0,
793
+ [`${awayTeamKey}_11_9`]: 0,
794
+ [`${awayTeamKey}_11_10`]: 0,
795
+ [`${awayTeamKey}_12_0`]: 0,
796
+ [`${awayTeamKey}_12_1`]: 0,
797
+ [`${awayTeamKey}_12_2`]: 0,
798
+ [`${awayTeamKey}_12_3`]: 0,
799
+ [`${awayTeamKey}_12_4`]: 0,
800
+ [`${awayTeamKey}_12_5`]: 0,
801
+ [`${awayTeamKey}_12_6`]: 0,
802
+ [`${awayTeamKey}_12_7`]: 0,
803
+ [`${awayTeamKey}_12_8`]: 0,
804
+ [`${awayTeamKey}_12_9`]: 0,
805
+ [`${awayTeamKey}_12_10`]: 0,
806
+ [`${awayTeamKey}_12_11`]: 0,
807
+ [`${awayTeamKey}_13_0`]: 0,
808
+ [`${awayTeamKey}_13_1`]: 0,
809
+ [`${awayTeamKey}_13_2`]: 0,
810
+ [`${awayTeamKey}_13_3`]: 0,
811
+ [`${awayTeamKey}_13_4`]: 0,
812
+ [`${awayTeamKey}_13_5`]: 0,
813
+ [`${awayTeamKey}_13_6`]: 0,
814
+ [`${awayTeamKey}_13_7`]: 0,
815
+ [`${awayTeamKey}_13_8`]: 0,
816
+ [`${awayTeamKey}_13_9`]: 0,
817
+ [`${awayTeamKey}_13_10`]: 0,
818
+ [`${awayTeamKey}_13_11`]: 0,
819
+ [`${awayTeamKey}_13_12`]: 0,
820
+ [`${awayTeamKey}_14_0`]: 0,
821
+ [`${awayTeamKey}_14_1`]: 0,
822
+ [`${awayTeamKey}_14_2`]: 0,
823
+ [`${awayTeamKey}_14_3`]: 0,
824
+ [`${awayTeamKey}_14_4`]: 0,
825
+ [`${awayTeamKey}_14_5`]: 0,
826
+ [`${awayTeamKey}_14_6`]: 0,
827
+ [`${awayTeamKey}_14_7`]: 0,
828
+ [`${awayTeamKey}_14_8`]: 0,
829
+ [`${awayTeamKey}_14_9`]: 0,
830
+ [`${awayTeamKey}_14_10`]: 0,
831
+ [`${awayTeamKey}_14_11`]: 0,
832
+ [`${awayTeamKey}_14_12`]: 0,
833
+ [`${awayTeamKey}_14_13`]: 0,
834
+ other: 0,
835
+ };
836
+
837
+ // Populate the oddsMap with the odds from this typeId's odds
838
+ odds.forEach((odd: any) => {
839
+ const normalizedSelection = `${odd.selection.toLowerCase().replace(/\s+/g, '_')}_${normalizeScoreLine(
840
+ odd.selectionLine
841
+ )}`;
842
+ if (oddsMap.hasOwnProperty(normalizedSelection)) {
843
+ oddsMap[normalizedSelection] = odd.price;
844
+ }
845
+ });
846
+
847
+ const allOddsAreZero = Object.values(oddsMap).every((odd) => odd === ZERO);
848
+
849
+ if (!allOddsAreZero) {
850
+ const positionNames = Object.keys(oddsMap);
851
+
852
+ // Create a market object for this typeId
853
+ const marketObject = {
854
+ homeTeam: commonData.homeTeam,
855
+ awayTeam: commonData.awayTeam,
856
+ line: 0,
857
+ positionNames: positionNames,
858
+ odds: Object.values(oddsMap),
859
+ type: odds?.[0]?.type ? odds[0].type : 'Correct Score',
860
+ typeId: Number(typeId),
861
+ sportId: odds?.[0]?.sportId ? odds[0].sportId : undefined,
862
+ };
863
+ marketObjects.push(marketObject);
864
+ }
865
+ });
866
+
867
+ return marketObjects;
868
+ };
869
+
870
+ /**
871
+ * Groups double chance odds by their lines and formats the result.
872
+ *
873
+ * @param {Array} oddsArray - The input array of odds objects.
874
+ * @param {Object} commonData - The common data object containing homeTeam and awayTeam information.
875
+ * @returns {Array} The grouped and formatted correct score odds.
876
+ */
877
+ export const groupAndFormatDoubleChanceOdds = (oddsArray: any[], commonData: HomeAwayTeams) => {
878
+ let probability1X = 0;
879
+ let probability12 = 0;
880
+ let probabilityX2 = 0;
881
+
882
+ let sportId;
883
+
884
+ oddsArray.forEach((odd) => {
885
+ if (odd.sportId) {
886
+ sportId = odd.sportId;
887
+ }
888
+ if (odd.selection.includes(commonData.homeTeam)) {
889
+ if (odd.selection.includes(commonData.awayTeam)) {
890
+ probability12 = odd.price;
891
+ } else {
892
+ probability1X = odd.price;
893
+ }
894
+ } else if (odd.selection.includes(commonData.awayTeam)) {
895
+ probabilityX2 = odd.price;
896
+ }
897
+ });
898
+
899
+ if ([probability1X, probability12, probabilityX2].every((odd) => odd === ZERO)) {
900
+ return [];
901
+ }
902
+ // Create the market object
903
+ const marketObject = {
904
+ homeTeam: commonData.homeTeam,
905
+ awayTeam: commonData.awayTeam,
906
+ line: 0,
907
+ odds: [probability1X, probability12, probabilityX2],
908
+ type: 'Double Chance',
909
+ typeId: 10003,
910
+ sportId: sportId,
911
+ };
912
+ return [marketObject];
913
+ };
914
+
915
+ // used for home/away markets
916
+ export const adjustSpreadOnChildOdds = (
917
+ iterableGroupedOdds: any[],
918
+ spreadDataForSport: any,
919
+ defaultSpreadForLiveMarkets: any
920
+ ) => {
921
+ const result: any[] = [];
922
+ iterableGroupedOdds.forEach((data) => {
923
+ const hasDrawOdds = data.odds.length === 3;
924
+ const homeTeamOdds = convertOddsToImpl(data.odds[0]) || ZERO;
925
+ const awayTeamOdds = convertOddsToImpl(data.odds[1]) || ZERO;
926
+ const drawOdds = convertOddsToImpl(data.odds[2]) || ZERO;
927
+ const odds = hasDrawOdds ? [homeTeamOdds, awayTeamOdds, drawOdds] : [homeTeamOdds, awayTeamOdds];
928
+
929
+ const isZeroOddsChild = homeTeamOdds === ZERO || awayTeamOdds === ZERO || (hasDrawOdds && drawOdds === ZERO);
930
+ if (!isZeroOddsChild) {
931
+ const spreadData = getSpreadData(
932
+ spreadDataForSport,
933
+ data.sportId,
934
+ data.typeId,
935
+ defaultSpreadForLiveMarkets
936
+ );
937
+
938
+ let adjustedOdds;
939
+ if (spreadData !== null) {
940
+ adjustedOdds = adjustSpreadOnOdds(odds, spreadData.minSpread, spreadData.targetSpread);
941
+ } else {
942
+ adjustedOdds = adjustSpreadOnOdds(odds, defaultSpreadForLiveMarkets, 0);
943
+ }
944
+
945
+ result.push({
946
+ ...data,
947
+ odds: adjustedOdds,
948
+ });
949
+ }
950
+ });
951
+ return result;
952
+ };