overtime-live-trading-utils 2.1.27 → 2.1.29

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 (38) hide show
  1. package/.circleci/config.yml +32 -32
  2. package/.prettierrc +9 -9
  3. package/CLAUDE.md +77 -0
  4. package/codecov.yml +20 -20
  5. package/index.ts +26 -26
  6. package/jest.config.ts +16 -16
  7. package/main.js +1 -1
  8. package/package.json +30 -30
  9. package/resolution_live_markets.md +351 -0
  10. package/src/constants/common.ts +7 -7
  11. package/src/constants/errors.ts +6 -6
  12. package/src/constants/sports.ts +78 -78
  13. package/src/enums/sports.ts +109 -109
  14. package/src/tests/mock/MockLeagueMap.ts +170 -170
  15. package/src/tests/mock/MockOpticOddsEvents.ts +518 -518
  16. package/src/tests/mock/MockOpticSoccer.ts +9378 -9378
  17. package/src/tests/mock/MockSoccerRedis.ts +2308 -2308
  18. package/src/tests/unit/bookmakers.test.ts +79 -79
  19. package/src/tests/unit/markets.test.ts +156 -156
  20. package/src/tests/unit/odds.test.ts +92 -92
  21. package/src/tests/unit/resolution.test.ts +935 -935
  22. package/src/tests/unit/sports.test.ts +58 -58
  23. package/src/tests/unit/spread.test.ts +131 -131
  24. package/src/types/missing-types.d.ts +2 -2
  25. package/src/types/odds.ts +61 -61
  26. package/src/types/resolution.ts +96 -96
  27. package/src/types/sports.ts +19 -19
  28. package/src/utils/bookmakers.ts +159 -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 +674 -912
  33. package/src/utils/opticOdds.ts +71 -71
  34. package/src/utils/resolution.ts +291 -291
  35. package/src/utils/sports.ts +51 -51
  36. package/src/utils/spread.ts +97 -97
  37. package/tsconfig.json +16 -16
  38. package/webpack.config.js +24 -24
