overtime-live-trading-utils 2.1.33 → 2.1.35
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.
- package/CLAUDE.md +26 -19
- package/main.js +1 -1
- package/package.json +1 -1
- package/resolution_live_markets.md +93 -88
- package/src/tests/mock/MockOpticOddsEvents.ts +144 -0
- package/src/tests/unit/resolution.test.ts +471 -71
- package/src/types/resolution.ts +25 -25
- package/src/utils/resolution.ts +43 -22
- package/src/utils/sportPeriodMapping.ts +36 -0
- package/tsconfig.json +3 -7
package/src/types/resolution.ts
CHANGED
|
@@ -36,8 +36,8 @@ export enum SportPeriodType {
|
|
|
36
36
|
* Period 2 = 2nd half
|
|
37
37
|
*/
|
|
38
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
|
|
39
|
+
1: [10021, 10031, 10041, 10051, 10061, 10071, 10081, 10111, 10112, 10121, 10163, 10200, 10101], // 1st half
|
|
40
|
+
2: [10022, 10032, 10042, 10052, 10062, 10072, 10082, 10211, 10212, 10122, 10164, 10201, 10102], // 2nd half
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -49,11 +49,11 @@ export const HALVES_PERIOD_TYPE_ID_MAPPING: { [period: number]: number[] } = {
|
|
|
49
49
|
* Period 5+ = Overtime
|
|
50
50
|
*/
|
|
51
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
|
|
52
|
+
1: [10021, 10031, 10041, 10061, 10071, 10081, 10111, 10112, 10121, 10163, 10101], // 1st quarter
|
|
53
|
+
2: [10022, 10032, 10042, 10051, 10062, 10072, 10082, 10211, 10212, 10122, 10164, 10102], // 2nd quarter + 1st half
|
|
54
|
+
3: [10023, 10033, 10043, 10063, 10073, 10083, 10311, 10312, 10123, 10165, 10103], // 3rd quarter
|
|
55
|
+
4: [10024, 10034, 10044, 10052, 10064, 10074, 10084, 10411, 10412, 10124, 10166, 10104], // 4th quarter + 2nd half
|
|
56
|
+
5: [10025, 10035, 10045, 10055, 10065, 10075, 10085, 10511, 10512, 10167, 10105], // Overtime/5th period
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
/**
|
|
@@ -62,15 +62,15 @@ export const QUARTERS_PERIOD_TYPE_ID_MAPPING: { [period: number]: number[] } = {
|
|
|
62
62
|
* Period 6-9 = Innings 6-9 (period 9 completes 2nd half - typeId 10052)
|
|
63
63
|
*/
|
|
64
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
|
|
65
|
+
1: [10021, 10031, 10041, 10061, 10071, 10081, 10111, 10112, 10121, 10163, 10101], // 1st inning
|
|
66
|
+
2: [10022, 10032, 10042, 10062, 10072, 10082, 10211, 10212, 10122, 10164, 10102], // 2nd inning
|
|
67
|
+
3: [10023, 10033, 10043, 10063, 10073, 10083, 10311, 10312, 10123, 10165, 10170, 10171, 10172, 10173, 10174, 10201, 10103], // 3rd inning + first 3 innings
|
|
68
|
+
4: [10024, 10034, 10044, 10064, 10074, 10084, 10411, 10412, 10124, 10166, 10104], // 4th inning
|
|
69
|
+
5: [10025, 10035, 10045, 10051, 10065, 10075, 10085, 10511, 10512, 10167, 10200, 10105], // 5th inning + 1st half (first 5 innings)
|
|
70
|
+
6: [10026, 10036, 10046, 10056, 10066, 10076, 10086, 10611, 10612, 10168, 10106], // 6th inning
|
|
71
|
+
7: [10027, 10037, 10047, 10057, 10067, 10077, 10087, 10711, 10712, 10169, 10175, 10176, 10177, 10178, 10179, 10202, 10107], // 7th inning + first 7 innings
|
|
72
|
+
8: [10028, 10038, 10048, 10058, 10068, 10078, 10088, 10811, 10812, 10197, 10108], // 8th inning
|
|
73
|
+
9: [10029, 10039, 10049, 10052, 10069, 10079, 10089, 10198, 10109], // 9th inning
|
|
74
74
|
};
|
|
75
75
|
|
|
76
76
|
/**
|
|
@@ -78,15 +78,15 @@ export const INNINGS_PERIOD_TYPE_ID_MAPPING: { [period: number]: number[] } = {
|
|
|
78
78
|
* Excludes halves types (10051, 10052) and secondary moneyline types (ending in 11/12)
|
|
79
79
|
*/
|
|
80
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
|
|
81
|
+
1: [10021, 10031, 10041, 10061, 10071, 10081, 10121, 10163, 10101], // 1st period
|
|
82
|
+
2: [10022, 10032, 10042, 10062, 10072, 10082, 10122, 10164, 10102], // 2nd period
|
|
83
|
+
3: [10023, 10033, 10043, 10063, 10073, 10083, 10123, 10165, 10103], // 3rd period
|
|
84
|
+
4: [10024, 10034, 10044, 10064, 10074, 10084, 10124, 10166, 10104], // 4th period
|
|
85
|
+
5: [10025, 10035, 10045, 10065, 10075, 10085, 10167, 10105], // 5th period
|
|
86
|
+
6: [10026, 10036, 10046, 10056, 10066, 10076, 10086, 10168, 10106], // 6th period
|
|
87
|
+
7: [10027, 10037, 10047, 10057, 10067, 10077, 10087, 10169, 10107], // 7th period
|
|
88
|
+
8: [10028, 10038, 10048, 10058, 10068, 10078, 10088, 10197, 10108], // 8th period
|
|
89
|
+
9: [10029, 10039, 10049, 10069, 10079, 10089, 10198, 10109], // 9th period
|
|
90
90
|
};
|
|
91
91
|
|
|
92
92
|
/**
|
package/src/utils/resolution.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
PERIOD_BASED_TYPE_ID_MAPPING,
|
|
10
10
|
FULL_GAME_TYPE_IDS,
|
|
11
11
|
} from '../types/resolution';
|
|
12
|
+
import { getSportPeriodTypeFromEvent } from './sportPeriodMapping';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Detects completed periods for a game based on OpticOdds API event data
|
|
@@ -16,12 +17,14 @@ import {
|
|
|
16
17
|
* @param event - Event object from OpticOdds API
|
|
17
18
|
* @returns PeriodResolutionData with completed periods, readiness status, and period scores
|
|
18
19
|
*/
|
|
19
|
-
export const detectCompletedPeriods = (
|
|
20
|
-
event: OpticOddsEvent
|
|
21
|
-
): PeriodResolutionData | null => {
|
|
20
|
+
export const detectCompletedPeriods = (event: OpticOddsEvent): PeriodResolutionData | null => {
|
|
22
21
|
const status = (event.fixture?.status || event.status || '').toLowerCase();
|
|
23
22
|
const isLive = event.fixture?.is_live ?? event.is_live ?? false;
|
|
24
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
|
+
|
|
25
28
|
// Extract period scores from the event
|
|
26
29
|
const homePeriods = event.scores?.home?.periods || {};
|
|
27
30
|
const awayPeriods = event.scores?.away?.periods || {};
|
|
@@ -44,14 +47,15 @@ export const detectCompletedPeriods = (
|
|
|
44
47
|
const currentLivePeriod = inPlayPeriod && !isNaN(parseInt(inPlayPeriod)) ? parseInt(inPlayPeriod) : null;
|
|
45
48
|
|
|
46
49
|
// Check if game is in overtime/extra time (non-numeric period indicator)
|
|
47
|
-
const isInOvertime =
|
|
50
|
+
const isInOvertime =
|
|
51
|
+
inPlayPeriod &&
|
|
48
52
|
(inPlayPeriod.toLowerCase().includes('overtime') ||
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
inPlayPeriod.toLowerCase().includes('ot') ||
|
|
54
|
+
inPlayPeriod.toLowerCase().includes('extra'));
|
|
51
55
|
|
|
52
|
-
// Check if game is at halftime
|
|
53
|
-
const isAtHalftime =
|
|
54
|
-
(inPlayPeriod && inPlayPeriod.toLowerCase() === 'half');
|
|
56
|
+
// Check if game is at halftime
|
|
57
|
+
const isAtHalftime =
|
|
58
|
+
status === 'half' || status === 'halftime' || (inPlayPeriod && inPlayPeriod.toLowerCase() === 'half');
|
|
55
59
|
|
|
56
60
|
// For each period, check if it's complete
|
|
57
61
|
for (const periodNum of periodKeys) {
|
|
@@ -67,16 +71,38 @@ export const detectCompletedPeriods = (
|
|
|
67
71
|
// 1. Game is completed (status = completed/finished), OR
|
|
68
72
|
// 2. Game is live AND in_play.period is GREATER than this period, OR
|
|
69
73
|
// 3. Game is in overtime (all regulation periods are complete), OR
|
|
70
|
-
// 4. Game is at halftime AND this is
|
|
74
|
+
// 4. Game is at halftime AND this period is complete based on sport type
|
|
71
75
|
//
|
|
72
76
|
// Note: We do NOT check if next period exists in data, as OpticOdds may include
|
|
73
77
|
// future periods with scores (including zeros). Only in_play.period is
|
|
74
78
|
// the source of truth for live games.
|
|
75
79
|
const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
|
|
76
80
|
const isCompletedInLiveGame = isLive && currentLivePeriod !== null && currentLivePeriod > periodNum;
|
|
77
|
-
const isFirstPeriodAtHalftime = isAtHalftime && periodNum === 1;
|
|
78
81
|
|
|
79
|
-
|
|
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) {
|
|
80
106
|
completedPeriods.push(periodNum);
|
|
81
107
|
}
|
|
82
108
|
}
|
|
@@ -86,9 +112,7 @@ export const detectCompletedPeriods = (
|
|
|
86
112
|
// For live games with numeric in_play period, use that as the authoritative current period
|
|
87
113
|
// Otherwise use highest period number from the data
|
|
88
114
|
const highestPeriodInData = periodKeys.length > 0 ? Math.max(...periodKeys) : undefined;
|
|
89
|
-
const currentPeriod = isLive && currentLivePeriod !== null
|
|
90
|
-
? currentLivePeriod
|
|
91
|
-
: highestPeriodInData;
|
|
115
|
+
const currentPeriod = isLive && currentLivePeriod !== null ? currentLivePeriod : highestPeriodInData;
|
|
92
116
|
|
|
93
117
|
return completedPeriods.length > 0
|
|
94
118
|
? {
|
|
@@ -116,7 +140,9 @@ export function mapNumberToSportPeriodType(sportTypeNum: number): SportPeriodTyp
|
|
|
116
140
|
case 3:
|
|
117
141
|
return SportPeriodType.PERIOD_BASED;
|
|
118
142
|
default:
|
|
119
|
-
throw new Error(
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Invalid sport type number: ${sportTypeNum}. Must be 0 (halves), 1 (quarters), 2 (innings), or 3 (period).`
|
|
145
|
+
);
|
|
120
146
|
}
|
|
121
147
|
}
|
|
122
148
|
|
|
@@ -153,11 +179,7 @@ function selectMappingForSportType(sportType: SportPeriodType): { [period: numbe
|
|
|
153
179
|
* @param sportType - The sport period type enum
|
|
154
180
|
* @returns true if the market can be resolved based on completed periods
|
|
155
181
|
*/
|
|
156
|
-
export function canResolveMarketsForEvent(
|
|
157
|
-
event: OpticOddsEvent,
|
|
158
|
-
typeId: number,
|
|
159
|
-
sportType: SportPeriodType
|
|
160
|
-
): boolean {
|
|
182
|
+
export function canResolveMarketsForEvent(event: OpticOddsEvent, typeId: number, sportType: SportPeriodType): boolean {
|
|
161
183
|
const periodData = detectCompletedPeriods(event);
|
|
162
184
|
if (!periodData) return false;
|
|
163
185
|
|
|
@@ -202,7 +224,6 @@ export function canResolveMarketsForEventNumber(
|
|
|
202
224
|
return canResolveMarketsForEvent(event, typeId, sportTypeEnum);
|
|
203
225
|
}
|
|
204
226
|
|
|
205
|
-
|
|
206
227
|
/**
|
|
207
228
|
* Checks if multiple market types can be resolved, returning a boolean for each
|
|
208
229
|
*
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { OpticOddsEvent, SportPeriodType } from '../types/resolution';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Determines the period structure type for a sport based on OpticOdds event data
|
|
5
|
+
* @param event - OpticOdds event object containing sport information
|
|
6
|
+
* @returns SportPeriodType enum value indicating the period structure
|
|
7
|
+
*/
|
|
8
|
+
export function getSportPeriodTypeFromEvent(event: OpticOddsEvent): SportPeriodType {
|
|
9
|
+
const sportId = event.sport?.id?.toLowerCase();
|
|
10
|
+
|
|
11
|
+
if (!sportId) {
|
|
12
|
+
// Default to PERIOD_BASED (no halftime processing) for unknown sports
|
|
13
|
+
return SportPeriodType.PERIOD_BASED;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Map OpticOdds sport string ID to SportPeriodType
|
|
17
|
+
switch (sportId) {
|
|
18
|
+
case 'soccer':
|
|
19
|
+
return SportPeriodType.HALVES_BASED;
|
|
20
|
+
|
|
21
|
+
case 'football':
|
|
22
|
+
case 'basketball':
|
|
23
|
+
return SportPeriodType.QUARTERS_BASED;
|
|
24
|
+
|
|
25
|
+
case 'baseball':
|
|
26
|
+
return SportPeriodType.INNINGS_BASED;
|
|
27
|
+
|
|
28
|
+
case 'hockey':
|
|
29
|
+
case 'ice_hockey':
|
|
30
|
+
return SportPeriodType.PERIOD_BASED;
|
|
31
|
+
|
|
32
|
+
default:
|
|
33
|
+
// Default to PERIOD_BASED (no halftime processing) for unknown sports
|
|
34
|
+
return SportPeriodType.PERIOD_BASED;
|
|
35
|
+
}
|
|
36
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -3,15 +3,11 @@
|
|
|
3
3
|
"module": "esnext",
|
|
4
4
|
"esModuleInterop": true,
|
|
5
5
|
"target": "es5",
|
|
6
|
-
"lib": [
|
|
7
|
-
"dom",
|
|
8
|
-
"dom.iterable",
|
|
9
|
-
"esnext"
|
|
10
|
-
],
|
|
6
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
11
7
|
"moduleResolution": "node",
|
|
12
8
|
"sourceMap": true,
|
|
13
9
|
"outDir": "build",
|
|
14
10
|
"declaration": true,
|
|
15
|
-
"strict": true
|
|
11
|
+
"strict": true
|
|
16
12
|
}
|
|
17
|
-
}
|
|
13
|
+
}
|