overtime-live-trading-utils 2.1.43 → 2.1.44

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 (39) 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 +662 -662
  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 +1489 -1489
  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 +656 -656
  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 +918 -918
  31. package/src/utils/opticOdds.ts +71 -71
  32. package/src/utils/resolution.ts +319 -319
  33. package/src/utils/sportPeriodMapping.ts +36 -36
  34. package/src/utils/sports.ts +51 -51
  35. package/src/utils/spread.ts +97 -97
  36. package/tsconfig.json +16 -16
  37. package/webpack.config.js +24 -24
  38. package/CLAUDE.md +0 -84
  39. package/resolution_live_markets.md +0 -356
@@ -1,319 +1,319 @@
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
- import { getSportPeriodTypeFromEvent } from './sportPeriodMapping';
13
-
14
- /**
15
- * Detects completed periods for a game based on OpticOdds API event data
16
- * A period is considered complete if the next period (period + 1) exists in the scores
17
- * @param event - Event object from OpticOdds API
18
- * @returns PeriodResolutionData with completed periods, readiness status, and period scores
19
- */
20
- export const detectCompletedPeriods = (event: OpticOddsEvent): PeriodResolutionData | null => {
21
- const status = (event.fixture?.status || event.status || '').toLowerCase();
22
- const isLive = event.fixture?.is_live ?? event.is_live ?? false;
23
-
24
- // Determine sport period type for sport-aware logic
25
- // Defaults to PERIOD_BASED (no halftime processing) for unknown sports
26
- const sportType = getSportPeriodTypeFromEvent(event);
27
-
28
- // Extract period scores from the event
29
- const homePeriods = event.scores?.home?.periods || {};
30
- const awayPeriods = event.scores?.away?.periods || {};
31
-
32
- const periodScores: PeriodScores = {};
33
- const completedPeriods: number[] = [];
34
-
35
- // Parse all available periods
36
- const periodKeys = Object.keys(homePeriods)
37
- .filter((key) => key.startsWith('period_'))
38
- .map((key) => parseInt(key.replace('period_', '')))
39
- .sort((a, b) => a - b);
40
-
41
- if (periodKeys.length === 0) {
42
- return null; // No period data available
43
- }
44
-
45
- // Get current period from in_play if available (only use if numeric)
46
- const inPlayPeriod = event.in_play?.period;
47
- const currentLivePeriod = inPlayPeriod && !isNaN(parseInt(inPlayPeriod)) ? parseInt(inPlayPeriod) : null;
48
-
49
- // Check if game is in overtime/extra time (non-numeric period indicator)
50
- const isInOvertime =
51
- inPlayPeriod &&
52
- (inPlayPeriod.toLowerCase().includes('overtime') ||
53
- inPlayPeriod.toLowerCase().includes('ot') ||
54
- inPlayPeriod.toLowerCase().includes('extra'));
55
-
56
- // Check if game is at halftime
57
- const isAtHalftime =
58
- status === 'half' || status === 'halftime' || (inPlayPeriod && inPlayPeriod.toLowerCase() === 'half');
59
-
60
- // For each period, check if it's complete
61
- for (const periodNum of periodKeys) {
62
- const key = `period_${periodNum}`;
63
- const homeScore = homePeriods[key];
64
- const awayScore = awayPeriods[key];
65
-
66
- // Add this period's score
67
- if (homeScore !== undefined && awayScore !== undefined && !isNaN(homeScore) && !isNaN(awayScore)) {
68
- periodScores[`period${periodNum}`] = { home: homeScore, away: awayScore };
69
-
70
- // Period is complete if:
71
- // 1. Game is completed (status = completed/finished), OR
72
- // 2. Game is live AND in_play.period is GREATER than this period, OR
73
- // 3. Game is in overtime (all regulation periods are complete), OR
74
- // 4. Game is at halftime AND this period is complete based on sport type
75
- //
76
- // Note: We do NOT check if next period exists in data, as OpticOdds may include
77
- // future periods with scores (including zeros). Only in_play.period is
78
- // the source of truth for live games.
79
- const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
80
- const isCompletedInLiveGame = isLive && currentLivePeriod !== null && currentLivePeriod > periodNum;
81
-
82
- // Sport-aware halftime logic
83
- let isCompletedAtHalftime = false;
84
- if (isAtHalftime) {
85
- switch (sportType) {
86
- case SportPeriodType.HALVES_BASED:
87
- // Soccer, etc: halftime = end of period 1 (first half complete)
88
- isCompletedAtHalftime = periodNum === 1;
89
- break;
90
- case SportPeriodType.QUARTERS_BASED:
91
- // Basketball, Football: halftime = end of period 2 (first two quarters complete)
92
- isCompletedAtHalftime = periodNum === 1 || periodNum === 2;
93
- break;
94
- case SportPeriodType.INNINGS_BASED:
95
- // Baseball: halftime = middle of game (first 5 innings complete)
96
- isCompletedAtHalftime = periodNum >= 1 && periodNum <= 5;
97
- break;
98
- case SportPeriodType.PERIOD_BASED:
99
- // Hockey: no traditional halftime concept
100
- isCompletedAtHalftime = false;
101
- break;
102
- }
103
- }
104
-
105
- if (isCompleted || isCompletedInLiveGame || isInOvertime || isCompletedAtHalftime) {
106
- completedPeriods.push(periodNum);
107
- }
108
- }
109
- }
110
-
111
- // Determine current period
112
- // For live games with numeric in_play period, use that as the authoritative current period
113
- // Otherwise use highest period number from the data
114
- const highestPeriodInData = periodKeys.length > 0 ? Math.max(...periodKeys) : undefined;
115
- const currentPeriod = isLive && currentLivePeriod !== null ? currentLivePeriod : highestPeriodInData;
116
-
117
- return completedPeriods.length > 0
118
- ? {
119
- completedPeriods,
120
- readyForResolution: true,
121
- periodScores,
122
- currentPeriod,
123
- }
124
- : null;
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(
144
- `Invalid sport type number: ${sportTypeNum}. Must be 0 (halves), 1 (quarters), 2 (innings), or 3 (period).`
145
- );
146
- }
147
- }
148
-
149
- /**
150
- * Selects the appropriate period-to-typeId mapping based on sport type
151
- * @param sportType - Sport period structure type
152
- * @returns Period-to-typeId mapping for the specified sport type
153
- */
154
- function selectMappingForSportType(sportType: SportPeriodType): { [period: number]: number[] } {
155
- switch (sportType) {
156
- case SportPeriodType.HALVES_BASED:
157
- return HALVES_PERIOD_TYPE_ID_MAPPING;
158
- case SportPeriodType.QUARTERS_BASED:
159
- return QUARTERS_PERIOD_TYPE_ID_MAPPING;
160
- case SportPeriodType.INNINGS_BASED:
161
- return INNINGS_PERIOD_TYPE_ID_MAPPING;
162
- case SportPeriodType.PERIOD_BASED:
163
- return PERIOD_BASED_TYPE_ID_MAPPING;
164
- }
165
- }
166
-
167
- /**
168
- * Checks if a specific market type can be resolved based on completed periods (Enum version)
169
- *
170
- * @example
171
- * // Check single typeId for NFL (quarters-based)
172
- * const canResolve = canResolveMarketsForEvent(event, 10021, SportPeriodType.QUARTERS_BASED);
173
- *
174
- * // Check with period-based (no halves/secondary moneyline)
175
- * const canResolve = canResolveMarketsForEvent(event, 10021, SportPeriodType.PERIOD_BASED);
176
- *
177
- * @param event - OpticOdds event object containing fixture data
178
- * @param typeId - The market type identifier
179
- * @param sportType - The sport period type enum
180
- * @returns true if the market can be resolved based on completed periods
181
- */
182
- export function canResolveMarketsForEvent(event: OpticOddsEvent, typeId: number, sportType: SportPeriodType): boolean {
183
- const periodData = detectCompletedPeriods(event);
184
- if (!periodData) return false;
185
-
186
- const status = (event.fixture?.status || event.status || '').toLowerCase();
187
- const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
188
-
189
- // Full game typeIds can only be resolved when game is completed
190
- if (FULL_GAME_TYPE_IDS.includes(typeId)) {
191
- return isCompleted;
192
- }
193
-
194
- // Select appropriate mapping based on sport type
195
- const mapping = selectMappingForSportType(sportType);
196
-
197
- const resolvableTypeIds = new Set<number>();
198
- for (const period of periodData.completedPeriods) {
199
- const typeIdsForPeriod = mapping[period] || [];
200
- typeIdsForPeriod.forEach((id) => resolvableTypeIds.add(id));
201
- }
202
-
203
- return resolvableTypeIds.has(typeId);
204
- }
205
-
206
- /**
207
- * Checks if a specific market type can be resolved based on completed periods (Number version)
208
- *
209
- * @example
210
- * // Check single typeId for NFL (quarters-based)
211
- * const canResolve = canResolveMarketsForEventNumber(event, 10021, 1);
212
- *
213
- * @param event - OpticOdds event object containing fixture data
214
- * @param typeId - The market type identifier
215
- * @param sportTypeNumber - Numeric value representing the sport period type
216
- * @returns true if the market can be resolved based on completed periods
217
- */
218
- export function canResolveMarketsForEventNumber(
219
- event: OpticOddsEvent,
220
- typeId: number,
221
- sportTypeNumber: number
222
- ): boolean {
223
- const sportTypeEnum = mapNumberToSportPeriodType(sportTypeNumber);
224
- return canResolveMarketsForEvent(event, typeId, sportTypeEnum);
225
- }
226
-
227
- /**
228
- * Checks if multiple market types can be resolved, returning a boolean for each
229
- *
230
- * @example
231
- * // Check multiple typeIds for MLB (innings-based)
232
- * const canResolve = canResolveMultipleTypeIdsForEvent(event, [10021, 10051], SportPeriodType.INNINGS_BASED);
233
- * // Returns: [true, false] if only 10021 can be resolved
234
- */
235
- export function canResolveMultipleTypeIdsForEvent(
236
- event: OpticOddsEvent,
237
- typeIds: number[],
238
- sportType: SportPeriodType | number
239
- ): boolean[] {
240
- // Get completed periods
241
- const periodData = detectCompletedPeriods(event);
242
- if (!periodData) {
243
- return typeIds.map(() => false);
244
- }
245
-
246
- // Check if game is fully completed
247
- const status = (event.fixture?.status || event.status || '').toLowerCase();
248
- const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
249
-
250
- // Convert number to SportPeriodType if needed
251
- const sportTypeEnum = typeof sportType === 'number' ? mapNumberToSportPeriodType(sportType) : sportType;
252
-
253
- // Select appropriate mapping based on sport type
254
- const mapping = selectMappingForSportType(sportTypeEnum);
255
-
256
- // Collect all resolvable typeIds based on completed periods
257
- const resolvableTypeIds = new Set<number>();
258
-
259
- for (const period of periodData.completedPeriods) {
260
- const typeIdsForPeriod = mapping[period] || [];
261
- typeIdsForPeriod.forEach((id) => resolvableTypeIds.add(id));
262
- }
263
-
264
- // Map each typeId to a boolean
265
- return typeIds.map((id) => {
266
- // Full game typeIds can only be resolved when game is completed
267
- if (FULL_GAME_TYPE_IDS.includes(id)) {
268
- return isCompleted;
269
- }
270
- return resolvableTypeIds.has(id);
271
- });
272
- }
273
-
274
- /**
275
- * Filters a list of market types to only those that can be resolved
276
- *
277
- * @example
278
- * // Filter typeIds for MLB (innings-based)
279
- * const resolvable = filterMarketsThatCanBeResolved(event, [10021, 10051, 10061], SportPeriodType.INNINGS_BASED);
280
- * // Returns: [10021] if only period 1-4 complete, [10021, 10051] if period 5 complete
281
- */
282
- export function filterMarketsThatCanBeResolved(
283
- event: OpticOddsEvent,
284
- typeIds: number[],
285
- sportType: SportPeriodType | number
286
- ): number[] {
287
- // Get completed periods
288
- const periodData = detectCompletedPeriods(event);
289
- if (!periodData) {
290
- return [];
291
- }
292
-
293
- // Check if game is fully completed
294
- const status = (event.fixture?.status || event.status || '').toLowerCase();
295
- const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
296
-
297
- // Convert number to SportPeriodType if needed
298
- const sportTypeEnum = typeof sportType === 'number' ? mapNumberToSportPeriodType(sportType) : sportType;
299
-
300
- // Select appropriate mapping based on sport type
301
- const mapping = selectMappingForSportType(sportTypeEnum);
302
-
303
- // Collect all resolvable typeIds based on completed periods
304
- const resolvableTypeIds = new Set<number>();
305
-
306
- for (const period of periodData.completedPeriods) {
307
- const typeIdsForPeriod = mapping[period] || [];
308
- typeIdsForPeriod.forEach((id) => resolvableTypeIds.add(id));
309
- }
310
-
311
- // Filter typeIds to only those that can be resolved
312
- return typeIds.filter((id) => {
313
- // Full game typeIds can only be resolved when game is completed
314
- if (FULL_GAME_TYPE_IDS.includes(id)) {
315
- return isCompleted;
316
- }
317
- return resolvableTypeIds.has(id);
318
- });
319
- }
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
+ import { getSportPeriodTypeFromEvent } from './sportPeriodMapping';
13
+
14
+ /**
15
+ * Detects completed periods for a game based on OpticOdds API event data
16
+ * A period is considered complete if the next period (period + 1) exists in the scores
17
+ * @param event - Event object from OpticOdds API
18
+ * @returns PeriodResolutionData with completed periods, readiness status, and period scores
19
+ */
20
+ export const detectCompletedPeriods = (event: OpticOddsEvent): PeriodResolutionData | null => {
21
+ const status = (event.fixture?.status || event.status || '').toLowerCase();
22
+ const isLive = event.fixture?.is_live ?? event.is_live ?? false;
23
+
24
+ // Determine sport period type for sport-aware logic
25
+ // Defaults to PERIOD_BASED (no halftime processing) for unknown sports
26
+ const sportType = getSportPeriodTypeFromEvent(event);
27
+
28
+ // Extract period scores from the event
29
+ const homePeriods = event.scores?.home?.periods || {};
30
+ const awayPeriods = event.scores?.away?.periods || {};
31
+
32
+ const periodScores: PeriodScores = {};
33
+ const completedPeriods: number[] = [];
34
+
35
+ // Parse all available periods
36
+ const periodKeys = Object.keys(homePeriods)
37
+ .filter((key) => key.startsWith('period_'))
38
+ .map((key) => parseInt(key.replace('period_', '')))
39
+ .sort((a, b) => a - b);
40
+
41
+ if (periodKeys.length === 0) {
42
+ return null; // No period data available
43
+ }
44
+
45
+ // Get current period from in_play if available (only use if numeric)
46
+ const inPlayPeriod = event.in_play?.period;
47
+ const currentLivePeriod = inPlayPeriod && !isNaN(parseInt(inPlayPeriod)) ? parseInt(inPlayPeriod) : null;
48
+
49
+ // Check if game is in overtime/extra time (non-numeric period indicator)
50
+ const isInOvertime =
51
+ inPlayPeriod &&
52
+ (inPlayPeriod.toLowerCase().includes('overtime') ||
53
+ inPlayPeriod.toLowerCase().includes('ot') ||
54
+ inPlayPeriod.toLowerCase().includes('extra'));
55
+
56
+ // Check if game is at halftime
57
+ const isAtHalftime =
58
+ status === 'half' || status === 'halftime' || (inPlayPeriod && inPlayPeriod.toLowerCase() === 'half');
59
+
60
+ // For each period, check if it's complete
61
+ for (const periodNum of periodKeys) {
62
+ const key = `period_${periodNum}`;
63
+ const homeScore = homePeriods[key];
64
+ const awayScore = awayPeriods[key];
65
+
66
+ // Add this period's score
67
+ if (homeScore !== undefined && awayScore !== undefined && !isNaN(homeScore) && !isNaN(awayScore)) {
68
+ periodScores[`period${periodNum}`] = { home: homeScore, away: awayScore };
69
+
70
+ // Period is complete if:
71
+ // 1. Game is completed (status = completed/finished), OR
72
+ // 2. Game is live AND in_play.period is GREATER than this period, OR
73
+ // 3. Game is in overtime (all regulation periods are complete), OR
74
+ // 4. Game is at halftime AND this period is complete based on sport type
75
+ //
76
+ // Note: We do NOT check if next period exists in data, as OpticOdds may include
77
+ // future periods with scores (including zeros). Only in_play.period is
78
+ // the source of truth for live games.
79
+ const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
80
+ const isCompletedInLiveGame = isLive && currentLivePeriod !== null && currentLivePeriod > periodNum;
81
+
82
+ // Sport-aware halftime logic
83
+ let isCompletedAtHalftime = false;
84
+ if (isAtHalftime) {
85
+ switch (sportType) {
86
+ case SportPeriodType.HALVES_BASED:
87
+ // Soccer, etc: halftime = end of period 1 (first half complete)
88
+ isCompletedAtHalftime = periodNum === 1;
89
+ break;
90
+ case SportPeriodType.QUARTERS_BASED:
91
+ // Basketball, Football: halftime = end of period 2 (first two quarters complete)
92
+ isCompletedAtHalftime = periodNum === 1 || periodNum === 2;
93
+ break;
94
+ case SportPeriodType.INNINGS_BASED:
95
+ // Baseball: halftime = middle of game (first 5 innings complete)
96
+ isCompletedAtHalftime = periodNum >= 1 && periodNum <= 5;
97
+ break;
98
+ case SportPeriodType.PERIOD_BASED:
99
+ // Hockey: no traditional halftime concept
100
+ isCompletedAtHalftime = false;
101
+ break;
102
+ }
103
+ }
104
+
105
+ if (isCompleted || isCompletedInLiveGame || isInOvertime || isCompletedAtHalftime) {
106
+ completedPeriods.push(periodNum);
107
+ }
108
+ }
109
+ }
110
+
111
+ // Determine current period
112
+ // For live games with numeric in_play period, use that as the authoritative current period
113
+ // Otherwise use highest period number from the data
114
+ const highestPeriodInData = periodKeys.length > 0 ? Math.max(...periodKeys) : undefined;
115
+ const currentPeriod = isLive && currentLivePeriod !== null ? currentLivePeriod : highestPeriodInData;
116
+
117
+ return completedPeriods.length > 0
118
+ ? {
119
+ completedPeriods,
120
+ readyForResolution: true,
121
+ periodScores,
122
+ currentPeriod,
123
+ }
124
+ : null;
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(
144
+ `Invalid sport type number: ${sportTypeNum}. Must be 0 (halves), 1 (quarters), 2 (innings), or 3 (period).`
145
+ );
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Selects the appropriate period-to-typeId mapping based on sport type
151
+ * @param sportType - Sport period structure type
152
+ * @returns Period-to-typeId mapping for the specified sport type
153
+ */
154
+ function selectMappingForSportType(sportType: SportPeriodType): { [period: number]: number[] } {
155
+ switch (sportType) {
156
+ case SportPeriodType.HALVES_BASED:
157
+ return HALVES_PERIOD_TYPE_ID_MAPPING;
158
+ case SportPeriodType.QUARTERS_BASED:
159
+ return QUARTERS_PERIOD_TYPE_ID_MAPPING;
160
+ case SportPeriodType.INNINGS_BASED:
161
+ return INNINGS_PERIOD_TYPE_ID_MAPPING;
162
+ case SportPeriodType.PERIOD_BASED:
163
+ return PERIOD_BASED_TYPE_ID_MAPPING;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Checks if a specific market type can be resolved based on completed periods (Enum version)
169
+ *
170
+ * @example
171
+ * // Check single typeId for NFL (quarters-based)
172
+ * const canResolve = canResolveMarketsForEvent(event, 10021, SportPeriodType.QUARTERS_BASED);
173
+ *
174
+ * // Check with period-based (no halves/secondary moneyline)
175
+ * const canResolve = canResolveMarketsForEvent(event, 10021, SportPeriodType.PERIOD_BASED);
176
+ *
177
+ * @param event - OpticOdds event object containing fixture data
178
+ * @param typeId - The market type identifier
179
+ * @param sportType - The sport period type enum
180
+ * @returns true if the market can be resolved based on completed periods
181
+ */
182
+ export function canResolveMarketsForEvent(event: OpticOddsEvent, typeId: number, sportType: SportPeriodType): boolean {
183
+ const periodData = detectCompletedPeriods(event);
184
+ if (!periodData) return false;
185
+
186
+ const status = (event.fixture?.status || event.status || '').toLowerCase();
187
+ const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
188
+
189
+ // Full game typeIds can only be resolved when game is completed
190
+ if (FULL_GAME_TYPE_IDS.includes(typeId)) {
191
+ return isCompleted;
192
+ }
193
+
194
+ // Select appropriate mapping based on sport type
195
+ const mapping = selectMappingForSportType(sportType);
196
+
197
+ const resolvableTypeIds = new Set<number>();
198
+ for (const period of periodData.completedPeriods) {
199
+ const typeIdsForPeriod = mapping[period] || [];
200
+ typeIdsForPeriod.forEach((id) => resolvableTypeIds.add(id));
201
+ }
202
+
203
+ return resolvableTypeIds.has(typeId);
204
+ }
205
+
206
+ /**
207
+ * Checks if a specific market type can be resolved based on completed periods (Number version)
208
+ *
209
+ * @example
210
+ * // Check single typeId for NFL (quarters-based)
211
+ * const canResolve = canResolveMarketsForEventNumber(event, 10021, 1);
212
+ *
213
+ * @param event - OpticOdds event object containing fixture data
214
+ * @param typeId - The market type identifier
215
+ * @param sportTypeNumber - Numeric value representing the sport period type
216
+ * @returns true if the market can be resolved based on completed periods
217
+ */
218
+ export function canResolveMarketsForEventNumber(
219
+ event: OpticOddsEvent,
220
+ typeId: number,
221
+ sportTypeNumber: number
222
+ ): boolean {
223
+ const sportTypeEnum = mapNumberToSportPeriodType(sportTypeNumber);
224
+ return canResolveMarketsForEvent(event, typeId, sportTypeEnum);
225
+ }
226
+
227
+ /**
228
+ * Checks if multiple market types can be resolved, returning a boolean for each
229
+ *
230
+ * @example
231
+ * // Check multiple typeIds for MLB (innings-based)
232
+ * const canResolve = canResolveMultipleTypeIdsForEvent(event, [10021, 10051], SportPeriodType.INNINGS_BASED);
233
+ * // Returns: [true, false] if only 10021 can be resolved
234
+ */
235
+ export function canResolveMultipleTypeIdsForEvent(
236
+ event: OpticOddsEvent,
237
+ typeIds: number[],
238
+ sportType: SportPeriodType | number
239
+ ): boolean[] {
240
+ // Get completed periods
241
+ const periodData = detectCompletedPeriods(event);
242
+ if (!periodData) {
243
+ return typeIds.map(() => false);
244
+ }
245
+
246
+ // Check if game is fully completed
247
+ const status = (event.fixture?.status || event.status || '').toLowerCase();
248
+ const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
249
+
250
+ // Convert number to SportPeriodType if needed
251
+ const sportTypeEnum = typeof sportType === 'number' ? mapNumberToSportPeriodType(sportType) : sportType;
252
+
253
+ // Select appropriate mapping based on sport type
254
+ const mapping = selectMappingForSportType(sportTypeEnum);
255
+
256
+ // Collect all resolvable typeIds based on completed periods
257
+ const resolvableTypeIds = new Set<number>();
258
+
259
+ for (const period of periodData.completedPeriods) {
260
+ const typeIdsForPeriod = mapping[period] || [];
261
+ typeIdsForPeriod.forEach((id) => resolvableTypeIds.add(id));
262
+ }
263
+
264
+ // Map each typeId to a boolean
265
+ return typeIds.map((id) => {
266
+ // Full game typeIds can only be resolved when game is completed
267
+ if (FULL_GAME_TYPE_IDS.includes(id)) {
268
+ return isCompleted;
269
+ }
270
+ return resolvableTypeIds.has(id);
271
+ });
272
+ }
273
+
274
+ /**
275
+ * Filters a list of market types to only those that can be resolved
276
+ *
277
+ * @example
278
+ * // Filter typeIds for MLB (innings-based)
279
+ * const resolvable = filterMarketsThatCanBeResolved(event, [10021, 10051, 10061], SportPeriodType.INNINGS_BASED);
280
+ * // Returns: [10021] if only period 1-4 complete, [10021, 10051] if period 5 complete
281
+ */
282
+ export function filterMarketsThatCanBeResolved(
283
+ event: OpticOddsEvent,
284
+ typeIds: number[],
285
+ sportType: SportPeriodType | number
286
+ ): number[] {
287
+ // Get completed periods
288
+ const periodData = detectCompletedPeriods(event);
289
+ if (!periodData) {
290
+ return [];
291
+ }
292
+
293
+ // Check if game is fully completed
294
+ const status = (event.fixture?.status || event.status || '').toLowerCase();
295
+ const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
296
+
297
+ // Convert number to SportPeriodType if needed
298
+ const sportTypeEnum = typeof sportType === 'number' ? mapNumberToSportPeriodType(sportType) : sportType;
299
+
300
+ // Select appropriate mapping based on sport type
301
+ const mapping = selectMappingForSportType(sportTypeEnum);
302
+
303
+ // Collect all resolvable typeIds based on completed periods
304
+ const resolvableTypeIds = new Set<number>();
305
+
306
+ for (const period of periodData.completedPeriods) {
307
+ const typeIdsForPeriod = mapping[period] || [];
308
+ typeIdsForPeriod.forEach((id) => resolvableTypeIds.add(id));
309
+ }
310
+
311
+ // Filter typeIds to only those that can be resolved
312
+ return typeIds.filter((id) => {
313
+ // Full game typeIds can only be resolved when game is completed
314
+ if (FULL_GAME_TYPE_IDS.includes(id)) {
315
+ return isCompleted;
316
+ }
317
+ return resolvableTypeIds.has(id);
318
+ });
319
+ }