overtime-live-trading-utils 2.1.32 → 2.1.33

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 +993 -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 +918 -918
  33. package/src/utils/opticOdds.ts +71 -71
  34. package/src/utils/resolution.ts +298 -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,291 +1,298 @@
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
- * Maps a numeric value to a SportPeriodType enum
97
- * @param sportTypeNum - Numeric representation of sport type (0 = halves, 1 = quarters, 2 = innings, 3 = period)
98
- * @returns SportPeriodType enum value
99
- * @throws Error if invalid number provided
100
- */
101
- export function mapNumberToSportPeriodType(sportTypeNum: number): SportPeriodType {
102
- switch (sportTypeNum) {
103
- case 0:
104
- return SportPeriodType.HALVES_BASED;
105
- case 1:
106
- return SportPeriodType.QUARTERS_BASED;
107
- case 2:
108
- return SportPeriodType.INNINGS_BASED;
109
- case 3:
110
- return SportPeriodType.PERIOD_BASED;
111
- default:
112
- throw new Error(`Invalid sport type number: ${sportTypeNum}. Must be 0 (halves), 1 (quarters), 2 (innings), or 3 (period).`);
113
- }
114
- }
115
-
116
- /**
117
- * Selects the appropriate period-to-typeId mapping based on sport type
118
- * @param sportType - Sport period structure type
119
- * @returns Period-to-typeId mapping for the specified sport type
120
- */
121
- function selectMappingForSportType(sportType: SportPeriodType): { [period: number]: number[] } {
122
- switch (sportType) {
123
- case SportPeriodType.HALVES_BASED:
124
- return HALVES_PERIOD_TYPE_ID_MAPPING;
125
- case SportPeriodType.QUARTERS_BASED:
126
- return QUARTERS_PERIOD_TYPE_ID_MAPPING;
127
- case SportPeriodType.INNINGS_BASED:
128
- return INNINGS_PERIOD_TYPE_ID_MAPPING;
129
- case SportPeriodType.PERIOD_BASED:
130
- return PERIOD_BASED_TYPE_ID_MAPPING;
131
- }
132
- }
133
-
134
- /**
135
- * Checks if a specific market type can be resolved based on completed periods (Enum version)
136
- *
137
- * @example
138
- * // Check single typeId for NFL (quarters-based)
139
- * const canResolve = canResolveMarketsForEvent(event, 10021, SportPeriodType.QUARTERS_BASED);
140
- *
141
- * // Check with period-based (no halves/secondary moneyline)
142
- * const canResolve = canResolveMarketsForEvent(event, 10021, SportPeriodType.PERIOD_BASED);
143
- *
144
- * @param event - OpticOdds event object containing fixture data
145
- * @param typeId - The market type identifier
146
- * @param sportType - The sport period type enum
147
- * @returns true if the market can be resolved based on completed periods
148
- */
149
- export function canResolveMarketsForEvent(
150
- event: OpticOddsEvent,
151
- typeId: number,
152
- sportType: SportPeriodType
153
- ): boolean {
154
- const periodData = detectCompletedPeriods(event);
155
- if (!periodData) return false;
156
-
157
- const status = (event.fixture?.status || event.status || '').toLowerCase();
158
- const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
159
-
160
- // Full game typeIds can only be resolved when game is completed
161
- if (FULL_GAME_TYPE_IDS.includes(typeId)) {
162
- return isCompleted;
163
- }
164
-
165
- // Select appropriate mapping based on sport type
166
- const mapping = selectMappingForSportType(sportType);
167
-
168
- const resolvableTypeIds = new Set<number>();
169
- for (const period of periodData.completedPeriods) {
170
- const typeIdsForPeriod = mapping[period] || [];
171
- typeIdsForPeriod.forEach((id) => resolvableTypeIds.add(id));
172
- }
173
-
174
- return resolvableTypeIds.has(typeId);
175
- }
176
-
177
- /**
178
- * Checks if a specific market type can be resolved based on completed periods (Number version)
179
- *
180
- * @example
181
- * // Check single typeId for NFL (quarters-based)
182
- * const canResolve = canResolveMarketsForEventNumber(event, 10021, 1);
183
- *
184
- * @param event - OpticOdds event object containing fixture data
185
- * @param typeId - The market type identifier
186
- * @param sportTypeNumber - Numeric value representing the sport period type
187
- * @returns true if the market can be resolved based on completed periods
188
- */
189
- export function canResolveMarketsForEventNumber(
190
- event: OpticOddsEvent,
191
- typeId: number,
192
- sportTypeNumber: number
193
- ): boolean {
194
- const sportTypeEnum = mapNumberToSportPeriodType(sportTypeNumber);
195
- return canResolveMarketsForEvent(event, typeId, sportTypeEnum);
196
- }
197
-
198
-
199
- /**
200
- * Checks if multiple market types can be resolved, returning a boolean for each
201
- *
202
- * @example
203
- * // Check multiple typeIds for MLB (innings-based)
204
- * const canResolve = canResolveMultipleTypeIdsForEvent(event, [10021, 10051], SportPeriodType.INNINGS_BASED);
205
- * // Returns: [true, false] if only 10021 can be resolved
206
- */
207
- export function canResolveMultipleTypeIdsForEvent(
208
- event: OpticOddsEvent,
209
- typeIds: number[],
210
- sportType: SportPeriodType | number
211
- ): boolean[] {
212
- // Get completed periods
213
- const periodData = detectCompletedPeriods(event);
214
- if (!periodData) {
215
- return typeIds.map(() => false);
216
- }
217
-
218
- // Check if game is fully completed
219
- const status = (event.fixture?.status || event.status || '').toLowerCase();
220
- const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
221
-
222
- // Convert number to SportPeriodType if needed
223
- const sportTypeEnum = typeof sportType === 'number' ? mapNumberToSportPeriodType(sportType) : sportType;
224
-
225
- // Select appropriate mapping based on sport type
226
- const mapping = selectMappingForSportType(sportTypeEnum);
227
-
228
- // Collect all resolvable typeIds based on completed periods
229
- const resolvableTypeIds = new Set<number>();
230
-
231
- for (const period of periodData.completedPeriods) {
232
- const typeIdsForPeriod = mapping[period] || [];
233
- typeIdsForPeriod.forEach((id) => resolvableTypeIds.add(id));
234
- }
235
-
236
- // Map each typeId to a boolean
237
- return typeIds.map((id) => {
238
- // Full game typeIds can only be resolved when game is completed
239
- if (FULL_GAME_TYPE_IDS.includes(id)) {
240
- return isCompleted;
241
- }
242
- return resolvableTypeIds.has(id);
243
- });
244
- }
245
-
246
- /**
247
- * Filters a list of market types to only those that can be resolved
248
- *
249
- * @example
250
- * // Filter typeIds for MLB (innings-based)
251
- * const resolvable = filterMarketsThatCanBeResolved(event, [10021, 10051, 10061], SportPeriodType.INNINGS_BASED);
252
- * // Returns: [10021] if only period 1-4 complete, [10021, 10051] if period 5 complete
253
- */
254
- export function filterMarketsThatCanBeResolved(
255
- event: OpticOddsEvent,
256
- typeIds: number[],
257
- sportType: SportPeriodType | number
258
- ): number[] {
259
- // Get completed periods
260
- const periodData = detectCompletedPeriods(event);
261
- if (!periodData) {
262
- return [];
263
- }
264
-
265
- // Check if game is fully completed
266
- const status = (event.fixture?.status || event.status || '').toLowerCase();
267
- const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
268
-
269
- // Convert number to SportPeriodType if needed
270
- const sportTypeEnum = typeof sportType === 'number' ? mapNumberToSportPeriodType(sportType) : sportType;
271
-
272
- // Select appropriate mapping based on sport type
273
- const mapping = selectMappingForSportType(sportTypeEnum);
274
-
275
- // Collect all resolvable typeIds based on completed periods
276
- const resolvableTypeIds = new Set<number>();
277
-
278
- for (const period of periodData.completedPeriods) {
279
- const typeIdsForPeriod = mapping[period] || [];
280
- typeIdsForPeriod.forEach((id) => resolvableTypeIds.add(id));
281
- }
282
-
283
- // Filter typeIds to only those that can be resolved
284
- return typeIds.filter((id) => {
285
- // Full game typeIds can only be resolved when game is completed
286
- if (FULL_GAME_TYPE_IDS.includes(id)) {
287
- return isCompleted;
288
- }
289
- return resolvableTypeIds.has(id);
290
- });
291
- }
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
+ // Check if game is at halftime (period 1 is complete)
53
+ const isAtHalftime = (status === 'half' || status === 'halftime') ||
54
+ (inPlayPeriod && inPlayPeriod.toLowerCase() === 'half');
55
+
56
+ // For each period, check if it's complete
57
+ for (const periodNum of periodKeys) {
58
+ const key = `period_${periodNum}`;
59
+ const homeScore = homePeriods[key];
60
+ const awayScore = awayPeriods[key];
61
+
62
+ // Add this period's score
63
+ if (homeScore !== undefined && awayScore !== undefined && !isNaN(homeScore) && !isNaN(awayScore)) {
64
+ periodScores[`period${periodNum}`] = { home: homeScore, away: awayScore };
65
+
66
+ // Period is complete if:
67
+ // 1. Game is completed (status = completed/finished), OR
68
+ // 2. Game is live AND in_play.period is GREATER than this period, OR
69
+ // 3. Game is in overtime (all regulation periods are complete), OR
70
+ // 4. Game is at halftime AND this is period 1
71
+ //
72
+ // Note: We do NOT check if next period exists in data, as OpticOdds may include
73
+ // future periods with scores (including zeros). Only in_play.period is
74
+ // the source of truth for live games.
75
+ const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
76
+ const isCompletedInLiveGame = isLive && currentLivePeriod !== null && currentLivePeriod > periodNum;
77
+ const isFirstPeriodAtHalftime = isAtHalftime && periodNum === 1;
78
+
79
+ if (isCompleted || isCompletedInLiveGame || isInOvertime || isFirstPeriodAtHalftime) {
80
+ completedPeriods.push(periodNum);
81
+ }
82
+ }
83
+ }
84
+
85
+ // Determine current period
86
+ // For live games with numeric in_play period, use that as the authoritative current period
87
+ // Otherwise use highest period number from the data
88
+ const highestPeriodInData = periodKeys.length > 0 ? Math.max(...periodKeys) : undefined;
89
+ const currentPeriod = isLive && currentLivePeriod !== null
90
+ ? currentLivePeriod
91
+ : highestPeriodInData;
92
+
93
+ return completedPeriods.length > 0
94
+ ? {
95
+ completedPeriods,
96
+ readyForResolution: true,
97
+ periodScores,
98
+ currentPeriod,
99
+ }
100
+ : null;
101
+ };
102
+ /**
103
+ * Maps a numeric value to a SportPeriodType enum
104
+ * @param sportTypeNum - Numeric representation of sport type (0 = halves, 1 = quarters, 2 = innings, 3 = period)
105
+ * @returns SportPeriodType enum value
106
+ * @throws Error if invalid number provided
107
+ */
108
+ export function mapNumberToSportPeriodType(sportTypeNum: number): SportPeriodType {
109
+ switch (sportTypeNum) {
110
+ case 0:
111
+ return SportPeriodType.HALVES_BASED;
112
+ case 1:
113
+ return SportPeriodType.QUARTERS_BASED;
114
+ case 2:
115
+ return SportPeriodType.INNINGS_BASED;
116
+ case 3:
117
+ return SportPeriodType.PERIOD_BASED;
118
+ default:
119
+ throw new Error(`Invalid sport type number: ${sportTypeNum}. Must be 0 (halves), 1 (quarters), 2 (innings), or 3 (period).`);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Selects the appropriate period-to-typeId mapping based on sport type
125
+ * @param sportType - Sport period structure type
126
+ * @returns Period-to-typeId mapping for the specified sport type
127
+ */
128
+ function selectMappingForSportType(sportType: SportPeriodType): { [period: number]: number[] } {
129
+ switch (sportType) {
130
+ case SportPeriodType.HALVES_BASED:
131
+ return HALVES_PERIOD_TYPE_ID_MAPPING;
132
+ case SportPeriodType.QUARTERS_BASED:
133
+ return QUARTERS_PERIOD_TYPE_ID_MAPPING;
134
+ case SportPeriodType.INNINGS_BASED:
135
+ return INNINGS_PERIOD_TYPE_ID_MAPPING;
136
+ case SportPeriodType.PERIOD_BASED:
137
+ return PERIOD_BASED_TYPE_ID_MAPPING;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Checks if a specific market type can be resolved based on completed periods (Enum version)
143
+ *
144
+ * @example
145
+ * // Check single typeId for NFL (quarters-based)
146
+ * const canResolve = canResolveMarketsForEvent(event, 10021, SportPeriodType.QUARTERS_BASED);
147
+ *
148
+ * // Check with period-based (no halves/secondary moneyline)
149
+ * const canResolve = canResolveMarketsForEvent(event, 10021, SportPeriodType.PERIOD_BASED);
150
+ *
151
+ * @param event - OpticOdds event object containing fixture data
152
+ * @param typeId - The market type identifier
153
+ * @param sportType - The sport period type enum
154
+ * @returns true if the market can be resolved based on completed periods
155
+ */
156
+ export function canResolveMarketsForEvent(
157
+ event: OpticOddsEvent,
158
+ typeId: number,
159
+ sportType: SportPeriodType
160
+ ): boolean {
161
+ const periodData = detectCompletedPeriods(event);
162
+ if (!periodData) return false;
163
+
164
+ const status = (event.fixture?.status || event.status || '').toLowerCase();
165
+ const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
166
+
167
+ // Full game typeIds can only be resolved when game is completed
168
+ if (FULL_GAME_TYPE_IDS.includes(typeId)) {
169
+ return isCompleted;
170
+ }
171
+
172
+ // Select appropriate mapping based on sport type
173
+ const mapping = selectMappingForSportType(sportType);
174
+
175
+ const resolvableTypeIds = new Set<number>();
176
+ for (const period of periodData.completedPeriods) {
177
+ const typeIdsForPeriod = mapping[period] || [];
178
+ typeIdsForPeriod.forEach((id) => resolvableTypeIds.add(id));
179
+ }
180
+
181
+ return resolvableTypeIds.has(typeId);
182
+ }
183
+
184
+ /**
185
+ * Checks if a specific market type can be resolved based on completed periods (Number version)
186
+ *
187
+ * @example
188
+ * // Check single typeId for NFL (quarters-based)
189
+ * const canResolve = canResolveMarketsForEventNumber(event, 10021, 1);
190
+ *
191
+ * @param event - OpticOdds event object containing fixture data
192
+ * @param typeId - The market type identifier
193
+ * @param sportTypeNumber - Numeric value representing the sport period type
194
+ * @returns true if the market can be resolved based on completed periods
195
+ */
196
+ export function canResolveMarketsForEventNumber(
197
+ event: OpticOddsEvent,
198
+ typeId: number,
199
+ sportTypeNumber: number
200
+ ): boolean {
201
+ const sportTypeEnum = mapNumberToSportPeriodType(sportTypeNumber);
202
+ return canResolveMarketsForEvent(event, typeId, sportTypeEnum);
203
+ }
204
+
205
+
206
+ /**
207
+ * Checks if multiple market types can be resolved, returning a boolean for each
208
+ *
209
+ * @example
210
+ * // Check multiple typeIds for MLB (innings-based)
211
+ * const canResolve = canResolveMultipleTypeIdsForEvent(event, [10021, 10051], SportPeriodType.INNINGS_BASED);
212
+ * // Returns: [true, false] if only 10021 can be resolved
213
+ */
214
+ export function canResolveMultipleTypeIdsForEvent(
215
+ event: OpticOddsEvent,
216
+ typeIds: number[],
217
+ sportType: SportPeriodType | number
218
+ ): boolean[] {
219
+ // Get completed periods
220
+ const periodData = detectCompletedPeriods(event);
221
+ if (!periodData) {
222
+ return typeIds.map(() => false);
223
+ }
224
+
225
+ // Check if game is fully completed
226
+ const status = (event.fixture?.status || event.status || '').toLowerCase();
227
+ const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
228
+
229
+ // Convert number to SportPeriodType if needed
230
+ const sportTypeEnum = typeof sportType === 'number' ? mapNumberToSportPeriodType(sportType) : sportType;
231
+
232
+ // Select appropriate mapping based on sport type
233
+ const mapping = selectMappingForSportType(sportTypeEnum);
234
+
235
+ // Collect all resolvable typeIds based on completed periods
236
+ const resolvableTypeIds = new Set<number>();
237
+
238
+ for (const period of periodData.completedPeriods) {
239
+ const typeIdsForPeriod = mapping[period] || [];
240
+ typeIdsForPeriod.forEach((id) => resolvableTypeIds.add(id));
241
+ }
242
+
243
+ // Map each typeId to a boolean
244
+ return typeIds.map((id) => {
245
+ // Full game typeIds can only be resolved when game is completed
246
+ if (FULL_GAME_TYPE_IDS.includes(id)) {
247
+ return isCompleted;
248
+ }
249
+ return resolvableTypeIds.has(id);
250
+ });
251
+ }
252
+
253
+ /**
254
+ * Filters a list of market types to only those that can be resolved
255
+ *
256
+ * @example
257
+ * // Filter typeIds for MLB (innings-based)
258
+ * const resolvable = filterMarketsThatCanBeResolved(event, [10021, 10051, 10061], SportPeriodType.INNINGS_BASED);
259
+ * // Returns: [10021] if only period 1-4 complete, [10021, 10051] if period 5 complete
260
+ */
261
+ export function filterMarketsThatCanBeResolved(
262
+ event: OpticOddsEvent,
263
+ typeIds: number[],
264
+ sportType: SportPeriodType | number
265
+ ): number[] {
266
+ // Get completed periods
267
+ const periodData = detectCompletedPeriods(event);
268
+ if (!periodData) {
269
+ return [];
270
+ }
271
+
272
+ // Check if game is fully completed
273
+ const status = (event.fixture?.status || event.status || '').toLowerCase();
274
+ const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
275
+
276
+ // Convert number to SportPeriodType if needed
277
+ const sportTypeEnum = typeof sportType === 'number' ? mapNumberToSportPeriodType(sportType) : sportType;
278
+
279
+ // Select appropriate mapping based on sport type
280
+ const mapping = selectMappingForSportType(sportTypeEnum);
281
+
282
+ // Collect all resolvable typeIds based on completed periods
283
+ const resolvableTypeIds = new Set<number>();
284
+
285
+ for (const period of periodData.completedPeriods) {
286
+ const typeIdsForPeriod = mapping[period] || [];
287
+ typeIdsForPeriod.forEach((id) => resolvableTypeIds.add(id));
288
+ }
289
+
290
+ // Filter typeIds to only those that can be resolved
291
+ return typeIds.filter((id) => {
292
+ // Full game typeIds can only be resolved when game is completed
293
+ if (FULL_GAME_TYPE_IDS.includes(id)) {
294
+ return isCompleted;
295
+ }
296
+ return resolvableTypeIds.has(id);
297
+ });
298
+ }