overtime-live-trading-utils 2.1.19 → 2.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) 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 +7 -7
  9. package/src/constants/errors.ts +6 -6
  10. package/src/constants/sports.ts +78 -78
  11. package/src/enums/sports.ts +109 -109
  12. package/src/tests/mock/MockLeagueMap.ts +170 -170
  13. package/src/tests/mock/MockOpticOddsEvents.ts +518 -518
  14. package/src/tests/mock/MockOpticSoccer.ts +9378 -9378
  15. package/src/tests/mock/MockSoccerRedis.ts +2308 -2308
  16. package/src/tests/unit/bookmakers.test.ts +79 -79
  17. package/src/tests/unit/markets.test.ts +156 -156
  18. package/src/tests/unit/odds.test.ts +92 -92
  19. package/src/tests/unit/resolution.test.ts +1043 -1043
  20. package/src/tests/unit/sports.test.ts +58 -58
  21. package/src/tests/unit/spread.test.ts +131 -131
  22. package/src/types/missing-types.d.ts +2 -2
  23. package/src/types/odds.ts +61 -61
  24. package/src/types/resolution.ts +96 -96
  25. package/src/types/sports.ts +19 -19
  26. package/src/utils/bookmakers.ts +159 -159
  27. package/src/utils/constraints.ts +210 -210
  28. package/src/utils/gameMatching.ts +81 -81
  29. package/src/utils/markets.ts +119 -119
  30. package/src/utils/odds.ts +674 -674
  31. package/src/utils/opticOdds.ts +71 -71
  32. package/src/utils/resolution.ts +229 -255
  33. package/src/utils/sports.ts +51 -51
  34. package/src/utils/spread.ts +97 -97
  35. package/tsconfig.json +16 -16
  36. package/webpack.config.js +24 -24
  37. package/CLAUDE.md +0 -77
  38. package/resolution_live_markets.md +0 -351
