overtime-live-trading-utils 2.1.30 → 2.1.32
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/codecov.yml +20 -20
- package/index.ts +26 -26
- package/jest.config.ts +16 -16
- package/main.js +1 -1
- package/package.json +30 -30
- 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 -518
- 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 +935 -935
- 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 +96 -96
- 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 +918 -912
- package/src/utils/opticOdds.ts +71 -71
- package/src/utils/resolution.ts +291 -291
- package/src/utils/sports.ts +51 -51
- package/src/utils/spread.ts +97 -97
- package/tsconfig.json +16 -16
- package/webpack.config.js +24 -30
- package/CLAUDE.md +0 -77
- package/resolution_live_markets.md +0 -351
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
|
+
};
|
package/tsconfig.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"module": "esnext",
|
|
4
|
-
"esModuleInterop": true,
|
|
5
|
-
"target": "es5",
|
|
6
|
-
"lib": [
|
|
7
|
-
"dom",
|
|
8
|
-
"dom.iterable",
|
|
9
|
-
"esnext"
|
|
10
|
-
],
|
|
11
|
-
"moduleResolution": "node",
|
|
12
|
-
"sourceMap": true,
|
|
13
|
-
"outDir": "build",
|
|
14
|
-
"declaration": true,
|
|
15
|
-
"strict": true,
|
|
16
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "esnext",
|
|
4
|
+
"esModuleInterop": true,
|
|
5
|
+
"target": "es5",
|
|
6
|
+
"lib": [
|
|
7
|
+
"dom",
|
|
8
|
+
"dom.iterable",
|
|
9
|
+
"esnext"
|
|
10
|
+
],
|
|
11
|
+
"moduleResolution": "node",
|
|
12
|
+
"sourceMap": true,
|
|
13
|
+
"outDir": "build",
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"strict": true,
|
|
16
|
+
}
|
|
17
17
|
}
|
package/webpack.config.js
CHANGED
|
@@ -1,30 +1,24 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
|
-
|
|
3
|
-
module.exports = {
|
|
4
|
-
entry: './index.ts',
|
|
5
|
-
module: {
|
|
6
|
-
rules: [
|
|
7
|
-
{
|
|
8
|
-
test: /\.ts?$/,
|
|
9
|
-
use: 'ts-loader',
|
|
10
|
-
exclude: /node_modules/,
|
|
11
|
-
},
|
|
12
|
-
],
|
|
13
|
-
},
|
|
14
|
-
resolve: {
|
|
15
|
-
extensions: ['.tsx', '.ts', '.js'],
|
|
16
|
-
},
|
|
17
|
-
output: {
|
|
18
|
-
filename: 'main.js',
|
|
19
|
-
path: path.resolve(__dirname),
|
|
20
|
-
library:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
},
|
|
26
|
-
optimization: {
|
|
27
|
-
usedExports: false,
|
|
28
|
-
sideEffects: false,
|
|
29
|
-
},
|
|
30
|
-
};
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
entry: './index.ts',
|
|
5
|
+
module: {
|
|
6
|
+
rules: [
|
|
7
|
+
{
|
|
8
|
+
test: /\.ts?$/,
|
|
9
|
+
use: 'ts-loader',
|
|
10
|
+
exclude: /node_modules/,
|
|
11
|
+
},
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
resolve: {
|
|
15
|
+
extensions: ['.tsx', '.ts', '.js'],
|
|
16
|
+
},
|
|
17
|
+
output: {
|
|
18
|
+
filename: 'main.js',
|
|
19
|
+
path: path.resolve(__dirname),
|
|
20
|
+
library: 'thalesUtils',
|
|
21
|
+
libraryTarget: 'umd',
|
|
22
|
+
globalObject: 'this',
|
|
23
|
+
},
|
|
24
|
+
};
|
package/CLAUDE.md
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Project Overview
|
|
6
|
-
|
|
7
|
-
This is a TypeScript utility library for live sports betting/trading. It processes odds data from multiple bookmakers, applies spreads, validates odds consistency, and generates child markets for various bet types (moneyline, spreads, totals). The library is designed to work with the Overtime sports betting platform.
|
|
8
|
-
|
|
9
|
-
## Commands
|
|
10
|
-
|
|
11
|
-
### Build & Compile
|
|
12
|
-
- `npm run compile` - Compile TypeScript to JavaScript
|
|
13
|
-
- `npm run pack` - Build production bundle with webpack
|
|
14
|
-
|
|
15
|
-
### Testing
|
|
16
|
-
- `npm test` - Run all Jest tests
|
|
17
|
-
- `npm test -- --coverage` - Run tests with coverage report
|
|
18
|
-
- `npm test -- src/tests/unit/odds.test.ts` - Run a specific test file
|
|
19
|
-
|
|
20
|
-
### Development
|
|
21
|
-
The library is published to npm. Run `npm run prepublish` to pack before publishing (runs automatically on npm publish).
|
|
22
|
-
|
|
23
|
-
## Architecture
|
|
24
|
-
|
|
25
|
-
### Core Processing Flow
|
|
26
|
-
|
|
27
|
-
The main processing pipeline:
|
|
28
|
-
1. **Bookmaker Selection** (`src/utils/bookmakers.ts`) - Selects primary/secondary/tertiary bookmakers for a sport, validates odds consistency across providers
|
|
29
|
-
2. **Odds Processing** (`src/utils/odds.ts`) - Converts odds formats (decimal ↔ implied probability), filters by market type and bookmaker, creates parent moneyline odds
|
|
30
|
-
3. **Spread Adjustment** (`src/utils/spread.ts`) - Applies spread data to odds, handles Asian handicap, game/point/goal spreads, run/puck lines
|
|
31
|
-
4. **Market Generation** (`src/utils/markets.ts`) - Main `processMarket()` function orchestrates the pipeline: gets moneyline odds → applies spread → creates child markets
|
|
32
|
-
5. **Child Markets** (`src/utils/odds.ts`) - Generates derivative markets (spreads, totals, double chance, correct score) from parent moneyline odds
|
|
33
|
-
|
|
34
|
-
### Key Concepts
|
|
35
|
-
|
|
36
|
-
**League Configuration**: Each league (sport) has a configuration defining enabled bet types, min/max odds, and added spreads. The `LeagueConfigInfo` type stores this data, typically loaded from CSV files like `live-markets-map.csv`.
|
|
37
|
-
|
|
38
|
-
**Two vs Three-Positional Sports**: The library distinguishes between sports with 2 positions (tennis, esports - no draw) and 3 positions (soccer, hockey - draw possible). This affects odds validation and market generation.
|
|
39
|
-
|
|
40
|
-
**Odds Validation**: The `checkOddsFromBookmakers()` function compares odds from multiple bookmakers using implied probability differences to detect discrepancies that might indicate stale or incorrect data.
|
|
41
|
-
|
|
42
|
-
**OpticOdds Integration**: `src/utils/opticOdds.ts` provides utilities for working with the OpticOdds API format, including filtering odds by market name/bookmaker and handling team name matching.
|
|
43
|
-
|
|
44
|
-
### Type System
|
|
45
|
-
|
|
46
|
-
Key types in `src/types/`:
|
|
47
|
-
- `OddsObject` - Complete game data with odds array from API
|
|
48
|
-
- `Odds` - Array of individual odds entries with bookmaker, price, market name
|
|
49
|
-
- `LeagueConfigInfo` - League/market configuration (enabled status, min/max odds, spread)
|
|
50
|
-
- `ChildMarket` - Generated derivative market with line and odds
|
|
51
|
-
- `Fixture` & `ScoresObject` - Game metadata and live scores
|
|
52
|
-
|
|
53
|
-
### Sports-Specific Logic
|
|
54
|
-
|
|
55
|
-
`src/constants/sports.ts` defines leagues without formal home/away designation (esports, tennis). `src/enums/sports.ts` defines comprehensive tennis subleague mappings and market type enums (`MoneylineTypes`, `SpreadTypes`, `TotalTypes`).
|
|
56
|
-
|
|
57
|
-
### Error Handling
|
|
58
|
-
|
|
59
|
-
Error messages in `src/constants/errors.ts`:
|
|
60
|
-
- `NO_MATCHING_BOOKMAKERS_MESSAGE` - None of the specified bookmakers have odds
|
|
61
|
-
- `DIFF_BETWEEN_BOOKMAKERS_MESSAGE` - Bookmaker odds diverge beyond threshold
|
|
62
|
-
- `ZERO_ODDS_MESSAGE` - Zero odds detected (invalid)
|
|
63
|
-
- `ZERO_ODDS_AFTER_SPREAD_ADJUSTMENT` - Spread adjustment resulted in zero odds
|
|
64
|
-
|
|
65
|
-
## Testing Notes
|
|
66
|
-
|
|
67
|
-
Tests use mock data from `src/tests/mock/`:
|
|
68
|
-
- `MockLeagueMap.ts` - League configuration fixtures
|
|
69
|
-
- `MockOpticSoccer.ts` - Sample OpticOdds API responses
|
|
70
|
-
- `MockSoccerRedis.ts` - Redis-cached odds data
|
|
71
|
-
|
|
72
|
-
Test files are organized by utility module: `odds.test.ts`, `sports.test.ts`, `spread.test.ts`, `markets.test.ts`, `bookmakers.test.ts`.
|
|
73
|
-
|
|
74
|
-
## Dependencies
|
|
75
|
-
|
|
76
|
-
- `oddslib` - Odds format conversion (decimal, moneyline, implied probability)
|
|
77
|
-
- `overtime-utils` - Shared types and enums for the Overtime platform (League enum, MarketType)
|