overtime-live-trading-utils 2.1.17 → 2.1.18
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/.circleci/config.yml +32 -32
- package/.prettierrc +9 -9
- package/CLAUDE.md +77 -0
- package/codecov.yml +20 -20
- package/index.ts +26 -16
- package/jest.config.ts +16 -16
- package/main.js +1 -1
- package/package.json +30 -30
- package/resolution_live_markets.md +351 -0
- package/src/constants/common.ts +7 -7
- package/src/constants/errors.ts +6 -6
- package/src/constants/sports.ts +78 -78
- package/src/enums/sports.ts +109 -109
- package/src/tests/mock/MockLeagueMap.ts +170 -170
- package/src/tests/mock/MockOpticOddsEvents.ts +518 -0
- package/src/tests/mock/MockOpticSoccer.ts +9378 -9378
- package/src/tests/mock/MockSoccerRedis.ts +2308 -2308
- package/src/tests/unit/bookmakers.test.ts +79 -79
- package/src/tests/unit/markets.test.ts +156 -156
- package/src/tests/unit/odds.test.ts +92 -92
- package/src/tests/unit/resolution.test.ts +1043 -0
- package/src/tests/unit/sports.test.ts +58 -58
- package/src/tests/unit/spread.test.ts +131 -131
- package/src/types/missing-types.d.ts +2 -2
- package/src/types/odds.ts +61 -61
- package/src/types/resolution.ts +78 -0
- package/src/types/sports.ts +19 -19
- package/src/utils/bookmakers.ts +159 -159
- package/src/utils/constraints.ts +210 -210
- package/src/utils/gameMatching.ts +81 -81
- package/src/utils/markets.ts +119 -119
- package/src/utils/odds.ts +674 -674
- package/src/utils/opticOdds.ts +71 -71
- package/src/utils/resolution.ts +221 -0
- package/src/utils/sports.ts +51 -51
- package/src/utils/spread.ts +97 -97
- package/tsconfig.json +16 -16
- package/webpack.config.js +24 -24
package/src/utils/opticOdds.ts
CHANGED
|
@@ -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
|
+
}, {});
|
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
FULL_GAME_TYPE_IDS,
|
|
10
|
+
} from '../types/resolution';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Detects completed periods for a game based on OpticOdds API event data
|
|
14
|
+
* A period is considered complete if the next period (period + 1) exists in the scores
|
|
15
|
+
* @param event - Event object from OpticOdds API
|
|
16
|
+
* @returns PeriodResolutionData with completed periods, readiness status, and period scores
|
|
17
|
+
*/
|
|
18
|
+
export const detectCompletedPeriods = (
|
|
19
|
+
event: OpticOddsEvent
|
|
20
|
+
): PeriodResolutionData | null => {
|
|
21
|
+
const status = (event.fixture?.status || event.status || '').toLowerCase();
|
|
22
|
+
const isLive = event.fixture?.is_live ?? event.is_live ?? false;
|
|
23
|
+
|
|
24
|
+
// Extract period scores from the event
|
|
25
|
+
const homePeriods = event.scores?.home?.periods || {};
|
|
26
|
+
const awayPeriods = event.scores?.away?.periods || {};
|
|
27
|
+
|
|
28
|
+
const periodScores: PeriodScores = {};
|
|
29
|
+
const completedPeriods: number[] = [];
|
|
30
|
+
|
|
31
|
+
// Parse all available periods
|
|
32
|
+
const periodKeys = Object.keys(homePeriods)
|
|
33
|
+
.filter((key) => key.startsWith('period_'))
|
|
34
|
+
.map((key) => parseInt(key.replace('period_', '')))
|
|
35
|
+
.sort((a, b) => a - b);
|
|
36
|
+
|
|
37
|
+
if (periodKeys.length === 0) {
|
|
38
|
+
return null; // No period data available
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get current period from in_play if available (only use if numeric)
|
|
42
|
+
const inPlayPeriod = event.in_play?.period;
|
|
43
|
+
const currentLivePeriod = inPlayPeriod && !isNaN(parseInt(inPlayPeriod)) ? parseInt(inPlayPeriod) : null;
|
|
44
|
+
|
|
45
|
+
// Check if game is in overtime/extra time (non-numeric period indicator)
|
|
46
|
+
const isInOvertime = inPlayPeriod &&
|
|
47
|
+
(inPlayPeriod.toLowerCase().includes('overtime') ||
|
|
48
|
+
inPlayPeriod.toLowerCase().includes('ot') ||
|
|
49
|
+
inPlayPeriod.toLowerCase().includes('extra'));
|
|
50
|
+
|
|
51
|
+
// For each period, check if it's complete
|
|
52
|
+
for (const periodNum of periodKeys) {
|
|
53
|
+
const key = `period_${periodNum}`;
|
|
54
|
+
const homeScore = homePeriods[key];
|
|
55
|
+
const awayScore = awayPeriods[key];
|
|
56
|
+
|
|
57
|
+
// Add this period's score
|
|
58
|
+
if (homeScore !== undefined && awayScore !== undefined && !isNaN(homeScore) && !isNaN(awayScore)) {
|
|
59
|
+
periodScores[`period${periodNum}`] = { home: homeScore, away: awayScore };
|
|
60
|
+
|
|
61
|
+
// Period is complete if:
|
|
62
|
+
// 1. Next period exists in periods data, OR
|
|
63
|
+
// 2. Game is completed, OR
|
|
64
|
+
// 3. Game is live with numeric period AND current live period is greater than this period, OR
|
|
65
|
+
// 4. Game is in overtime (all regulation periods are complete)
|
|
66
|
+
const nextPeriodKey = `period_${periodNum + 1}`;
|
|
67
|
+
const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
|
|
68
|
+
const hasNextPeriod = homePeriods[nextPeriodKey] !== undefined && awayPeriods[nextPeriodKey] !== undefined;
|
|
69
|
+
const isCompletedInLiveGame = isLive && currentLivePeriod !== null && currentLivePeriod > periodNum;
|
|
70
|
+
|
|
71
|
+
if (hasNextPeriod || isCompleted || isCompletedInLiveGame || isInOvertime) {
|
|
72
|
+
completedPeriods.push(periodNum);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Determine current period
|
|
78
|
+
// If we have a numeric in_play period and it's greater than highest period in data, use it
|
|
79
|
+
// Otherwise use highest period number from the data
|
|
80
|
+
const highestPeriodInData = periodKeys.length > 0 ? Math.max(...periodKeys) : undefined;
|
|
81
|
+
const currentPeriod = currentLivePeriod && currentLivePeriod > (highestPeriodInData || 0)
|
|
82
|
+
? currentLivePeriod
|
|
83
|
+
: highestPeriodInData;
|
|
84
|
+
|
|
85
|
+
return completedPeriods.length > 0
|
|
86
|
+
? {
|
|
87
|
+
completedPeriods,
|
|
88
|
+
readyForResolution: true,
|
|
89
|
+
periodScores,
|
|
90
|
+
currentPeriod,
|
|
91
|
+
}
|
|
92
|
+
: null;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Checks if a market can be resolved based on game and sport
|
|
97
|
+
* @param gameId - The game/fixture ID
|
|
98
|
+
* @param event - Event object from OpticOdds API
|
|
99
|
+
* @returns boolean indicating if market can be resolved
|
|
100
|
+
*/
|
|
101
|
+
export const canResolveMarketForGameIdAndSport = (
|
|
102
|
+
gameId: string,
|
|
103
|
+
event: OpticOddsEvent
|
|
104
|
+
): boolean => {
|
|
105
|
+
const eventId = event.fixture?.id || event.id || '';
|
|
106
|
+
if (eventId !== gameId) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const periodData = detectCompletedPeriods(event);
|
|
111
|
+
return periodData !== null && periodData.readyForResolution;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Convenience function to check resolution status via OpticOdds API event
|
|
116
|
+
* @param event - Event object from OpticOdds API
|
|
117
|
+
* @returns PeriodResolutionData if periods are complete, null otherwise
|
|
118
|
+
*/
|
|
119
|
+
export const canResolveMarketViaOpticOddsApi = (
|
|
120
|
+
event: OpticOddsEvent
|
|
121
|
+
): PeriodResolutionData | null => {
|
|
122
|
+
return detectCompletedPeriods(event);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Selects the appropriate period-to-typeId mapping based on sport type
|
|
127
|
+
* @param sportType - Sport period structure type
|
|
128
|
+
* @returns Period-to-typeId mapping for the specified sport type
|
|
129
|
+
*/
|
|
130
|
+
function selectMappingForSportType(sportType: SportPeriodType): { [period: number]: number[] } {
|
|
131
|
+
switch (sportType) {
|
|
132
|
+
case SportPeriodType.HALVES_BASED:
|
|
133
|
+
return HALVES_PERIOD_TYPE_ID_MAPPING;
|
|
134
|
+
case SportPeriodType.QUARTERS_BASED:
|
|
135
|
+
return QUARTERS_PERIOD_TYPE_ID_MAPPING;
|
|
136
|
+
case SportPeriodType.INNINGS_BASED:
|
|
137
|
+
return INNINGS_PERIOD_TYPE_ID_MAPPING;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Checks if a single market type can be resolved based on completed periods
|
|
143
|
+
* @param event - Event object from OpticOdds API
|
|
144
|
+
* @param typeId - Single market type ID to check
|
|
145
|
+
* @param sportType - Sport period structure type (halves, quarters, or innings based) - REQUIRED
|
|
146
|
+
* @returns boolean indicating if that typeId can be resolved
|
|
147
|
+
*/
|
|
148
|
+
export function canResolveMarketsForEvent(
|
|
149
|
+
event: OpticOddsEvent,
|
|
150
|
+
typeId: number,
|
|
151
|
+
sportType: SportPeriodType
|
|
152
|
+
): boolean;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Checks which market types can be resolved from a batch based on completed periods
|
|
156
|
+
* @param event - Event object from OpticOdds API
|
|
157
|
+
* @param typeIds - Array of market type IDs to check
|
|
158
|
+
* @param sportType - Sport period structure type (halves, quarters, or innings based) - REQUIRED
|
|
159
|
+
* @returns Array of typeIds that can be resolved
|
|
160
|
+
*/
|
|
161
|
+
export function canResolveMarketsForEvent(
|
|
162
|
+
event: OpticOddsEvent,
|
|
163
|
+
typeIds: number[],
|
|
164
|
+
sportType: SportPeriodType
|
|
165
|
+
): number[];
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Implementation - checks if specific market type(s) can be resolved based on completed periods
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* // Check single typeId for NFL (quarters-based)
|
|
172
|
+
* const canResolve = canResolveMarketsForEvent(event, 10021, SportPeriodType.QUARTERS_BASED);
|
|
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
|
+
export function canResolveMarketsForEvent(
|
|
179
|
+
event: OpticOddsEvent,
|
|
180
|
+
typeIdOrTypeIds: number | number[],
|
|
181
|
+
sportType: SportPeriodType
|
|
182
|
+
): boolean | number[] {
|
|
183
|
+
// Get completed periods
|
|
184
|
+
const periodData = detectCompletedPeriods(event);
|
|
185
|
+
if (!periodData) {
|
|
186
|
+
return Array.isArray(typeIdOrTypeIds) ? [] : false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check if game is fully completed
|
|
190
|
+
const status = (event.fixture?.status || event.status || '').toLowerCase();
|
|
191
|
+
const isCompleted = status === 'completed' || status === 'complete' || status === 'finished';
|
|
192
|
+
|
|
193
|
+
// Select appropriate mapping based on sport type
|
|
194
|
+
const mapping = selectMappingForSportType(sportType);
|
|
195
|
+
|
|
196
|
+
// Collect all resolvable typeIds based on completed periods
|
|
197
|
+
const resolvableTypeIds = new Set<number>();
|
|
198
|
+
|
|
199
|
+
for (const period of periodData.completedPeriods) {
|
|
200
|
+
const typeIdsForPeriod = mapping[period] || [];
|
|
201
|
+
typeIdsForPeriod.forEach((id) => resolvableTypeIds.add(id));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Single typeId check
|
|
205
|
+
if (typeof typeIdOrTypeIds === 'number') {
|
|
206
|
+
// Full game typeIds can only be resolved when game is completed
|
|
207
|
+
if (FULL_GAME_TYPE_IDS.includes(typeIdOrTypeIds)) {
|
|
208
|
+
return isCompleted;
|
|
209
|
+
}
|
|
210
|
+
return resolvableTypeIds.has(typeIdOrTypeIds);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Batch typeIds check
|
|
214
|
+
return typeIdOrTypeIds.filter((id) => {
|
|
215
|
+
// Full game typeIds can only be resolved when game is completed
|
|
216
|
+
if (FULL_GAME_TYPE_IDS.includes(id)) {
|
|
217
|
+
return isCompleted;
|
|
218
|
+
}
|
|
219
|
+
return resolvableTypeIds.has(id);
|
|
220
|
+
});
|
|
221
|
+
};
|
package/src/utils/sports.ts
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
import { League } from 'overtime-utils';
|
|
2
|
-
import { LeagueConfigInfo } from '../types/sports';
|
|
3
|
-
|
|
4
|
-
// Methods are using data from live-markets-map.csv
|
|
5
|
-
export const getLiveSupportedLeagues = (leagueInfoArray: LeagueConfigInfo[]) => {
|
|
6
|
-
const uniqueId = new Set();
|
|
7
|
-
leagueInfoArray
|
|
8
|
-
.filter((leagueInfo) => leagueInfo.enabled === 'true')
|
|
9
|
-
.map((league) => uniqueId.add(Number(league.sportId)));
|
|
10
|
-
return Array.from(uniqueId);
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const getBetTypesForLeague = (league: League, leagueInfoArray: LeagueConfigInfo[]) => {
|
|
14
|
-
const uniqueMarketNames = new Set();
|
|
15
|
-
leagueInfoArray
|
|
16
|
-
.filter((leagueInfo) => Number(leagueInfo.sportId) === Number(league) && leagueInfo.enabled === 'true')
|
|
17
|
-
.map((leagueInfo) => uniqueMarketNames.add(leagueInfo.marketName));
|
|
18
|
-
|
|
19
|
-
return Array.from(uniqueMarketNames) as string[];
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const getLeagueInfo = (league: League, leagueInfoArray: LeagueConfigInfo[]) => {
|
|
23
|
-
const leagueInfos = leagueInfoArray.filter((leagueInfo) => Number(leagueInfo.sportId) === league);
|
|
24
|
-
return leagueInfos;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export const getLeagueSpreadTypes = (league: League, leagueInfoArray: LeagueConfigInfo[]) => {
|
|
28
|
-
const betTypes = leagueInfoArray
|
|
29
|
-
.filter(
|
|
30
|
-
(leagueInfo) =>
|
|
31
|
-
Number(leagueInfo.sportId) === Number(league) &&
|
|
32
|
-
leagueInfo.type === 'Spread' &&
|
|
33
|
-
leagueInfo.enabled === 'true'
|
|
34
|
-
)
|
|
35
|
-
.map((leagueInfo) => leagueInfo.marketName.toLowerCase());
|
|
36
|
-
|
|
37
|
-
return betTypes;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export const getLeagueTotalTypes = (league: League, leagueInfoArray: LeagueConfigInfo[]) => {
|
|
41
|
-
const betTypes = leagueInfoArray
|
|
42
|
-
.filter(
|
|
43
|
-
(leagueInfo) =>
|
|
44
|
-
Number(leagueInfo.sportId) === Number(league) &&
|
|
45
|
-
leagueInfo.type === 'Total' &&
|
|
46
|
-
leagueInfo.enabled === 'true'
|
|
47
|
-
)
|
|
48
|
-
.map((leagueInfo) => leagueInfo.marketName.toLowerCase());
|
|
49
|
-
|
|
50
|
-
return betTypes;
|
|
51
|
-
};
|
|
1
|
+
import { League } from 'overtime-utils';
|
|
2
|
+
import { LeagueConfigInfo } from '../types/sports';
|
|
3
|
+
|
|
4
|
+
// Methods are using data from live-markets-map.csv
|
|
5
|
+
export const getLiveSupportedLeagues = (leagueInfoArray: LeagueConfigInfo[]) => {
|
|
6
|
+
const uniqueId = new Set();
|
|
7
|
+
leagueInfoArray
|
|
8
|
+
.filter((leagueInfo) => leagueInfo.enabled === 'true')
|
|
9
|
+
.map((league) => uniqueId.add(Number(league.sportId)));
|
|
10
|
+
return Array.from(uniqueId);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const getBetTypesForLeague = (league: League, leagueInfoArray: LeagueConfigInfo[]) => {
|
|
14
|
+
const uniqueMarketNames = new Set();
|
|
15
|
+
leagueInfoArray
|
|
16
|
+
.filter((leagueInfo) => Number(leagueInfo.sportId) === Number(league) && leagueInfo.enabled === 'true')
|
|
17
|
+
.map((leagueInfo) => uniqueMarketNames.add(leagueInfo.marketName));
|
|
18
|
+
|
|
19
|
+
return Array.from(uniqueMarketNames) as string[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const getLeagueInfo = (league: League, leagueInfoArray: LeagueConfigInfo[]) => {
|
|
23
|
+
const leagueInfos = leagueInfoArray.filter((leagueInfo) => Number(leagueInfo.sportId) === league);
|
|
24
|
+
return leagueInfos;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const getLeagueSpreadTypes = (league: League, leagueInfoArray: LeagueConfigInfo[]) => {
|
|
28
|
+
const betTypes = leagueInfoArray
|
|
29
|
+
.filter(
|
|
30
|
+
(leagueInfo) =>
|
|
31
|
+
Number(leagueInfo.sportId) === Number(league) &&
|
|
32
|
+
leagueInfo.type === 'Spread' &&
|
|
33
|
+
leagueInfo.enabled === 'true'
|
|
34
|
+
)
|
|
35
|
+
.map((leagueInfo) => leagueInfo.marketName.toLowerCase());
|
|
36
|
+
|
|
37
|
+
return betTypes;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const getLeagueTotalTypes = (league: League, leagueInfoArray: LeagueConfigInfo[]) => {
|
|
41
|
+
const betTypes = leagueInfoArray
|
|
42
|
+
.filter(
|
|
43
|
+
(leagueInfo) =>
|
|
44
|
+
Number(leagueInfo.sportId) === Number(league) &&
|
|
45
|
+
leagueInfo.type === 'Total' &&
|
|
46
|
+
leagueInfo.enabled === 'true'
|
|
47
|
+
)
|
|
48
|
+
.map((leagueInfo) => leagueInfo.marketName.toLowerCase());
|
|
49
|
+
|
|
50
|
+
return betTypes;
|
|
51
|
+
};
|
package/src/utils/spread.ts
CHANGED
|
@@ -1,97 +1,97 @@
|
|
|
1
|
-
import { LeagueConfigInfo } from '../types/sports';
|
|
2
|
-
|
|
3
|
-
export const adjustSpreadOnOdds = (impliedProbs: number[], minSpread: number, targetSpread: number) => {
|
|
4
|
-
// Step 1: Check if any implied probability is zero
|
|
5
|
-
if (impliedProbs.some((prob) => prob === 0)) {
|
|
6
|
-
return impliedProbs;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
// Step 2: Calculate the current total implied probabilities
|
|
10
|
-
const totalImpliedProbs = impliedProbs.reduce((sum, prob) => sum + prob, 0);
|
|
11
|
-
|
|
12
|
-
// Step 3: Check if the sum of implied probabilities is greater than 1
|
|
13
|
-
if (totalImpliedProbs <= 1) {
|
|
14
|
-
return Array(impliedProbs.length).fill(0);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Step 4: Check if targetSpread is zero
|
|
18
|
-
if (targetSpread === 0) {
|
|
19
|
-
const currentSpread = (totalImpliedProbs - 1) * 100;
|
|
20
|
-
// If minSpread is set and greater than current spread, use minSpread
|
|
21
|
-
if (minSpread > currentSpread) {
|
|
22
|
-
targetSpread = minSpread;
|
|
23
|
-
} else {
|
|
24
|
-
// If minSpread is less than current spread, return odds as they are
|
|
25
|
-
return impliedProbs;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Step 5: Calculate the target total implied probabilities
|
|
30
|
-
const targetTotalImpliedProbs = 1 + targetSpread / 100;
|
|
31
|
-
|
|
32
|
-
// Step 6: Calculate the adjustment factor
|
|
33
|
-
const adjustmentFactor = targetTotalImpliedProbs / totalImpliedProbs;
|
|
34
|
-
|
|
35
|
-
// Step 7: Adjust the probabilities to reflect the target spread
|
|
36
|
-
let adjustedImpliedProbs = impliedProbs.map((prob) => prob * adjustmentFactor);
|
|
37
|
-
|
|
38
|
-
// Step 8: Check if any adjusted probability equals or exceeds 1
|
|
39
|
-
if (adjustedImpliedProbs.some((prob) => prob >= 1)) {
|
|
40
|
-
return Array(impliedProbs.length).fill(0);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Step 9: Ensure the sum of the adjusted probabilities equals the target total implied probabilities
|
|
44
|
-
const sumAdjustedProbs = adjustedImpliedProbs.reduce((sum, prob) => sum + prob, 0);
|
|
45
|
-
|
|
46
|
-
// Step 10: If the sum of the adjusted probabilities is less than 1, return zeros
|
|
47
|
-
if (sumAdjustedProbs < 1) {
|
|
48
|
-
return Array(impliedProbs.length).fill(0);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const normalizationFactor = targetTotalImpliedProbs / sumAdjustedProbs;
|
|
52
|
-
adjustedImpliedProbs = adjustedImpliedProbs.map((prob) => prob * normalizationFactor);
|
|
53
|
-
|
|
54
|
-
return adjustedImpliedProbs;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export const getSpreadData = (
|
|
58
|
-
spreadData: any[],
|
|
59
|
-
sportId: string,
|
|
60
|
-
typeId: number,
|
|
61
|
-
defaultSpreadForLiveMarkets: number
|
|
62
|
-
) => {
|
|
63
|
-
const sportSpreadData = spreadData.find(
|
|
64
|
-
(data) => Number(data.typeId) === Number(typeId) && Number(data.sportId) === Number(sportId)
|
|
65
|
-
);
|
|
66
|
-
if (sportSpreadData) {
|
|
67
|
-
return {
|
|
68
|
-
minSpread: sportSpreadData.minSpread ? Number(sportSpreadData.minSpread) : defaultSpreadForLiveMarkets,
|
|
69
|
-
targetSpread: sportSpreadData.targetSpread ? Number(sportSpreadData.targetSpread) : 0,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
return { minSpread: defaultSpreadForLiveMarkets, targetSpread: 0 };
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
export const adjustAddedSpread = (odds: number[], leagueInfo: LeagueConfigInfo[], typeId: number) => {
|
|
76
|
-
// Pack market odds for UI
|
|
77
|
-
return odds.map((probability) => {
|
|
78
|
-
if (probability != 0) {
|
|
79
|
-
const leagueInfoByTypeId = leagueInfo.find((league) => Number(league.typeId) === Number(typeId));
|
|
80
|
-
let finalProbability = probability;
|
|
81
|
-
|
|
82
|
-
if (probability < 0.95) {
|
|
83
|
-
if (leagueInfoByTypeId && Number(leagueInfoByTypeId.addedSpread)) {
|
|
84
|
-
finalProbability = (probability * (100 + Number(leagueInfoByTypeId.addedSpread))) / 100;
|
|
85
|
-
// edge case if added spread is bigger than 5%, it can happen that odd goes above 1, in that case return odd from api.
|
|
86
|
-
if (finalProbability >= 1) {
|
|
87
|
-
finalProbability = probability;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return finalProbability;
|
|
93
|
-
} else {
|
|
94
|
-
return 0;
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
};
|
|
1
|
+
import { LeagueConfigInfo } from '../types/sports';
|
|
2
|
+
|
|
3
|
+
export const adjustSpreadOnOdds = (impliedProbs: number[], minSpread: number, targetSpread: number) => {
|
|
4
|
+
// Step 1: Check if any implied probability is zero
|
|
5
|
+
if (impliedProbs.some((prob) => prob === 0)) {
|
|
6
|
+
return impliedProbs;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Step 2: Calculate the current total implied probabilities
|
|
10
|
+
const totalImpliedProbs = impliedProbs.reduce((sum, prob) => sum + prob, 0);
|
|
11
|
+
|
|
12
|
+
// Step 3: Check if the sum of implied probabilities is greater than 1
|
|
13
|
+
if (totalImpliedProbs <= 1) {
|
|
14
|
+
return Array(impliedProbs.length).fill(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Step 4: Check if targetSpread is zero
|
|
18
|
+
if (targetSpread === 0) {
|
|
19
|
+
const currentSpread = (totalImpliedProbs - 1) * 100;
|
|
20
|
+
// If minSpread is set and greater than current spread, use minSpread
|
|
21
|
+
if (minSpread > currentSpread) {
|
|
22
|
+
targetSpread = minSpread;
|
|
23
|
+
} else {
|
|
24
|
+
// If minSpread is less than current spread, return odds as they are
|
|
25
|
+
return impliedProbs;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Step 5: Calculate the target total implied probabilities
|
|
30
|
+
const targetTotalImpliedProbs = 1 + targetSpread / 100;
|
|
31
|
+
|
|
32
|
+
// Step 6: Calculate the adjustment factor
|
|
33
|
+
const adjustmentFactor = targetTotalImpliedProbs / totalImpliedProbs;
|
|
34
|
+
|
|
35
|
+
// Step 7: Adjust the probabilities to reflect the target spread
|
|
36
|
+
let adjustedImpliedProbs = impliedProbs.map((prob) => prob * adjustmentFactor);
|
|
37
|
+
|
|
38
|
+
// Step 8: Check if any adjusted probability equals or exceeds 1
|
|
39
|
+
if (adjustedImpliedProbs.some((prob) => prob >= 1)) {
|
|
40
|
+
return Array(impliedProbs.length).fill(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Step 9: Ensure the sum of the adjusted probabilities equals the target total implied probabilities
|
|
44
|
+
const sumAdjustedProbs = adjustedImpliedProbs.reduce((sum, prob) => sum + prob, 0);
|
|
45
|
+
|
|
46
|
+
// Step 10: If the sum of the adjusted probabilities is less than 1, return zeros
|
|
47
|
+
if (sumAdjustedProbs < 1) {
|
|
48
|
+
return Array(impliedProbs.length).fill(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const normalizationFactor = targetTotalImpliedProbs / sumAdjustedProbs;
|
|
52
|
+
adjustedImpliedProbs = adjustedImpliedProbs.map((prob) => prob * normalizationFactor);
|
|
53
|
+
|
|
54
|
+
return adjustedImpliedProbs;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const getSpreadData = (
|
|
58
|
+
spreadData: any[],
|
|
59
|
+
sportId: string,
|
|
60
|
+
typeId: number,
|
|
61
|
+
defaultSpreadForLiveMarkets: number
|
|
62
|
+
) => {
|
|
63
|
+
const sportSpreadData = spreadData.find(
|
|
64
|
+
(data) => Number(data.typeId) === Number(typeId) && Number(data.sportId) === Number(sportId)
|
|
65
|
+
);
|
|
66
|
+
if (sportSpreadData) {
|
|
67
|
+
return {
|
|
68
|
+
minSpread: sportSpreadData.minSpread ? Number(sportSpreadData.minSpread) : defaultSpreadForLiveMarkets,
|
|
69
|
+
targetSpread: sportSpreadData.targetSpread ? Number(sportSpreadData.targetSpread) : 0,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return { minSpread: defaultSpreadForLiveMarkets, targetSpread: 0 };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const adjustAddedSpread = (odds: number[], leagueInfo: LeagueConfigInfo[], typeId: number) => {
|
|
76
|
+
// Pack market odds for UI
|
|
77
|
+
return odds.map((probability) => {
|
|
78
|
+
if (probability != 0) {
|
|
79
|
+
const leagueInfoByTypeId = leagueInfo.find((league) => Number(league.typeId) === Number(typeId));
|
|
80
|
+
let finalProbability = probability;
|
|
81
|
+
|
|
82
|
+
if (probability < 0.95) {
|
|
83
|
+
if (leagueInfoByTypeId && Number(leagueInfoByTypeId.addedSpread)) {
|
|
84
|
+
finalProbability = (probability * (100 + Number(leagueInfoByTypeId.addedSpread))) / 100;
|
|
85
|
+
// edge case if added spread is bigger than 5%, it can happen that odd goes above 1, in that case return odd from api.
|
|
86
|
+
if (finalProbability >= 1) {
|
|
87
|
+
finalProbability = probability;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return finalProbability;
|
|
93
|
+
} else {
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
};
|