@@ -1,71 +1,71 @@
1
- import { Fixture, OddsObject, ScoresObject } from '../types/odds';
2
-
3
- export const mapOpticOddsApiFixtures = (fixturesData: any[]): Fixture[] =>
4
- fixturesData.map(
5
- (fixtureData) =>
6
- ({
7
- gameId: fixtureData.id, // fixture_id
8
- startDate: fixtureData.start_date,
9
- homeTeam: fixtureData.home_team_display,
10
- awayTeam: fixtureData.away_team_display,
11
- } as Fixture)
12
- );
13
-
14
- export const mapOpticOddsApiResults = (resultsData: any[]): ScoresObject[] =>
15
- resultsData.map(
16
- (resultData) =>
17
- ({
18
- gameId: resultData.fixture.id, // fixture_id
19
- sport: resultData.sport.name,
20
- league: resultData.league.name.toLowerCase(),
21
- status: resultData.fixture.status ? resultData.fixture.status.toLowerCase() : resultData.fixture.status,
22
- isLive: resultData.fixture.is_live,
23
- clock: resultData.in_play.clock,
24
- period: resultData.in_play.period ? resultData.in_play.period.toLowerCase() : resultData.in_play.period,
25
- homeTeam: resultData.fixture.home_team_display,
26
- awayTeam: resultData.fixture.away_team_display,
27
- homeTotal: resultData.scores.home.total,
28
- awayTotal: resultData.scores.away.total,
29
- ...mapScorePeriods(resultData.scores.home.periods, 'home'),
30
- ...mapScorePeriods(resultData.scores.away.periods, 'away'),
31
- } as ScoresObject)
32
- );
33
-
34
- export const mapOpticOddsApiFixtureOdds = (oddsDataArray: any[]): OddsObject[] =>
35
- oddsDataArray.map(
36
- (oddsData) =>
37
- ({
38
- gameId: oddsData.id, // fixture_id
39
- startDate: oddsData.start_date,
40
- homeTeam: oddsData.home_team_display,
41
- awayTeam: oddsData.away_team_display,
42
- isLive: oddsData.is_live,
43
- status: oddsData.status,
44
- sport: oddsData.sport.id,
45
- league: oddsData.league.name,
46
- odds: oddsData.odds.map((oddsObj: any) => ({
47
- id: oddsObj.id, // 39920-20584-2024-35:draftkings:2nd_set_moneyline:francisco_comesana
48
- sportsBookName: oddsObj.sportsbook,
49
- name: oddsObj.name,
50
- price: oddsObj.price,
51
- timestamp: oddsObj.timestamp,
52
- points: oddsObj.points,
53
- isMain: oddsObj.is_main,
54
- isLive: oddsData.is_live,
55
- marketName: oddsObj.market.toLowerCase(),
56
- playerId: oddsObj.player_id,
57
- selection: oddsObj.selection,
58
- selectionLine: oddsObj.selection_line,
59
- })),
60
- } as OddsObject)
61
- );
62
-
63
- const mapScorePeriods = (periods: any, homeAwayType: string) =>
64
- Object.entries(periods).reduce((acc, period) => {
65
- const periodKey = period[0].split('_')[1];
66
- const periodValue = period[1];
67
- return {
68
- ...acc,
69
- [`${homeAwayType}Period${periodKey}`]: periodValue,
70
- };
71
- }, {});
1
+ import { Fixture, OddsObject, ScoresObject } from '../types/odds';
2
+
3
+ export const mapOpticOddsApiFixtures = (fixturesData: any[]): Fixture[] =>
4
+ fixturesData.map(
5
+ (fixtureData) =>
6
+ ({
7
+ gameId: fixtureData.id, // fixture_id
8
+ startDate: fixtureData.start_date,
9
+ homeTeam: fixtureData.home_team_display,
10
+ awayTeam: fixtureData.away_team_display,
11
+ } as Fixture)
12
+ );
13
+
14
+ export const mapOpticOddsApiResults = (resultsData: any[]): ScoresObject[] =>
15
+ resultsData.map(
16
+ (resultData) =>
17
+ ({
18
+ gameId: resultData.fixture.id, // fixture_id
19
+ sport: resultData.sport.name,
20
+ league: resultData.league.name.toLowerCase(),
21
+ status: resultData.fixture.status ? resultData.fixture.status.toLowerCase() : resultData.fixture.status,
22
+ isLive: resultData.fixture.is_live,
23
+ clock: resultData.in_play.clock,
24
+ period: resultData.in_play.period ? resultData.in_play.period.toLowerCase() : resultData.in_play.period,
25
+ homeTeam: resultData.fixture.home_team_display,
26
+ awayTeam: resultData.fixture.away_team_display,
27
+ homeTotal: resultData.scores.home.total,
28
+ awayTotal: resultData.scores.away.total,
29
+ ...mapScorePeriods(resultData.scores.home.periods, 'home'),
30
+ ...mapScorePeriods(resultData.scores.away.periods, 'away'),
31
+ } as ScoresObject)
32
+ );
33
+
34
+ export const mapOpticOddsApiFixtureOdds = (oddsDataArray: any[]): OddsObject[] =>
35
+ oddsDataArray.map(
36
+ (oddsData) =>
37
+ ({
38
+ gameId: oddsData.id, // fixture_id
39
+ startDate: oddsData.start_date,
40
+ homeTeam: oddsData.home_team_display,
41
+ awayTeam: oddsData.away_team_display,
42
+ isLive: oddsData.is_live,
43
+ status: oddsData.status,
44
+ sport: oddsData.sport.id,
45
+ league: oddsData.league.name,
46
+ odds: oddsData.odds.map((oddsObj: any) => ({
47
+ id: oddsObj.id, // 39920-20584-2024-35:draftkings:2nd_set_moneyline:francisco_comesana
48
+ sportsBookName: oddsObj.sportsbook,
49
+ name: oddsObj.name,
50
+ price: oddsObj.price,
51
+ timestamp: oddsObj.timestamp,
52
+ points: oddsObj.points,
53
+ isMain: oddsObj.is_main,
54
+ isLive: oddsData.is_live,
55
+ marketName: oddsObj.market.toLowerCase(),
56
+ playerId: oddsObj.player_id,
57
+ selection: oddsObj.selection,
58
+ selectionLine: oddsObj.selection_line,
59
+ })),
60
+ } as OddsObject)
61
+ );
62
+
63
+ const mapScorePeriods = (periods: any, homeAwayType: string) =>
64
+ Object.entries(periods).reduce((acc, period) => {
65
+ const periodKey = period[0].split('_')[1];
66
+ const periodValue = period[1];
67
+ return {
68
+ ...acc,
69
+ [`${homeAwayType}Period${periodKey}`]: periodValue,
70
+ };
71
+ }, {});
@@ -1,255 +1,229 @@
1
- import {
2
- PeriodResolutionData,
3
- PeriodScores,
4
- OpticOddsEvent,
5
- SportPeriodType,
6
- HALVES_PERIOD_TYPE_ID_MAPPING,
7
- QUARTERS_PERIOD_TYPE_ID_MAPPING,
8
- INNINGS_PERIOD_TYPE_ID_MAPPING,
9
- PERIOD_BASED_TYPE_ID_MAPPING,
10
- FULL_GAME_TYPE_IDS,
11
- } from '../types/resolution';
12
-
13
- /**
14
- * Detects completed periods for a game based on OpticOdds API event data
15
- * A period is considered complete if the next period (period + 1) exists in the scores
16
- * @param event - Event object from OpticOdds API
17
- * @returns PeriodResolutionData with completed periods, readiness status, and period scores
18
- */
19
- export const detectCompletedPeriods = (
20
- event: OpticOddsEvent
21
- ): PeriodResolutionData | null => {
22
- const status = (event.fixture?.status || event.status || '').toLowerCase();
23
- const isLive = event.fixture?.is_live ?? event.is_live ?? false;
24
-
25
- // Extract period scores from the event
26
- const homePeriods = event.scores?.home?.periods || {};
27
- const awayPeriods = event.scores?.away?.periods || {};
28
-
29
- const periodScores: PeriodScores = {};
30
- const completedPeriods: number[] = [];
31
-
32
- // Parse all available periods
33
- const periodKeys = Object.keys(homePeriods)
34
- .filter((key) => key.startsWith('period_'))
35
- .map((key) => parseInt(key.replace('period_', '')))
36
- .sort((a, b) => a - b);
37
-
38
- if (periodKeys.length === 0) {
39
- return null; // No period data available
40
- }
41
-
42
- // Get current period from in_play if available (only use if numeric)
43
- const inPlayPeriod = event.in_play?.period;
44
- const currentLivePeriod = inPlayPeriod && !isNaN(parseInt(inPlayPeriod)) ? parseInt(inPlayPeriod) : null;
45
-
46
- // Check if game is in overtime/extra time (non-numeric period indicator)
47
- const isInOvertime = inPlayPeriod &&
48
- (inPlayPeriod.toLowerCase().includes('overtime') ||
49
- inPlayPeriod.toLowerCase().includes('ot') ||
50
- inPlayPeriod.toLowerCase().includes('extra'));
51
-
52
- // For each period, check if it's complete
53
- for (const periodNum of periodKeys) {
54
- const key = `period_${periodNum}`;
55
- const homeScore = homePeriods[key];
56
- const awayScore = awayPeriods[key];
57
-
58
- // Add this period's score
59
- if (homeScore !== undefined && awayScore !== undefined && !isNaN(homeScore) && !isNaN(awayScore)) {
60
- periodScores[`period${periodNum}`] = { home: homeScore, away: awayScore };
61
-
62
- // Period is complete if:
63
- // 1. Next period exists in periods data, OR
64
- // 2. Game is completed, OR
65
- // 3. Game is live with numeric period AND current live period is greater than this period, OR
66
- // 4. Game is in overtime (all regulation periods are complete)
67
- const nextPeriodKey = `period_${periodNum + 1}`;
68
- const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
69
- const hasNextPeriod = homePeriods[nextPeriodKey] !== undefined && awayPeriods[nextPeriodKey] !== undefined;
70
- const isCompletedInLiveGame = isLive && currentLivePeriod !== null && currentLivePeriod > periodNum;
71
-
72
- if (hasNextPeriod || isCompleted || isCompletedInLiveGame || isInOvertime) {
73
- completedPeriods.push(periodNum);
74
- }
75
- }
76
- }
77
-
78
- // Determine current period
79
- // If we have a numeric in_play period and it's greater than highest period in data, use it
80
- // Otherwise use highest period number from the data
81
- const highestPeriodInData = periodKeys.length > 0 ? Math.max(...periodKeys) : undefined;
82
- const currentPeriod = currentLivePeriod && currentLivePeriod > (highestPeriodInData || 0)
83
- ? currentLivePeriod
84
- : highestPeriodInData;
85
-
86
- return completedPeriods.length > 0
87
- ? {
88
- completedPeriods,
89
- readyForResolution: true,
90
- periodScores,
91
- currentPeriod,
92
- }
93
- : null;
94
- };
95
-
96
- /**
97
- * Checks if a market can be resolved based on game and sport
98
- * @param gameId - The game/fixture ID
99
- * @param event - Event object from OpticOdds API
100
- * @returns boolean indicating if market can be resolved
101
- */
102
- export const canResolveMarketForGameIdAndSport = (
103
- gameId: string,
104
- event: OpticOddsEvent
105
- ): boolean => {
106
- const eventId = event.fixture?.id || event.id || '';
107
- if (eventId !== gameId) {
108
- return false;
109
- }
110
-
111
- const periodData = detectCompletedPeriods(event);
112
- return periodData !== null && periodData.readyForResolution;
113
- };
114
-
115
- /**
116
- * Convenience function to check resolution status via OpticOdds API event
117
- * @param event - Event object from OpticOdds API
118
- * @returns PeriodResolutionData if periods are complete, null otherwise
119
- */
120
- export const canResolveMarketViaOpticOddsApi = (
121
- event: OpticOddsEvent
122
- ): PeriodResolutionData | null => {
123
- return detectCompletedPeriods(event);
124
- };
125
-
126
- /**
127
- * Maps a numeric value to a SportPeriodType enum
128
- * @param sportTypeNum - Numeric representation of sport type (0 = halves, 1 = quarters, 2 = innings, 3 = period)
129
- * @returns SportPeriodType enum value
130
- * @throws Error if invalid number provided
131
- */
132
- export function mapNumberToSportPeriodType(sportTypeNum: number): SportPeriodType {
133
- switch (sportTypeNum) {
134
- case 0:
135
- return SportPeriodType.HALVES_BASED;
136
- case 1:
137
- return SportPeriodType.QUARTERS_BASED;
138
- case 2:
139
- return SportPeriodType.INNINGS_BASED;
140
- case 3:
141
- return SportPeriodType.PERIOD_BASED;
142
- default:
143
- throw new Error(`Invalid sport type number: ${sportTypeNum}. Must be 0 (halves), 1 (quarters), 2 (innings), or 3 (period).`);
144
- }
145
- }
146
-
147
- /**
148
- * Selects the appropriate period-to-typeId mapping based on sport type
149
- * @param sportType - Sport period structure type
150
- * @returns Period-to-typeId mapping for the specified sport type
151
- */
152
- function selectMappingForSportType(sportType: SportPeriodType): { [period: number]: number[] } {
153
- switch (sportType) {
154
- case SportPeriodType.HALVES_BASED:
155
- return HALVES_PERIOD_TYPE_ID_MAPPING;
156
- case SportPeriodType.QUARTERS_BASED:
157
- return QUARTERS_PERIOD_TYPE_ID_MAPPING;
158
- case SportPeriodType.INNINGS_BASED:
159
- return INNINGS_PERIOD_TYPE_ID_MAPPING;
160
- case SportPeriodType.PERIOD_BASED:
161
- return PERIOD_BASED_TYPE_ID_MAPPING;
162
- }
163
- }
164
-
165
- /**
166
- * Checks if a single market type can be resolved based on completed periods
167
- * @param event - Event object from OpticOdds API
168
- * @param typeId - Single market type ID to check
169
- * @param sportType - Sport period structure type - REQUIRED (enum or number: 0=halves, 1=quarters, 2=innings, 3=period)
170
- * @returns boolean indicating if that typeId can be resolved
171
- */
172
- export function canResolveMarketsForEvent(
173
- event: OpticOddsEvent,
174
- typeId: number,
175
- sportType: SportPeriodType | number
176
- ): boolean;
177
-
178
- /**
179
- * Checks which market types can be resolved from a batch based on completed periods
180
- * @param event - Event object from OpticOdds API
181
- * @param typeIds - Array of market type IDs to check
182
- * @param sportType - Sport period structure type - REQUIRED (enum or number: 0=halves, 1=quarters, 2=innings, 3=period)
183
- * @returns Array of typeIds that can be resolved
184
- */
185
- export function canResolveMarketsForEvent(
186
- event: OpticOddsEvent,
187
- typeIds: number[],
188
- sportType: SportPeriodType | number
189
- ): number[];
190
-
191
- /**
192
- * Implementation - checks if specific market type(s) can be resolved based on completed periods
193
- *
194
- * @example
195
- * // Check single typeId for NFL (quarters-based)
196
- * const canResolve = canResolveMarketsForEvent(event, 10021, SportPeriodType.QUARTERS_BASED);
197
- * // Or using number
198
- * const canResolve = canResolveMarketsForEvent(event, 10021, 1);
199
- *
200
- * // Check batch of typeIds for MLB (innings-based)
201
- * const resolvable = canResolveMarketsForEvent(event, [10021, 10051], SportPeriodType.INNINGS_BASED);
202
- * // Returns: [10021] if only period 1-4 complete, [10021, 10051] if period 5 complete
203
- *
204
- * // Check with period-based (no halves/secondary moneyline)
205
- * const canResolve = canResolveMarketsForEvent(event, 10021, SportPeriodType.PERIOD_BASED);
206
- * // Or using number
207
- * const canResolve = canResolveMarketsForEvent(event, 10021, 3);
208
- */
209
- export function canResolveMarketsForEvent(
210
- event: OpticOddsEvent,
211
- typeIdOrTypeIds: number | number[],
212
- sportType: SportPeriodType | number
213
- ): boolean | number[] {
214
- // Get completed periods
215
- const periodData = detectCompletedPeriods(event);
216
- if (!periodData) {
217
- return Array.isArray(typeIdOrTypeIds) ? [] : false;
218
- }
219
-
220
- // Check if game is fully completed
221
- const status = (event.fixture?.status || event.status || '').toLowerCase();
222
- const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
223
-
224
- // Convert number to SportPeriodType if needed
225
- const sportTypeEnum = typeof sportType === 'number' ? mapNumberToSportPeriodType(sportType) : sportType;
226
-
227
- // Select appropriate mapping based on sport type
228
- const mapping = selectMappingForSportType(sportTypeEnum);
229
-
230
- // Collect all resolvable typeIds based on completed periods
231
- const resolvableTypeIds = new Set<number>();
232
-
233
- for (const period of periodData.completedPeriods) {
234
- const typeIdsForPeriod = mapping[period] || [];
235
- typeIdsForPeriod.forEach((id) => resolvableTypeIds.add(id));
236
- }
237
-
238
- // Single typeId check
239
- if (typeof typeIdOrTypeIds === 'number') {
240
- // Full game typeIds can only be resolved when game is completed
241
- if (FULL_GAME_TYPE_IDS.includes(typeIdOrTypeIds)) {
242
- return isCompleted;
243
- }
244
- return resolvableTypeIds.has(typeIdOrTypeIds);
245
- }
246
-
247
- // Batch typeIds check
248
- return typeIdOrTypeIds.filter((id) => {
249
- // Full game typeIds can only be resolved when game is completed
250
- if (FULL_GAME_TYPE_IDS.includes(id)) {
251
- return isCompleted;
252
- }
253
- return resolvableTypeIds.has(id);
254
- });
255
- };
1
+ import {
2
+ PeriodResolutionData,
3
+ PeriodScores,
4
+ OpticOddsEvent,
5
+ SportPeriodType,
6
+ HALVES_PERIOD_TYPE_ID_MAPPING,
7
+ QUARTERS_PERIOD_TYPE_ID_MAPPING,
8
+ INNINGS_PERIOD_TYPE_ID_MAPPING,
9
+ PERIOD_BASED_TYPE_ID_MAPPING,
10
+ FULL_GAME_TYPE_IDS,
11
+ } from '../types/resolution';
12
+
13
+ /**
14
+ * Detects completed periods for a game based on OpticOdds API event data
15
+ * A period is considered complete if the next period (period + 1) exists in the scores
16
+ * @param event - Event object from OpticOdds API
17
+ * @returns PeriodResolutionData with completed periods, readiness status, and period scores
18
+ */
19
+ export const detectCompletedPeriods = (
20
+ event: OpticOddsEvent
21
+ ): PeriodResolutionData | null => {
22
+ const status = (event.fixture?.status || event.status || '').toLowerCase();
23
+ const isLive = event.fixture?.is_live ?? event.is_live ?? false;
24
+
25
+ // Extract period scores from the event
26
+ const homePeriods = event.scores?.home?.periods || {};
27
+ const awayPeriods = event.scores?.away?.periods || {};
28
+
29
+ const periodScores: PeriodScores = {};
30
+ const completedPeriods: number[] = [];
31
+
32
+ // Parse all available periods
33
+ const periodKeys = Object.keys(homePeriods)
34
+ .filter((key) => key.startsWith('period_'))
35
+ .map((key) => parseInt(key.replace('period_', '')))
36
+ .sort((a, b) => a - b);
37
+
38
+ if (periodKeys.length === 0) {
39
+ return null; // No period data available
40
+ }
41
+
42
+ // Get current period from in_play if available (only use if numeric)
43
+ const inPlayPeriod = event.in_play?.period;
44
+ const currentLivePeriod = inPlayPeriod && !isNaN(parseInt(inPlayPeriod)) ? parseInt(inPlayPeriod) : null;
45
+
46
+ // Check if game is in overtime/extra time (non-numeric period indicator)
47
+ const isInOvertime = inPlayPeriod &&
48
+ (inPlayPeriod.toLowerCase().includes('overtime') ||
49
+ inPlayPeriod.toLowerCase().includes('ot') ||
50
+ inPlayPeriod.toLowerCase().includes('extra'));
51
+
52
+ // For each period, check if it's complete
53
+ for (const periodNum of periodKeys) {
54
+ const key = `period_${periodNum}`;
55
+ const homeScore = homePeriods[key];
56
+ const awayScore = awayPeriods[key];
57
+
58
+ // Add this period's score
59
+ if (homeScore !== undefined && awayScore !== undefined && !isNaN(homeScore) && !isNaN(awayScore)) {
60
+ periodScores[`period${periodNum}`] = { home: homeScore, away: awayScore };
61
+
62
+ // Period is complete if:
63
+ // 1. Next period exists in periods data, OR
64
+ // 2. Game is completed, OR
65
+ // 3. Game is live with numeric period AND current live period is greater than this period, OR
66
+ // 4. Game is in overtime (all regulation periods are complete)
67
+ const nextPeriodKey = `period_${periodNum + 1}`;
68
+ const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
69
+ const hasNextPeriod = homePeriods[nextPeriodKey] !== undefined && awayPeriods[nextPeriodKey] !== undefined;
70
+ const isCompletedInLiveGame = isLive && currentLivePeriod !== null && currentLivePeriod > periodNum;
71
+
72
+ if (hasNextPeriod || isCompleted || isCompletedInLiveGame || isInOvertime) {
73
+ completedPeriods.push(periodNum);
74
+ }
75
+ }
76
+ }
77
+
78
+ // Determine current period
79
+ // If we have a numeric in_play period and it's greater than highest period in data, use it
80
+ // Otherwise use highest period number from the data
81
+ const highestPeriodInData = periodKeys.length > 0 ? Math.max(...periodKeys) : undefined;
82
+ const currentPeriod = currentLivePeriod && currentLivePeriod > (highestPeriodInData || 0)
83
+ ? currentLivePeriod
84
+ : highestPeriodInData;
85
+
86
+ return completedPeriods.length > 0
87
+ ? {
88
+ completedPeriods,
89
+ readyForResolution: true,
90
+ periodScores,
91
+ currentPeriod,
92
+ }
93
+ : null;
94
+ };
95
+
96
+ /**
97
+ * Checks if a market can be resolved based on game and sport
98
+ * @param gameId - The game/fixture ID
99
+ * @param event - Event object from OpticOdds API
100
+ * @returns boolean indicating if market can be resolved
101
+ */
102
+ export const canResolveMarketForGameIdAndSport = (
103
+ gameId: string,
104
+ event: OpticOddsEvent
105
+ ): boolean => {
106
+ const eventId = event.fixture?.id || event.id || '';
107
+ if (eventId !== gameId) {
108
+ return false;
109
+ }
110
+
111
+ const periodData = detectCompletedPeriods(event);
112
+ return periodData !== null && periodData.readyForResolution;
113
+ };
114
+
115
+ /**
116
+ * Convenience function to check resolution status via OpticOdds API event
117
+ * @param event - Event object from OpticOdds API
118
+ * @returns PeriodResolutionData if periods are complete, null otherwise
119
+ */
120
+ export const canResolveMarketViaOpticOddsApi = (
121
+ event: OpticOddsEvent
122
+ ): PeriodResolutionData | null => {
123
+ return detectCompletedPeriods(event);
124
+ };
125
+
126
+ /**
127
+ * Maps a numeric value to a SportPeriodType enum
128
+ * @param sportTypeNum - Numeric representation of sport type (0 = halves, 1 = quarters, 2 = innings, 3 = period)
129
+ * @returns SportPeriodType enum value
130
+ * @throws Error if invalid number provided
131
+ */
132
+ export function mapNumberToSportPeriodType(sportTypeNum: number): SportPeriodType {
133
+ switch (sportTypeNum) {
134
+ case 0:
135
+ return SportPeriodType.HALVES_BASED;
136
+ case 1:
137
+ return SportPeriodType.QUARTERS_BASED;
138
+ case 2:
139
+ return SportPeriodType.INNINGS_BASED;
140
+ case 3:
141
+ return SportPeriodType.PERIOD_BASED;
142
+ default:
143
+ throw new Error(`Invalid sport type number: ${sportTypeNum}. Must be 0 (halves), 1 (quarters), 2 (innings), or 3 (period).`);
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Selects the appropriate period-to-typeId mapping based on sport type
149
+ * @param sportType - Sport period structure type
150
+ * @returns Period-to-typeId mapping for the specified sport type
151
+ */
152
+ function selectMappingForSportType(sportType: SportPeriodType): { [period: number]: number[] } {
153
+ switch (sportType) {
154
+ case SportPeriodType.HALVES_BASED:
155
+ return HALVES_PERIOD_TYPE_ID_MAPPING;
156
+ case SportPeriodType.QUARTERS_BASED:
157
+ return QUARTERS_PERIOD_TYPE_ID_MAPPING;
158
+ case SportPeriodType.INNINGS_BASED:
159
+ return INNINGS_PERIOD_TYPE_ID_MAPPING;
160
+ case SportPeriodType.PERIOD_BASED:
161
+ return PERIOD_BASED_TYPE_ID_MAPPING;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Implementation - checks if specific market type(s) can be resolved based on completed periods
167
+ *
168
+ * @example
169
+ * // Check single typeId for NFL (quarters-based)
170
+ * const canResolve = canResolveMarketsForEvent(event, 10021, SportPeriodType.QUARTERS_BASED);
171
+ * // Or using number
172
+ * const canResolve = canResolveMarketsForEvent(event, 10021, 1);
173
+ *
174
+ * // Check batch of typeIds for MLB (innings-based)
175
+ * const resolvable = canResolveMarketsForEvent(event, [10021, 10051], SportPeriodType.INNINGS_BASED);
176
+ * // Returns: [10021] if only period 1-4 complete, [10021, 10051] if period 5 complete
177
+ *
178
+ * // Check with period-based (no halves/secondary moneyline)
179
+ * const canResolve = canResolveMarketsForEvent(event, 10021, SportPeriodType.PERIOD_BASED);
180
+ * // Or using number
181
+ * const canResolve = canResolveMarketsForEvent(event, 10021, 3);
182
+ */
183
+ export function canResolveMarketsForEvent(
184
+ event: OpticOddsEvent,
185
+ typeIdOrTypeIds: number | number[],
186
+ sportType: SportPeriodType | number
187
+ ): boolean | number[] {
188
+ // Get completed periods
189
+ const periodData = detectCompletedPeriods(event);
190
+ if (!periodData) {
191
+ return Array.isArray(typeIdOrTypeIds) ? [] : false;
192
+ }
193
+
194
+ // Check if game is fully completed
195
+ const status = (event.fixture?.status || event.status || '').toLowerCase();
196
+ const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
197
+
198
+ // Convert number to SportPeriodType if needed
199
+ const sportTypeEnum = typeof sportType === 'number' ? mapNumberToSportPeriodType(sportType) : sportType;
200
+
201
+ // Select appropriate mapping based on sport type
202
+ const mapping = selectMappingForSportType(sportTypeEnum);
203
+
204
+ // Collect all resolvable typeIds based on completed periods
205
+ const resolvableTypeIds = new Set<number>();
206
+
207
+ for (const period of periodData.completedPeriods) {
208
+ const typeIdsForPeriod = mapping[period] || [];
209
+ typeIdsForPeriod.forEach((id) => resolvableTypeIds.add(id));
210
+ }
211
+
212
+ // Single typeId check
213
+ if (typeof typeIdOrTypeIds === 'number') {
214
+ // Full game typeIds can only be resolved when game is completed
215
+ if (FULL_GAME_TYPE_IDS.includes(typeIdOrTypeIds)) {
216
+ return isCompleted;
217
+ }
218
+ return resolvableTypeIds.has(typeIdOrTypeIds);
219
+ }
220
+
221
+ // Batch typeIds check
222
+ return typeIdOrTypeIds.filter((id) => {
223
+ // Full game typeIds can only be resolved when game is completed
224
+ if (FULL_GAME_TYPE_IDS.includes(id)) {
225
+ return isCompleted;
226
+ }
227
+ return resolvableTypeIds.has(id);
228
+ });
229
+ };