@gotza02/sequential-thinking 10000.0.8 → 10000.1.0
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/README.md +249 -39
- package/dist/dashboard/server.d.ts +79 -2
- package/dist/dashboard/server.js +466 -61
- package/dist/index.js +1 -4
- package/dist/tools/sports/core/alert-manager.d.ts +145 -0
- package/dist/tools/sports/core/alert-manager.js +380 -0
- package/dist/tools/sports/core/cache.d.ts +19 -8
- package/dist/tools/sports/core/cache.js +95 -38
- package/dist/tools/sports/core/circuit-breaker.d.ts +40 -0
- package/dist/tools/sports/core/circuit-breaker.js +99 -0
- package/dist/tools/sports/core/constants.d.ts +63 -4
- package/dist/tools/sports/core/constants.js +86 -11
- package/dist/tools/sports/core/data-quality.d.ts +80 -0
- package/dist/tools/sports/core/data-quality.js +460 -0
- package/dist/tools/sports/core/historical-analyzer.d.ts +108 -0
- package/dist/tools/sports/core/historical-analyzer.js +461 -0
- package/dist/tools/sports/core/index.d.ts +13 -0
- package/dist/tools/sports/core/index.js +16 -0
- package/dist/tools/sports/core/ml-prediction.d.ts +134 -0
- package/dist/tools/sports/core/ml-prediction.js +402 -0
- package/dist/tools/sports/core/realtime-manager.d.ts +102 -0
- package/dist/tools/sports/core/realtime-manager.js +331 -0
- package/dist/tools/sports/core/retry.d.ts +29 -0
- package/dist/tools/sports/core/retry.js +77 -0
- package/dist/tools/sports/core/types.d.ts +40 -1
- package/package.json +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DATA QUALITY VALIDATOR
|
|
3
|
+
* Validates and scores data quality for football matches
|
|
4
|
+
*/
|
|
5
|
+
import type { Match, Team, MatchOdds } from './types.js';
|
|
6
|
+
export interface DataQualityReport {
|
|
7
|
+
overall: number;
|
|
8
|
+
completeness: number;
|
|
9
|
+
accuracy: number;
|
|
10
|
+
freshness: number;
|
|
11
|
+
consistency: number;
|
|
12
|
+
issues: DataQualityIssue[];
|
|
13
|
+
warnings: DataQualityIssue[];
|
|
14
|
+
suggestions: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface DataQualityIssue {
|
|
17
|
+
field: string;
|
|
18
|
+
severity: 'error' | 'warning' | 'info';
|
|
19
|
+
message: string;
|
|
20
|
+
suggestion?: string;
|
|
21
|
+
value?: any;
|
|
22
|
+
}
|
|
23
|
+
export declare class DataQualityValidator {
|
|
24
|
+
private knownTeams;
|
|
25
|
+
private knownLeagues;
|
|
26
|
+
constructor();
|
|
27
|
+
/**
|
|
28
|
+
* Validate match data and return quality report
|
|
29
|
+
*/
|
|
30
|
+
validateMatchData(data: Partial<Match>): DataQualityReport;
|
|
31
|
+
/**
|
|
32
|
+
* Validate team data
|
|
33
|
+
*/
|
|
34
|
+
validateTeamData(data: Partial<Team>): DataQualityReport;
|
|
35
|
+
/**
|
|
36
|
+
* Validate odds data
|
|
37
|
+
*/
|
|
38
|
+
validateOddsData(odds: MatchOdds): DataQualityReport;
|
|
39
|
+
/**
|
|
40
|
+
* Check data completeness
|
|
41
|
+
*/
|
|
42
|
+
private checkCompleteness;
|
|
43
|
+
/**
|
|
44
|
+
* Check data accuracy
|
|
45
|
+
*/
|
|
46
|
+
private checkAccuracy;
|
|
47
|
+
/**
|
|
48
|
+
* Check data freshness
|
|
49
|
+
*/
|
|
50
|
+
private checkFreshness;
|
|
51
|
+
/**
|
|
52
|
+
* Check data consistency
|
|
53
|
+
*/
|
|
54
|
+
private checkConsistency;
|
|
55
|
+
/**
|
|
56
|
+
* Validate team stats
|
|
57
|
+
*/
|
|
58
|
+
private validateTeamStats;
|
|
59
|
+
/**
|
|
60
|
+
* Generate improvement suggestions
|
|
61
|
+
*/
|
|
62
|
+
private generateSuggestions;
|
|
63
|
+
/**
|
|
64
|
+
* Calculate freshness score from timestamp
|
|
65
|
+
*/
|
|
66
|
+
private calculateFreshness;
|
|
67
|
+
/**
|
|
68
|
+
* Calculate overall score from issues and warnings
|
|
69
|
+
*/
|
|
70
|
+
private calculateScore;
|
|
71
|
+
/**
|
|
72
|
+
* Quick validation - returns true if data passes basic checks
|
|
73
|
+
*/
|
|
74
|
+
isValid(data: Partial<Match>): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Get quality grade (A-F)
|
|
77
|
+
*/
|
|
78
|
+
getGrade(score: number): string;
|
|
79
|
+
}
|
|
80
|
+
export declare function getDataQualityValidator(): DataQualityValidator;
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DATA QUALITY VALIDATOR
|
|
3
|
+
* Validates and scores data quality for football matches
|
|
4
|
+
*/
|
|
5
|
+
import { logger } from '../../../utils.js';
|
|
6
|
+
export class DataQualityValidator {
|
|
7
|
+
knownTeams = new Set();
|
|
8
|
+
knownLeagues = new Set();
|
|
9
|
+
constructor() {
|
|
10
|
+
// Initialize known leagues
|
|
11
|
+
this.knownLeagues.add('Premier League');
|
|
12
|
+
this.knownLeagues.add('La Liga');
|
|
13
|
+
this.knownLeagues.add('Bundesliga');
|
|
14
|
+
this.knownLeagues.add('Serie A');
|
|
15
|
+
this.knownLeagues.add('Ligue 1');
|
|
16
|
+
this.knownLeagues.add('Champions League');
|
|
17
|
+
this.knownLeagues.add('Europa League');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Validate match data and return quality report
|
|
21
|
+
*/
|
|
22
|
+
validateMatchData(data) {
|
|
23
|
+
const issues = [];
|
|
24
|
+
const warnings = [];
|
|
25
|
+
const suggestions = [];
|
|
26
|
+
// Check completeness
|
|
27
|
+
const completeness = this.checkCompleteness(data, issues);
|
|
28
|
+
// Check accuracy
|
|
29
|
+
const accuracy = this.checkAccuracy(data, issues, warnings);
|
|
30
|
+
// Check freshness
|
|
31
|
+
const freshness = this.checkFreshness(data, warnings);
|
|
32
|
+
// Check consistency
|
|
33
|
+
const consistency = this.checkConsistency(data, issues, warnings);
|
|
34
|
+
// Generate suggestions
|
|
35
|
+
this.generateSuggestions(data, suggestions);
|
|
36
|
+
// Calculate overall score
|
|
37
|
+
const overall = Math.round((completeness + accuracy + freshness + consistency) / 4);
|
|
38
|
+
const report = {
|
|
39
|
+
overall,
|
|
40
|
+
completeness,
|
|
41
|
+
accuracy,
|
|
42
|
+
freshness,
|
|
43
|
+
consistency,
|
|
44
|
+
issues: issues.filter(i => i.severity === 'error'),
|
|
45
|
+
warnings: [...issues.filter(i => i.severity === 'warning'), ...warnings],
|
|
46
|
+
suggestions,
|
|
47
|
+
};
|
|
48
|
+
logger.debug(`[DataQuality] Match ${data.id}: Overall=${overall}, Completeness=${completeness}, Accuracy=${accuracy}`);
|
|
49
|
+
return report;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Validate team data
|
|
53
|
+
*/
|
|
54
|
+
validateTeamData(data) {
|
|
55
|
+
const issues = [];
|
|
56
|
+
const warnings = [];
|
|
57
|
+
const suggestions = [];
|
|
58
|
+
// Check required fields
|
|
59
|
+
if (!data.name) {
|
|
60
|
+
issues.push({
|
|
61
|
+
field: 'name',
|
|
62
|
+
severity: 'error',
|
|
63
|
+
message: 'Team name is required',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (!data.country) {
|
|
67
|
+
warnings.push({
|
|
68
|
+
field: 'country',
|
|
69
|
+
severity: 'warning',
|
|
70
|
+
message: 'Team country is missing',
|
|
71
|
+
suggestion: 'Add country information for better filtering',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// Validate stats if present
|
|
75
|
+
if (data.stats) {
|
|
76
|
+
this.validateTeamStats(data.stats, issues, warnings);
|
|
77
|
+
}
|
|
78
|
+
const completeness = this.calculateScore(issues, warnings, 3);
|
|
79
|
+
const accuracy = 100; // Simplified
|
|
80
|
+
const freshness = 100;
|
|
81
|
+
const consistency = 100;
|
|
82
|
+
return {
|
|
83
|
+
overall: Math.round((completeness + accuracy + freshness + consistency) / 4),
|
|
84
|
+
completeness,
|
|
85
|
+
accuracy,
|
|
86
|
+
freshness,
|
|
87
|
+
consistency,
|
|
88
|
+
issues: issues.filter(i => i.severity === 'error'),
|
|
89
|
+
warnings: [...issues.filter(i => i.severity === 'warning'), ...warnings],
|
|
90
|
+
suggestions,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Validate odds data
|
|
95
|
+
*/
|
|
96
|
+
validateOddsData(odds) {
|
|
97
|
+
const issues = [];
|
|
98
|
+
const warnings = [];
|
|
99
|
+
// Check required fields
|
|
100
|
+
if (!odds.homeWin || odds.homeWin <= 1) {
|
|
101
|
+
issues.push({
|
|
102
|
+
field: 'homeWin',
|
|
103
|
+
severity: 'error',
|
|
104
|
+
message: `Invalid home win odds: ${odds.homeWin}`,
|
|
105
|
+
value: odds.homeWin,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (!odds.draw || odds.draw <= 1) {
|
|
109
|
+
issues.push({
|
|
110
|
+
field: 'draw',
|
|
111
|
+
severity: 'error',
|
|
112
|
+
message: `Invalid draw odds: ${odds.draw}`,
|
|
113
|
+
value: odds.draw,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (!odds.awayWin || odds.awayWin <= 1) {
|
|
117
|
+
issues.push({
|
|
118
|
+
field: 'awayWin',
|
|
119
|
+
severity: 'error',
|
|
120
|
+
message: `Invalid away win odds: ${odds.awayWin}`,
|
|
121
|
+
value: odds.awayWin,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// Check implied probability (overround)
|
|
125
|
+
const impliedProb = (1 / odds.homeWin) + (1 / odds.draw) + (1 / odds.awayWin);
|
|
126
|
+
if (impliedProb < 1 || impliedProb > 1.5) {
|
|
127
|
+
warnings.push({
|
|
128
|
+
field: 'odds',
|
|
129
|
+
severity: 'warning',
|
|
130
|
+
message: `Unusual implied probability: ${(impliedProb * 100).toFixed(1)}%`,
|
|
131
|
+
suggestion: 'Normal range is 100-115%',
|
|
132
|
+
value: impliedProb,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// Check for arbitrage opportunity (unlikely but possible error)
|
|
136
|
+
if (impliedProb < 0.95) {
|
|
137
|
+
issues.push({
|
|
138
|
+
field: 'odds',
|
|
139
|
+
severity: 'error',
|
|
140
|
+
message: 'Possible arbitrage opportunity detected - verify odds accuracy',
|
|
141
|
+
value: impliedProb,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const completeness = this.calculateScore(issues, warnings, 3);
|
|
145
|
+
return {
|
|
146
|
+
overall: completeness,
|
|
147
|
+
completeness,
|
|
148
|
+
accuracy: completeness,
|
|
149
|
+
freshness: odds.timestamp ? this.calculateFreshness(odds.timestamp) : 50,
|
|
150
|
+
consistency: 100,
|
|
151
|
+
issues: issues.filter(i => i.severity === 'error'),
|
|
152
|
+
warnings: [...issues.filter(i => i.severity === 'warning'), ...warnings],
|
|
153
|
+
suggestions: [],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Check data completeness
|
|
158
|
+
*/
|
|
159
|
+
checkCompleteness(data, issues) {
|
|
160
|
+
const requiredFields = [
|
|
161
|
+
{ key: 'id', weight: 15 },
|
|
162
|
+
{ key: 'homeTeam', weight: 15 },
|
|
163
|
+
{ key: 'awayTeam', weight: 15 },
|
|
164
|
+
{ key: 'date', weight: 15 },
|
|
165
|
+
{ key: 'status', weight: 10 },
|
|
166
|
+
{ key: 'league', weight: 10 },
|
|
167
|
+
];
|
|
168
|
+
const optionalFields = [
|
|
169
|
+
{ key: 'score', weight: 5 },
|
|
170
|
+
{ key: 'odds', weight: 5 },
|
|
171
|
+
{ key: 'lineups', weight: 3 },
|
|
172
|
+
{ key: 'events', weight: 3 },
|
|
173
|
+
{ key: 'venue', weight: 2 },
|
|
174
|
+
{ key: 'referee', weight: 2 },
|
|
175
|
+
];
|
|
176
|
+
let score = 0;
|
|
177
|
+
requiredFields.forEach(({ key, weight }) => {
|
|
178
|
+
const value = data[key];
|
|
179
|
+
if (value !== undefined && value !== null) {
|
|
180
|
+
score += weight;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
issues.push({
|
|
184
|
+
field: key,
|
|
185
|
+
severity: 'error',
|
|
186
|
+
message: `Required field '${key}' is missing`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
optionalFields.forEach(({ key, weight }) => {
|
|
191
|
+
const value = data[key];
|
|
192
|
+
if (value !== undefined && value !== null) {
|
|
193
|
+
score += weight;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
return score;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Check data accuracy
|
|
200
|
+
*/
|
|
201
|
+
checkAccuracy(data, issues, warnings) {
|
|
202
|
+
let score = 100;
|
|
203
|
+
// Check score consistency with status
|
|
204
|
+
if (data.score && data.status === 'scheduled') {
|
|
205
|
+
score -= 15;
|
|
206
|
+
warnings.push({
|
|
207
|
+
field: 'score',
|
|
208
|
+
severity: 'warning',
|
|
209
|
+
message: 'Match has score but status is "scheduled"',
|
|
210
|
+
suggestion: 'Update status to "live" or "finished"',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
// Check for negative scores
|
|
214
|
+
if (data.score) {
|
|
215
|
+
if (data.score.home < 0 || data.score.away < 0) {
|
|
216
|
+
score -= 20;
|
|
217
|
+
issues.push({
|
|
218
|
+
field: 'score',
|
|
219
|
+
severity: 'error',
|
|
220
|
+
message: 'Negative score detected',
|
|
221
|
+
value: data.score,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
// Check for unrealistic scores
|
|
225
|
+
if (data.score.home > 15 || data.score.away > 15) {
|
|
226
|
+
score -= 10;
|
|
227
|
+
warnings.push({
|
|
228
|
+
field: 'score',
|
|
229
|
+
severity: 'warning',
|
|
230
|
+
message: 'Unusually high score detected',
|
|
231
|
+
value: `${data.score.home}-${data.score.away}`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Check date validity
|
|
236
|
+
if (data.date) {
|
|
237
|
+
const matchDate = new Date(data.date);
|
|
238
|
+
const now = new Date();
|
|
239
|
+
const oneYearAgo = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
|
|
240
|
+
const oneYearFromNow = new Date(now.getFullYear() + 1, now.getMonth(), now.getDate());
|
|
241
|
+
if (matchDate < oneYearAgo || matchDate > oneYearFromNow) {
|
|
242
|
+
score -= 10;
|
|
243
|
+
warnings.push({
|
|
244
|
+
field: 'date',
|
|
245
|
+
severity: 'warning',
|
|
246
|
+
message: 'Match date is outside expected range',
|
|
247
|
+
value: data.date,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Check team names
|
|
252
|
+
if (data.homeTeam && data.awayTeam) {
|
|
253
|
+
if (data.homeTeam.name === data.awayTeam.name) {
|
|
254
|
+
score -= 20;
|
|
255
|
+
issues.push({
|
|
256
|
+
field: 'teams',
|
|
257
|
+
severity: 'error',
|
|
258
|
+
message: 'Home and away teams have the same name',
|
|
259
|
+
value: data.homeTeam.name,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return Math.max(0, score);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Check data freshness
|
|
267
|
+
*/
|
|
268
|
+
checkFreshness(data, warnings) {
|
|
269
|
+
if (!data.date)
|
|
270
|
+
return 50;
|
|
271
|
+
const matchDate = new Date(data.date).getTime();
|
|
272
|
+
const now = Date.now();
|
|
273
|
+
const age = now - matchDate;
|
|
274
|
+
// For live matches, data should be very fresh
|
|
275
|
+
if (data.status === 'live') {
|
|
276
|
+
// Live data older than 2 minutes is stale
|
|
277
|
+
if (age > 2 * 60 * 1000) {
|
|
278
|
+
warnings.push({
|
|
279
|
+
field: 'freshness',
|
|
280
|
+
severity: 'warning',
|
|
281
|
+
message: 'Live match data is stale (>2 minutes old)',
|
|
282
|
+
suggestion: 'Refresh data from source',
|
|
283
|
+
});
|
|
284
|
+
return 50;
|
|
285
|
+
}
|
|
286
|
+
return 100;
|
|
287
|
+
}
|
|
288
|
+
// For scheduled matches, freshness is less critical
|
|
289
|
+
if (data.status === 'scheduled') {
|
|
290
|
+
return 100;
|
|
291
|
+
}
|
|
292
|
+
// For finished matches, check if recently completed
|
|
293
|
+
if (data.status === 'finished') {
|
|
294
|
+
// Match finished within last hour
|
|
295
|
+
if (age < 60 * 60 * 1000) {
|
|
296
|
+
return 100;
|
|
297
|
+
}
|
|
298
|
+
// Match finished within last day
|
|
299
|
+
if (age < 24 * 60 * 60 * 1000) {
|
|
300
|
+
return 80;
|
|
301
|
+
}
|
|
302
|
+
return 60;
|
|
303
|
+
}
|
|
304
|
+
return 70;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Check data consistency
|
|
308
|
+
*/
|
|
309
|
+
checkConsistency(data, issues, warnings) {
|
|
310
|
+
let score = 100;
|
|
311
|
+
// Check team stats consistency
|
|
312
|
+
if (data.homeTeam?.stats && data.awayTeam?.stats) {
|
|
313
|
+
const homeStats = data.homeTeam.stats;
|
|
314
|
+
const awayStats = data.awayTeam.stats;
|
|
315
|
+
// Check if matches played is consistent
|
|
316
|
+
if (homeStats.matchesPlayed !== awayStats.matchesPlayed &&
|
|
317
|
+
data.league?.name !== 'Champions League' &&
|
|
318
|
+
data.league?.name !== 'Europa League') {
|
|
319
|
+
// This might be OK for cup competitions
|
|
320
|
+
warnings.push({
|
|
321
|
+
field: 'stats',
|
|
322
|
+
severity: 'info',
|
|
323
|
+
message: 'Teams have different number of matches played',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Check score consistency
|
|
328
|
+
if (data.score && data.status === 'finished') {
|
|
329
|
+
const totalGoals = data.score.home + data.score.away;
|
|
330
|
+
// Very high scoring match - verify
|
|
331
|
+
if (totalGoals > 10) {
|
|
332
|
+
score -= 5;
|
|
333
|
+
warnings.push({
|
|
334
|
+
field: 'score',
|
|
335
|
+
severity: 'info',
|
|
336
|
+
message: `High scoring match: ${totalGoals} total goals`,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return Math.max(0, score);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Validate team stats
|
|
344
|
+
*/
|
|
345
|
+
validateTeamStats(stats, issues, warnings) {
|
|
346
|
+
// Check matches played
|
|
347
|
+
if (stats.matchesPlayed < 0) {
|
|
348
|
+
issues.push({
|
|
349
|
+
field: 'matchesPlayed',
|
|
350
|
+
severity: 'error',
|
|
351
|
+
message: 'Matches played cannot be negative',
|
|
352
|
+
value: stats.matchesPlayed,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
// Check wins + draws + losses = matches played
|
|
356
|
+
const totalResults = stats.wins + stats.draws + stats.losses;
|
|
357
|
+
if (totalResults !== stats.matchesPlayed && stats.matchesPlayed > 0) {
|
|
358
|
+
warnings.push({
|
|
359
|
+
field: 'stats',
|
|
360
|
+
severity: 'warning',
|
|
361
|
+
message: `Wins+Draws+Losses (${totalResults}) != Matches Played (${stats.matchesPlayed})`,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
// Check goal difference
|
|
365
|
+
const calculatedGD = stats.goalsFor - stats.goalsAgainst;
|
|
366
|
+
if (stats.goalDifference !== undefined && stats.goalDifference !== calculatedGD) {
|
|
367
|
+
warnings.push({
|
|
368
|
+
field: 'goalDifference',
|
|
369
|
+
severity: 'warning',
|
|
370
|
+
message: `Goal difference mismatch: ${stats.goalDifference} vs calculated ${calculatedGD}`,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
// Check points
|
|
374
|
+
const calculatedPoints = (stats.wins * 3) + stats.draws;
|
|
375
|
+
if (stats.points !== undefined && stats.points !== calculatedPoints) {
|
|
376
|
+
warnings.push({
|
|
377
|
+
field: 'points',
|
|
378
|
+
severity: 'warning',
|
|
379
|
+
message: `Points mismatch: ${stats.points} vs calculated ${calculatedPoints}`,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Generate improvement suggestions
|
|
385
|
+
*/
|
|
386
|
+
generateSuggestions(data, suggestions) {
|
|
387
|
+
if (!data.odds) {
|
|
388
|
+
suggestions.push('Add betting odds for value analysis');
|
|
389
|
+
}
|
|
390
|
+
if (!data.homeLineup && !data.awayLineup) {
|
|
391
|
+
suggestions.push('Add team lineups for better predictions');
|
|
392
|
+
}
|
|
393
|
+
if (!data.homeTeam?.stats?.xG && !data.awayTeam?.stats?.xG) {
|
|
394
|
+
suggestions.push('Add expected goals (xG) data for advanced analysis');
|
|
395
|
+
}
|
|
396
|
+
if (!data.referee) {
|
|
397
|
+
suggestions.push('Add referee information for bias analysis');
|
|
398
|
+
}
|
|
399
|
+
if (!data.venue) {
|
|
400
|
+
suggestions.push('Add venue information for home advantage analysis');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Calculate freshness score from timestamp
|
|
405
|
+
*/
|
|
406
|
+
calculateFreshness(timestamp) {
|
|
407
|
+
const ts = timestamp instanceof Date ? timestamp.getTime() : timestamp;
|
|
408
|
+
const age = Date.now() - ts;
|
|
409
|
+
if (age < 60000)
|
|
410
|
+
return 100; // < 1 minute
|
|
411
|
+
if (age < 300000)
|
|
412
|
+
return 90; // < 5 minutes
|
|
413
|
+
if (age < 600000)
|
|
414
|
+
return 80; // < 10 minutes
|
|
415
|
+
if (age < 1800000)
|
|
416
|
+
return 60; // < 30 minutes
|
|
417
|
+
if (age < 3600000)
|
|
418
|
+
return 40; // < 1 hour
|
|
419
|
+
return 20;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Calculate overall score from issues and warnings
|
|
423
|
+
*/
|
|
424
|
+
calculateScore(errors, warnings, maxFields) {
|
|
425
|
+
const errorWeight = 20;
|
|
426
|
+
const warningWeight = 5;
|
|
427
|
+
const errorDeduction = errors.filter(i => i.severity === 'error').length * errorWeight;
|
|
428
|
+
const warningDeduction = warnings.length * warningWeight;
|
|
429
|
+
return Math.max(0, 100 - errorDeduction - warningDeduction);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Quick validation - returns true if data passes basic checks
|
|
433
|
+
*/
|
|
434
|
+
isValid(data) {
|
|
435
|
+
const report = this.validateMatchData(data);
|
|
436
|
+
return report.issues.length === 0 && report.overall >= 70;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Get quality grade (A-F)
|
|
440
|
+
*/
|
|
441
|
+
getGrade(score) {
|
|
442
|
+
if (score >= 90)
|
|
443
|
+
return 'A';
|
|
444
|
+
if (score >= 80)
|
|
445
|
+
return 'B';
|
|
446
|
+
if (score >= 70)
|
|
447
|
+
return 'C';
|
|
448
|
+
if (score >= 60)
|
|
449
|
+
return 'D';
|
|
450
|
+
return 'F';
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// Singleton instance
|
|
454
|
+
let globalValidator = null;
|
|
455
|
+
export function getDataQualityValidator() {
|
|
456
|
+
if (!globalValidator) {
|
|
457
|
+
globalValidator = new DataQualityValidator();
|
|
458
|
+
}
|
|
459
|
+
return globalValidator;
|
|
460
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HISTORICAL ANALYZER
|
|
3
|
+
* Analyzes historical data for patterns and trends
|
|
4
|
+
*/
|
|
5
|
+
import type { Match, HeadToHead, HistoricalPattern, TeamHistoricalPerformance } from './types.js';
|
|
6
|
+
export interface HistoricalQuery {
|
|
7
|
+
team?: string;
|
|
8
|
+
league?: string;
|
|
9
|
+
dateFrom?: Date;
|
|
10
|
+
dateTo?: Date;
|
|
11
|
+
opponent?: string;
|
|
12
|
+
venue?: 'home' | 'away' | 'neutral';
|
|
13
|
+
minOdds?: number;
|
|
14
|
+
maxOdds?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface TrendAnalysis {
|
|
17
|
+
direction: 'up' | 'down' | 'stable';
|
|
18
|
+
strength: number;
|
|
19
|
+
confidence: number;
|
|
20
|
+
dataPoints: number;
|
|
21
|
+
description: string;
|
|
22
|
+
}
|
|
23
|
+
export interface PerformanceMetrics {
|
|
24
|
+
winRate: number;
|
|
25
|
+
drawRate: number;
|
|
26
|
+
lossRate: number;
|
|
27
|
+
avgGoalsFor: number;
|
|
28
|
+
avgGoalsAgainst: number;
|
|
29
|
+
cleanSheetRate: number;
|
|
30
|
+
bothTeamsScoreRate: number;
|
|
31
|
+
over25Rate: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Historical Analyzer
|
|
35
|
+
* Analyzes past matches for patterns and predictive insights
|
|
36
|
+
*/
|
|
37
|
+
export declare class HistoricalAnalyzer {
|
|
38
|
+
private matchHistory;
|
|
39
|
+
private patterns;
|
|
40
|
+
constructor();
|
|
41
|
+
/**
|
|
42
|
+
* Analyze team performance over time
|
|
43
|
+
*/
|
|
44
|
+
analyzeTeamPerformance(teamId: string, query?: HistoricalQuery): TeamHistoricalPerformance;
|
|
45
|
+
/**
|
|
46
|
+
* Analyze head-to-head history
|
|
47
|
+
*/
|
|
48
|
+
analyzeHeadToHead(teamAId: string, teamBId: string, limit?: number): HeadToHead;
|
|
49
|
+
/**
|
|
50
|
+
* Analyze trends for a team
|
|
51
|
+
*/
|
|
52
|
+
analyzeTrends(teamId: string, windowSize?: number): TrendAnalysis;
|
|
53
|
+
/**
|
|
54
|
+
* Calculate performance metrics
|
|
55
|
+
*/
|
|
56
|
+
calculateMetrics(teamId: string, query?: HistoricalQuery): PerformanceMetrics;
|
|
57
|
+
/**
|
|
58
|
+
* Find betting patterns
|
|
59
|
+
*/
|
|
60
|
+
findBettingPatterns(query: HistoricalQuery): HistoricalPattern[];
|
|
61
|
+
/**
|
|
62
|
+
* Analyze referee bias
|
|
63
|
+
*/
|
|
64
|
+
analyzeRefereeBias(refereeName: string, teamId?: string): {
|
|
65
|
+
referee: string;
|
|
66
|
+
matches: number;
|
|
67
|
+
avgCards: number;
|
|
68
|
+
homeWinRate: number;
|
|
69
|
+
teamBias?: number;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Analyze weather impact
|
|
73
|
+
*/
|
|
74
|
+
analyzeWeatherImpact(condition: string, teamId?: string): {
|
|
75
|
+
condition: string;
|
|
76
|
+
matches: number;
|
|
77
|
+
avgGoals: number;
|
|
78
|
+
homeWinRate: number;
|
|
79
|
+
teamPerformance?: number;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Get similar matches
|
|
83
|
+
*/
|
|
84
|
+
findSimilarMatches(match: Match, limit?: number): Match[];
|
|
85
|
+
/**
|
|
86
|
+
* Calculate similarity between two matches
|
|
87
|
+
*/
|
|
88
|
+
private calculateSimilarity;
|
|
89
|
+
private getTeamMatches;
|
|
90
|
+
private getH2HMatches;
|
|
91
|
+
private isWin;
|
|
92
|
+
private isDraw;
|
|
93
|
+
private isLoss;
|
|
94
|
+
private getGoalsFor;
|
|
95
|
+
private getGoalsAgainst;
|
|
96
|
+
private getMatchPoints;
|
|
97
|
+
private calculateHomeAdvantage;
|
|
98
|
+
private findPatterns;
|
|
99
|
+
private calculateProfit;
|
|
100
|
+
private queryMatches;
|
|
101
|
+
private createEmptyPerformance;
|
|
102
|
+
private loadHistoricalData;
|
|
103
|
+
/**
|
|
104
|
+
* Add match to history
|
|
105
|
+
*/
|
|
106
|
+
addMatch(match: Match): void;
|
|
107
|
+
}
|
|
108
|
+
export declare function getHistoricalAnalyzer(): HistoricalAnalyzer;
|