@@ -1,96 +1,96 @@
1
- export type PeriodScores = {
2
- [key: string]: { home: number; away: number };
3
- };
4
-
5
- export type PeriodResolutionData = {
6
- completedPeriods: number[];
7
- readyForResolution: boolean;
8
- periodScores: PeriodScores;
9
- currentPeriod?: number;
10
- };
11
-
12
- /**
13
- * OpticOdds Event type from their API response
14
- * Uses 'any' for flexibility as API structure may vary
15
- */
16
- export type OpticOddsEvent = any;
17
-
18
- /**
19
- * Sport period structure types
20
- * Different sports have different period structures that affect how typeIds are resolved
21
- */
22
- export enum SportPeriodType {
23
- /** Sports with 2 halves (Soccer, NCAAB) */
24
- HALVES_BASED = 'halves_based',
25
- /** Sports with 4 quarters (NFL, NBA) */
26
- QUARTERS_BASED = 'quarters_based',
27
- /** Sports with 9+ innings (MLB, NPB, KBO, College Baseball, etc.) */
28
- INNINGS_BASED = 'innings_based',
29
- /** Sports with 9 periods without halves or secondary moneyline types */
30
- PERIOD_BASED = 'period_based',
31
- }
32
-
33
- /**
34
- * Halves-based sports period-to-typeId mapping (Soccer, NCAAB)
35
- * Period 1 = 1st half
36
- * Period 2 = 2nd half
37
- */
38
- export const HALVES_PERIOD_TYPE_ID_MAPPING: { [period: number]: number[] } = {
39
- 1: [10021, 10031, 10041, 10051, 10061, 10071, 10081, 10111, 10112, 10121, 10163], // 1st half
40
- 2: [10022, 10032, 10042, 10052, 10062, 10072, 10082, 10211, 10212, 10122], // 2nd half
41
- };
42
-
43
- /**
44
- * Quarters-based sports period-to-typeId mapping (NFL, NBA)
45
- * Period 1 = 1st quarter
46
- * Period 2 = 2nd quarter (also completes 1st half - typeId 10051)
47
- * Period 3 = 3rd quarter
48
- * Period 4 = 4th quarter (also completes 2nd half - typeId 10052)
49
- * Period 5+ = Overtime
50
- */
51
- export const QUARTERS_PERIOD_TYPE_ID_MAPPING: { [period: number]: number[] } = {
52
- 1: [10021, 10031, 10041, 10061, 10071, 10081, 10111, 10112, 10121, 10163], // 1st quarter
53
- 2: [10022, 10032, 10042, 10051, 10062, 10072, 10082, 10211, 10212, 10122], // 2nd quarter + 1st half
54
- 3: [10023, 10033, 10043, 10063, 10073, 10083, 10311, 10312, 10123], // 3rd quarter
55
- 4: [10024, 10034, 10044, 10052, 10064, 10074, 10084, 10411, 10412, 10124], // 4th quarter + 2nd half
56
- 5: [10025, 10035, 10045, 10055, 10065, 10075, 10085, 10511, 10512], // Overtime/5th period
57
- };
58
-
59
- /**
60
- * Innings-based sports period-to-typeId mapping (MLB, NPB, KBO, College Baseball)
61
- * Period 1-5 = Innings 1-5 (period 5 completes 1st half - typeId 10051)
62
- * Period 6-9 = Innings 6-9 (period 9 completes 2nd half - typeId 10052)
63
- */
64
- export const INNINGS_PERIOD_TYPE_ID_MAPPING: { [period: number]: number[] } = {
65
- 1: [10021, 10031, 10041, 10061, 10071, 10081, 10111, 10112, 10121, 10163], // 1st inning
66
- 2: [10022, 10032, 10042, 10062, 10072, 10082, 10211, 10212, 10122], // 2nd inning
67
- 3: [10023, 10033, 10043, 10063, 10073, 10083, 10311, 10312, 10123], // 3rd inning
68
- 4: [10024, 10034, 10044, 10064, 10074, 10084, 10411, 10412, 10124], // 4th inning
69
- 5: [10025, 10035, 10045, 10051, 10065, 10075, 10085, 10511, 10512], // 5th inning + 1st half
70
- 6: [10026, 10036, 10046, 10056, 10066, 10076, 10086, 10611, 10612], // 6th inning
71
- 7: [10027, 10037, 10047, 10057, 10067, 10077, 10087, 10711, 10712], // 7th inning
72
- 8: [10028, 10038, 10048, 10058, 10068, 10078, 10088, 10811, 10812], // 8th inning
73
- 9: [10029, 10039, 10049, 10052, 10069, 10079, 10089], // 9th inning + 2nd half
74
- };
75
-
76
- /**
77
- * Period-based sports period-to-typeId mapping (9 periods)
78
- * Excludes halves types (10051, 10052) and secondary moneyline types (ending in 11/12)
79
- */
80
- export const PERIOD_BASED_TYPE_ID_MAPPING: { [period: number]: number[] } = {
81
- 1: [10021, 10031, 10041, 10061, 10071, 10081, 10121, 10163], // 1st period
82
- 2: [10022, 10032, 10042, 10062, 10072, 10082, 10122], // 2nd period
83
- 3: [10023, 10033, 10043, 10063, 10073, 10083, 10123], // 3rd period
84
- 4: [10024, 10034, 10044, 10064, 10074, 10084, 10124], // 4th period
85
- 5: [10025, 10035, 10045, 10065, 10075, 10085], // 5th period
86
- 6: [10026, 10036, 10046, 10056, 10066, 10076, 10086], // 6th period
87
- 7: [10027, 10037, 10047, 10057, 10067, 10077, 10087], // 7th period
88
- 8: [10028, 10038, 10048, 10058, 10068, 10078, 10088], // 8th period
89
- 9: [10029, 10039, 10049, 10069, 10079, 10089], // 9th period
90
- };
91
-
92
- /**
93
- * Full game type IDs that should NOT be resolved during live games
94
- * These can only be resolved when the game status is "completed"
95
- */
96
- export const FULL_GAME_TYPE_IDS: number[] = [0, 10001, 10002, 10003, 10004, 10010, 10011, 10012];
1
+ export type PeriodScores = {
2
+ [key: string]: { home: number; away: number };
3
+ };
4
+
5
+ export type PeriodResolutionData = {
6
+ completedPeriods: number[];
7
+ readyForResolution: boolean;
8
+ periodScores: PeriodScores;
9
+ currentPeriod?: number;
10
+ };
11
+
12
+ /**
13
+ * OpticOdds Event type from their API response
14
+ * Uses 'any' for flexibility as API structure may vary
15
+ */
16
+ export type OpticOddsEvent = any;
17
+
18
+ /**
19
+ * Sport period structure types
20
+ * Different sports have different period structures that affect how typeIds are resolved
21
+ */
22
+ export enum SportPeriodType {
23
+ /** Sports with 2 halves (Soccer, NCAAB) */
24
+ HALVES_BASED = 'halves_based',
25
+ /** Sports with 4 quarters (NFL, NBA) */
26
+ QUARTERS_BASED = 'quarters_based',
27
+ /** Sports with 9+ innings (MLB, NPB, KBO, College Baseball, etc.) */
28
+ INNINGS_BASED = 'innings_based',
29
+ /** Sports with 9 periods without halves or secondary moneyline types */
30
+ PERIOD_BASED = 'period_based',
31
+ }
32
+
33
+ /**
34
+ * Halves-based sports period-to-typeId mapping (Soccer, NCAAB)
35
+ * Period 1 = 1st half
36
+ * Period 2 = 2nd half
37
+ */
38
+ export const HALVES_PERIOD_TYPE_ID_MAPPING: { [period: number]: number[] } = {
39
+ 1: [10021, 10031, 10041, 10051, 10061, 10071, 10081, 10111, 10112, 10121, 10163, 10200], // 1st half
40
+ 2: [10022, 10032, 10042, 10052, 10062, 10072, 10082, 10211, 10212, 10122, 10164, 10201], // 2nd half
41
+ };
42
+
43
+ /**
44
+ * Quarters-based sports period-to-typeId mapping (NFL, NBA)
45
+ * Period 1 = 1st quarter
46
+ * Period 2 = 2nd quarter (also completes 1st half - typeId 10051)
47
+ * Period 3 = 3rd quarter
48
+ * Period 4 = 4th quarter (also completes 2nd half - typeId 10052)
49
+ * Period 5+ = Overtime
50
+ */
51
+ export const QUARTERS_PERIOD_TYPE_ID_MAPPING: { [period: number]: number[] } = {
52
+ 1: [10021, 10031, 10041, 10061, 10071, 10081, 10111, 10112, 10121, 10163], // 1st quarter
53
+ 2: [10022, 10032, 10042, 10051, 10062, 10072, 10082, 10211, 10212, 10122, 10164], // 2nd quarter + 1st half
54
+ 3: [10023, 10033, 10043, 10063, 10073, 10083, 10311, 10312, 10123, 10165], // 3rd quarter
55
+ 4: [10024, 10034, 10044, 10052, 10064, 10074, 10084, 10411, 10412, 10124, 10166], // 4th quarter + 2nd half
56
+ 5: [10025, 10035, 10045, 10055, 10065, 10075, 10085, 10511, 10512, 10167], // Overtime/5th period
57
+ };
58
+
59
+ /**
60
+ * Innings-based sports period-to-typeId mapping (MLB, NPB, KBO, College Baseball)
61
+ * Period 1-5 = Innings 1-5 (period 5 completes 1st half - typeId 10051)
62
+ * Period 6-9 = Innings 6-9 (period 9 completes 2nd half - typeId 10052)
63
+ */
64
+ export const INNINGS_PERIOD_TYPE_ID_MAPPING: { [period: number]: number[] } = {
65
+ 1: [10021, 10031, 10041, 10061, 10071, 10081, 10111, 10112, 10121, 10163], // 1st inning
66
+ 2: [10022, 10032, 10042, 10062, 10072, 10082, 10211, 10212, 10122, 10164], // 2nd inning
67
+ 3: [10023, 10033, 10043, 10063, 10073, 10083, 10311, 10312, 10123, 10165, 10170, 10171, 10172, 10173, 10174, 10201], // 3rd inning + first 3 innings
68
+ 4: [10024, 10034, 10044, 10064, 10074, 10084, 10411, 10412, 10124, 10166], // 4th inning
69
+ 5: [10025, 10035, 10045, 10051, 10065, 10075, 10085, 10511, 10512, 10167, 10200], // 5th inning + 1st half (first 5 innings)
70
+ 6: [10026, 10036, 10046, 10056, 10066, 10076, 10086, 10611, 10612, 10168], // 6th inning
71
+ 7: [10027, 10037, 10047, 10057, 10067, 10077, 10087, 10711, 10712, 10169, 10175, 10176, 10177, 10178, 10179, 10202], // 7th inning + first 7 innings
72
+ 8: [10028, 10038, 10048, 10058, 10068, 10078, 10088, 10811, 10812, 10197], // 8th inning
73
+ 9: [10029, 10039, 10049, 10052, 10069, 10079, 10089, 10198], // 9th inning
74
+ };
75
+
76
+ /**
77
+ * Period-based sports period-to-typeId mapping (9 periods)
78
+ * Excludes halves types (10051, 10052) and secondary moneyline types (ending in 11/12)
79
+ */
80
+ export const PERIOD_BASED_TYPE_ID_MAPPING: { [period: number]: number[] } = {
81
+ 1: [10021, 10031, 10041, 10061, 10071, 10081, 10121, 10163], // 1st period
82
+ 2: [10022, 10032, 10042, 10062, 10072, 10082, 10122, 10164], // 2nd period
83
+ 3: [10023, 10033, 10043, 10063, 10073, 10083, 10123, 10165], // 3rd period
84
+ 4: [10024, 10034, 10044, 10064, 10074, 10084, 10124, 10166], // 4th period
85
+ 5: [10025, 10035, 10045, 10065, 10075, 10085, 10167], // 5th period
86
+ 6: [10026, 10036, 10046, 10056, 10066, 10076, 10086, 10168], // 6th period
87
+ 7: [10027, 10037, 10047, 10057, 10067, 10077, 10087, 10169], // 7th period
88
+ 8: [10028, 10038, 10048, 10058, 10068, 10078, 10088, 10197], // 8th period
89
+ 9: [10029, 10039, 10049, 10069, 10079, 10089, 10198], // 9th period
90
+ };
91
+
92
+ /**
93
+ * Full game type IDs that should NOT be resolved during live games
94
+ * These can only be resolved when the game status is "completed"
95
+ */
96
+ export const FULL_GAME_TYPE_IDS: number[] = [0, 10001, 10002, 10003, 10004, 10010, 10011, 10012];
@@ -1,19 +1,19 @@
1
-
2
- export type LeagueConfigInfo = {
3
- sportId: number;
4
- typeId: number;
5
- marketName: string;
6
- type: string;
7
- enabled: string;
8
- minOdds: number;
9
- maxOdds: number;
10
- addedSpread?: number;
11
- };
12
-
13
- export type ChildMarket = {
14
- leagueId: number;
15
- typeId: number;
16
- type: string;
17
- line: number;
18
- odds: Array<number>;
19
- };
1
+
2
+ export type LeagueConfigInfo = {
3
+ sportId: number;
4
+ typeId: number;
5
+ marketName: string;
6
+ type: string;
7
+ enabled: string;
8
+ minOdds: number;
9
+ maxOdds: number;
10
+ addedSpread?: number;
11
+ };
12
+
13
+ export type ChildMarket = {
14
+ leagueId: number;
15
+ typeId: number;
16
+ type: string;
17
+ line: number;
18
+ odds: Array<number>;
19
+ };
@@ -1,159 +1,159 @@
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 {
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
+ };