@gotza02/sequential-thinking 10000.1.2 → 10000.1.4
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 +39 -249
- package/dist/dashboard/server.d.ts +2 -45
- package/dist/dashboard/server.js +64 -250
- package/dist/index.js +4 -1
- package/dist/tools/sports/core/base.d.ts +31 -1
- package/dist/tools/sports/core/base.js +25 -0
- package/dist/tools/sports/core/cache.d.ts +8 -19
- package/dist/tools/sports/core/cache.js +38 -95
- package/dist/tools/sports/core/constants.d.ts +4 -63
- package/dist/tools/sports/core/constants.js +11 -86
- package/dist/tools/sports/core/types.d.ts +146 -34
- package/dist/tools/sports/providers/api.d.ts +12 -1
- package/dist/tools/sports/providers/api.js +170 -0
- package/dist/tools/sports/tools/match.d.ts +5 -0
- package/dist/tools/sports/tools/match.js +530 -5
- package/package.json +1 -1
- package/dist/tools/sports/core/alert-manager.d.ts +0 -96
- package/dist/tools/sports/core/alert-manager.js +0 -319
- package/dist/tools/sports/core/circuit-breaker.d.ts +0 -40
- package/dist/tools/sports/core/circuit-breaker.js +0 -99
- package/dist/tools/sports/core/data-quality.d.ts +0 -36
- package/dist/tools/sports/core/data-quality.js +0 -243
- package/dist/tools/sports/core/historical-analyzer.d.ts +0 -54
- package/dist/tools/sports/core/historical-analyzer.js +0 -261
- package/dist/tools/sports/core/index.d.ts +0 -13
- package/dist/tools/sports/core/index.js +0 -16
- package/dist/tools/sports/core/ml-prediction.d.ts +0 -76
- package/dist/tools/sports/core/ml-prediction.js +0 -260
- package/dist/tools/sports/core/realtime-manager.d.ts +0 -51
- package/dist/tools/sports/core/realtime-manager.js +0 -222
- package/dist/tools/sports/core/retry.d.ts +0 -29
- package/dist/tools/sports/core/retry.js +0 -77
|
@@ -9,6 +9,7 @@ import { scrapeMatchContent, findBestMatchUrl, findBestNewsUrl } from '../provid
|
|
|
9
9
|
import { getGlobalCache, CacheService } from '../core/cache.js';
|
|
10
10
|
import { formatMatchesTable, formatScore, formatMatchStatus } from '../utils/formatter.js';
|
|
11
11
|
import { CACHE_CONFIG, LEAGUES } from '../core/constants.js';
|
|
12
|
+
import { logger } from '../../../utils.js';
|
|
12
13
|
// ============= Helper Functions =============
|
|
13
14
|
/**
|
|
14
15
|
* Get date context for search queries
|
|
@@ -62,6 +63,200 @@ async function findMatchId(homeTeam, awayTeam, league) {
|
|
|
62
63
|
}
|
|
63
64
|
return null;
|
|
64
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Fetch comprehensive API data for a match
|
|
68
|
+
*/
|
|
69
|
+
async function fetchAPIMatchData(api, matchId, homeTeam, awayTeam) {
|
|
70
|
+
try {
|
|
71
|
+
// Fetch all data in parallel
|
|
72
|
+
const [matchResult, h2hResult, oddsResult] = await Promise.all([
|
|
73
|
+
api.getMatch(matchId),
|
|
74
|
+
api.getH2H(matchId),
|
|
75
|
+
api.getMatchOdds(matchId),
|
|
76
|
+
]);
|
|
77
|
+
let homeForm;
|
|
78
|
+
let awayForm;
|
|
79
|
+
// Get team form if we have match data
|
|
80
|
+
if (matchResult.success && matchResult.data) {
|
|
81
|
+
const [homeFormResult, awayFormResult] = await Promise.all([
|
|
82
|
+
api.getTeamForm(matchResult.data.homeTeam.id, 5),
|
|
83
|
+
api.getTeamForm(matchResult.data.awayTeam.id, 5),
|
|
84
|
+
]);
|
|
85
|
+
if (homeFormResult.success && homeFormResult.data) {
|
|
86
|
+
homeForm = homeFormResult.data;
|
|
87
|
+
}
|
|
88
|
+
if (awayFormResult.success && awayFormResult.data) {
|
|
89
|
+
awayForm = awayFormResult.data;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
match: matchResult.success ? matchResult.data : undefined,
|
|
94
|
+
h2h: h2hResult.success ? h2hResult.data : undefined,
|
|
95
|
+
homeForm,
|
|
96
|
+
awayForm,
|
|
97
|
+
odds: oddsResult.success ? oddsResult.data : undefined,
|
|
98
|
+
lastUpdated: new Date(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
logger.error(`Error fetching API match data: ${error}`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Assess what data coverage we have from API
|
|
108
|
+
*/
|
|
109
|
+
function assessDataCoverage(apiData) {
|
|
110
|
+
if (!apiData) {
|
|
111
|
+
return {
|
|
112
|
+
score: 0,
|
|
113
|
+
factors: {
|
|
114
|
+
hasAPIData: false,
|
|
115
|
+
hasRecentData: false,
|
|
116
|
+
sampleSize: 'small',
|
|
117
|
+
dataAge: 999,
|
|
118
|
+
missingDataPoints: ['all'],
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
let score = 0;
|
|
123
|
+
const missingDataPoints = [];
|
|
124
|
+
// Base match data
|
|
125
|
+
if (apiData.match) {
|
|
126
|
+
score += 40;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
missingDataPoints.push('match');
|
|
130
|
+
}
|
|
131
|
+
// H2H data
|
|
132
|
+
if (apiData.h2h && apiData.h2h.totalMatches > 0) {
|
|
133
|
+
score += 20;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
missingDataPoints.push('h2h');
|
|
137
|
+
}
|
|
138
|
+
// Form data
|
|
139
|
+
if (apiData.homeForm && apiData.homeForm.length >= 3) {
|
|
140
|
+
score += 15;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
missingDataPoints.push('homeForm');
|
|
144
|
+
}
|
|
145
|
+
if (apiData.awayForm && apiData.awayForm.length >= 3) {
|
|
146
|
+
score += 15;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
missingDataPoints.push('awayForm');
|
|
150
|
+
}
|
|
151
|
+
// Odds data
|
|
152
|
+
if (apiData.odds) {
|
|
153
|
+
score += 10;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
missingDataPoints.push('odds');
|
|
157
|
+
}
|
|
158
|
+
// Calculate data age
|
|
159
|
+
const dataAge = apiData.lastUpdated
|
|
160
|
+
? (Date.now() - apiData.lastUpdated.getTime()) / (1000 * 60 * 60)
|
|
161
|
+
: 999;
|
|
162
|
+
// Determine sample size
|
|
163
|
+
let sampleSize = 'small';
|
|
164
|
+
const totalMatches = (apiData.homeForm?.length || 0) + (apiData.awayForm?.length || 0);
|
|
165
|
+
if (totalMatches >= 10)
|
|
166
|
+
sampleSize = 'large';
|
|
167
|
+
else if (totalMatches >= 5)
|
|
168
|
+
sampleSize = 'medium';
|
|
169
|
+
return {
|
|
170
|
+
score: Math.min(100, score),
|
|
171
|
+
factors: {
|
|
172
|
+
hasAPIData: score > 0,
|
|
173
|
+
hasRecentData: dataAge < 24,
|
|
174
|
+
sampleSize,
|
|
175
|
+
dataAge,
|
|
176
|
+
missingDataPoints,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Optimize search queries based on API data coverage
|
|
182
|
+
*/
|
|
183
|
+
function optimizeQueries(coverage, baseQueries) {
|
|
184
|
+
// Always search for these (API doesn't cover well)
|
|
185
|
+
const alwaysSearch = ['fatigue', 'setpieces', 'referee', 'weather'];
|
|
186
|
+
return baseQueries.filter(q => {
|
|
187
|
+
// Always include critical queries
|
|
188
|
+
if (alwaysSearch.includes(q.type))
|
|
189
|
+
return true;
|
|
190
|
+
// Skip queries that API already covers well
|
|
191
|
+
switch (q.type) {
|
|
192
|
+
case 'h2h':
|
|
193
|
+
// Skip H2H search if we have good H2H data
|
|
194
|
+
return !coverage.factors.hasAPIData || coverage.factors.missingDataPoints.includes('h2h');
|
|
195
|
+
case 'form':
|
|
196
|
+
// Skip form search if we have form data
|
|
197
|
+
return !coverage.factors.hasAPIData ||
|
|
198
|
+
(coverage.factors.missingDataPoints.includes('homeForm') ||
|
|
199
|
+
coverage.factors.missingDataPoints.includes('awayForm'));
|
|
200
|
+
case 'stats':
|
|
201
|
+
// Stats need web search for xG
|
|
202
|
+
return true;
|
|
203
|
+
case 'news':
|
|
204
|
+
// Always search for news/lineups
|
|
205
|
+
return true;
|
|
206
|
+
case 'odds':
|
|
207
|
+
// Skip odds search if we have API odds
|
|
208
|
+
return !coverage.factors.hasAPIData || coverage.factors.missingDataPoints.includes('odds');
|
|
209
|
+
default:
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Calculate adaptive TTL based on data type and match timing
|
|
216
|
+
*/
|
|
217
|
+
function calculateAdaptiveTTL(dataType, matchDate) {
|
|
218
|
+
const now = Date.now();
|
|
219
|
+
const hoursUntilMatch = matchDate
|
|
220
|
+
? (matchDate.getTime() - now) / (1000 * 60 * 60)
|
|
221
|
+
: Infinity;
|
|
222
|
+
switch (dataType) {
|
|
223
|
+
case 'odds':
|
|
224
|
+
// Odds change frequently - short TTL
|
|
225
|
+
if (hoursUntilMatch < 1)
|
|
226
|
+
return 60 * 1000; // 1 minute
|
|
227
|
+
if (hoursUntilMatch < 24)
|
|
228
|
+
return 5 * 60 * 1000; // 5 minutes
|
|
229
|
+
return 15 * 60 * 1000; // 15 minutes
|
|
230
|
+
case 'lineups':
|
|
231
|
+
// Lineups announced ~1 hour before match
|
|
232
|
+
if (hoursUntilMatch < 2)
|
|
233
|
+
return 5 * 60 * 1000; // 5 minutes
|
|
234
|
+
return 60 * 60 * 1000; // 1 hour
|
|
235
|
+
case 'news':
|
|
236
|
+
// News changes throughout the day
|
|
237
|
+
if (hoursUntilMatch < 24)
|
|
238
|
+
return 15 * 60 * 1000; // 15 minutes
|
|
239
|
+
return 60 * 60 * 1000; // 1 hour
|
|
240
|
+
case 'stats':
|
|
241
|
+
case 'h2h':
|
|
242
|
+
// Historical stats don't change
|
|
243
|
+
return 24 * 60 * 60 * 1000; // 24 hours
|
|
244
|
+
case 'form':
|
|
245
|
+
// Form updates after each match
|
|
246
|
+
if (hoursUntilMatch < 0)
|
|
247
|
+
return 60 * 1000; // Live match - 1 minute
|
|
248
|
+
return 6 * 60 * 60 * 1000; // 6 hours
|
|
249
|
+
case 'match':
|
|
250
|
+
// Match data changes based on timing
|
|
251
|
+
if (hoursUntilMatch < 0)
|
|
252
|
+
return 60 * 1000; // Live - 1 minute
|
|
253
|
+
if (hoursUntilMatch < 1)
|
|
254
|
+
return 5 * 60 * 1000; // 5 minutes
|
|
255
|
+
return 30 * 60 * 1000; // 30 minutes
|
|
256
|
+
default:
|
|
257
|
+
return 30 * 60 * 1000; // 30 minutes default
|
|
258
|
+
}
|
|
259
|
+
}
|
|
65
260
|
/**
|
|
66
261
|
* Perform comprehensive match analysis using web search
|
|
67
262
|
*/
|
|
@@ -83,15 +278,73 @@ export async function performMatchAnalysis(homeTeam, awayTeam, league, context)
|
|
|
83
278
|
if (context) {
|
|
84
279
|
queries.push({ type: 'general', query: `${baseQuery} ${context}${dateQuery}`, title: 'Specific Context' });
|
|
85
280
|
}
|
|
86
|
-
//
|
|
281
|
+
// Try to get API data first
|
|
282
|
+
const api = createAPIProvider();
|
|
87
283
|
const matchId = await findMatchId(homeTeam, awayTeam, league);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
284
|
+
let apiData = null;
|
|
285
|
+
if (matchId) {
|
|
286
|
+
apiData = await fetchAPIMatchData(api, matchId, homeTeam, awayTeam);
|
|
287
|
+
}
|
|
288
|
+
// Assess data coverage and optimize queries
|
|
289
|
+
const coverage = assessDataCoverage(apiData);
|
|
290
|
+
const queriesToExecute = optimizeQueries(coverage, queries);
|
|
91
291
|
let combinedResults = `--- FOOTBALL MATCH DATA: ${homeTeam} vs ${awayTeam} ---\n`;
|
|
92
292
|
if (matchId)
|
|
93
293
|
combinedResults += `Resolved API Match ID: ${matchId}\n`;
|
|
294
|
+
combinedResults += `Data Quality Score: ${coverage.score}/100\n`;
|
|
94
295
|
combinedResults += `Match Context Date: ${new Date().toLocaleDateString()}\n\n`;
|
|
296
|
+
// Add API Data section if available
|
|
297
|
+
if (apiData && coverage.score > 0) {
|
|
298
|
+
combinedResults += `--- API DATA (Reliable Source) ---\n\n`;
|
|
299
|
+
if (apiData.h2h) {
|
|
300
|
+
combinedResults += `### Head-to-Head Record\n`;
|
|
301
|
+
combinedResults += `- Total Matches: ${apiData.h2h.totalMatches}\n`;
|
|
302
|
+
combinedResults += `- ${homeTeam} Wins: ${apiData.h2h.homeWins}\n`;
|
|
303
|
+
combinedResults += `- Draws: ${apiData.h2h.draws}\n`;
|
|
304
|
+
combinedResults += `- ${awayTeam} Wins: ${apiData.h2h.awayWins}\n`;
|
|
305
|
+
combinedResults += `- Goals: ${apiData.h2h.homeGoals}-${apiData.h2h.awayGoals}\n\n`;
|
|
306
|
+
}
|
|
307
|
+
if (apiData.homeForm?.length) {
|
|
308
|
+
combinedResults += `### ${homeTeam} Recent Form (Last ${apiData.homeForm.length})\n`;
|
|
309
|
+
combinedResults += apiData.homeForm.map(m => {
|
|
310
|
+
const isHome = m.homeTeam.name.includes(homeTeam);
|
|
311
|
+
const score = m.score ? `${m.score.home}-${m.score.away}` : '?';
|
|
312
|
+
const opponent = isHome ? m.awayTeam.name : m.homeTeam.name;
|
|
313
|
+
const result = isHome
|
|
314
|
+
? (m.score?.home ?? 0) > (m.score?.away ?? 0) ? 'W' : (m.score?.home === m.score?.away ? 'D' : 'L')
|
|
315
|
+
: (m.score?.away ?? 0) > (m.score?.home ?? 0) ? 'W' : (m.score?.home === m.score?.away ? 'D' : 'L');
|
|
316
|
+
return `- ${result} vs ${opponent} (${score})`;
|
|
317
|
+
}).join('\n');
|
|
318
|
+
combinedResults += '\n\n';
|
|
319
|
+
}
|
|
320
|
+
if (apiData.awayForm?.length) {
|
|
321
|
+
combinedResults += `### ${awayTeam} Recent Form (Last ${apiData.awayForm.length})\n`;
|
|
322
|
+
combinedResults += apiData.awayForm.map(m => {
|
|
323
|
+
const isAway = m.awayTeam.name.includes(awayTeam);
|
|
324
|
+
const score = m.score ? `${m.score.home}-${m.score.away}` : '?';
|
|
325
|
+
const opponent = isAway ? m.homeTeam.name : m.awayTeam.name;
|
|
326
|
+
const result = isAway
|
|
327
|
+
? (m.score?.away ?? 0) > (m.score?.home ?? 0) ? 'W' : (m.score?.home === m.score?.away ? 'D' : 'L')
|
|
328
|
+
: (m.score?.home ?? 0) > (m.score?.away ?? 0) ? 'W' : (m.score?.home === m.score?.away ? 'D' : 'L');
|
|
329
|
+
return `- ${result} vs ${opponent} (${score})`;
|
|
330
|
+
}).join('\n');
|
|
331
|
+
combinedResults += '\n\n';
|
|
332
|
+
}
|
|
333
|
+
if (apiData.odds) {
|
|
334
|
+
combinedResults += `### Current Odds (from API)\n`;
|
|
335
|
+
combinedResults += `- Home Win: ${apiData.odds.homeWin.toFixed(2)}\n`;
|
|
336
|
+
combinedResults += `- Draw: ${apiData.odds.draw.toFixed(2)}\n`;
|
|
337
|
+
combinedResults += `- Away Win: ${apiData.odds.awayWin.toFixed(2)}\n`;
|
|
338
|
+
if (apiData.odds.asianHandicap) {
|
|
339
|
+
combinedResults += `- Asian Handicap: ${apiData.odds.asianHandicap.line}\n`;
|
|
340
|
+
}
|
|
341
|
+
combinedResults += '\n';
|
|
342
|
+
}
|
|
343
|
+
combinedResults += `--- END API DATA ---\n\n`;
|
|
344
|
+
}
|
|
345
|
+
if (coverage.factors.missingDataPoints.length > 0) {
|
|
346
|
+
combinedResults += `*Note: Using web search to supplement: ${coverage.factors.missingDataPoints.join(', ')}*\n\n`;
|
|
347
|
+
}
|
|
95
348
|
const candidateUrls = [];
|
|
96
349
|
// Execute searches in parallel (in batches)
|
|
97
350
|
const BATCH_SIZE = 4;
|
|
@@ -164,6 +417,225 @@ export async function performMatchAnalysis(homeTeam, awayTeam, league, context)
|
|
|
164
417
|
combinedResults += `🏆 FINAL VERDICT:\n - Asian Handicap Leans\n - Goal Line (Over/Under)\n - The "Value Pick"`;
|
|
165
418
|
return combinedResults;
|
|
166
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* Calculate probability range with uncertainty quantification
|
|
422
|
+
*/
|
|
423
|
+
function calculateProbabilityRange(baseProbability, dataQuality) {
|
|
424
|
+
// Higher data quality = narrower range
|
|
425
|
+
const qualityFactor = dataQuality.score / 100;
|
|
426
|
+
const baseUncertainty = 0.15; // 15% base uncertainty
|
|
427
|
+
// Adjust uncertainty based on data quality
|
|
428
|
+
const adjustedUncertainty = baseUncertainty * (1 - qualityFactor * 0.7);
|
|
429
|
+
// Calculate range
|
|
430
|
+
const mid = baseProbability;
|
|
431
|
+
const margin = adjustedUncertainty * Math.sqrt(baseProbability * (1 - baseProbability));
|
|
432
|
+
return {
|
|
433
|
+
low: Math.max(0, mid - margin * 1.645), // 5th percentile
|
|
434
|
+
mid,
|
|
435
|
+
high: Math.min(1, mid + margin * 1.645), // 95th percentile
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Detect value bets from predictions and odds
|
|
440
|
+
*/
|
|
441
|
+
async function detectValueBets(apiData, homeTeam, awayTeam) {
|
|
442
|
+
const valueBets = [];
|
|
443
|
+
if (!apiData?.odds)
|
|
444
|
+
return valueBets;
|
|
445
|
+
// Estimate probabilities from API data
|
|
446
|
+
// Simple model: use form and H2H to estimate
|
|
447
|
+
let homeWinProb = 0.33;
|
|
448
|
+
let drawProb = 0.33;
|
|
449
|
+
let awayWinProb = 0.33;
|
|
450
|
+
if (apiData.homeForm && apiData.awayForm) {
|
|
451
|
+
const homeWins = apiData.homeForm.filter(m => {
|
|
452
|
+
const isHome = m.homeTeam.name.includes(homeTeam);
|
|
453
|
+
return isHome ? (m.score?.home ?? 0) > (m.score?.away ?? 0) : (m.score?.away ?? 0) > (m.score?.home ?? 0);
|
|
454
|
+
}).length;
|
|
455
|
+
const awayWins = apiData.awayForm.filter(m => {
|
|
456
|
+
const isAway = m.awayTeam.name.includes(awayTeam);
|
|
457
|
+
return isAway ? (m.score?.away ?? 0) > (m.score?.home ?? 0) : (m.score?.home ?? 0) > (m.score?.away ?? 0);
|
|
458
|
+
}).length;
|
|
459
|
+
// Normalize to probabilities
|
|
460
|
+
const total = homeWins + awayWins + 2; // +2 for draws
|
|
461
|
+
homeWinProb = (homeWins + 1) / total;
|
|
462
|
+
awayWinProb = (awayWins + 1) / total;
|
|
463
|
+
drawProb = 1 - homeWinProb - awayWinProb;
|
|
464
|
+
}
|
|
465
|
+
// Calculate value for each market
|
|
466
|
+
const markets = [
|
|
467
|
+
{ selection: `${homeTeam} Win`, prob: homeWinProb, odds: apiData.odds.homeWin },
|
|
468
|
+
{ selection: 'Draw', prob: drawProb, odds: apiData.odds.draw },
|
|
469
|
+
{ selection: `${awayTeam} Win`, prob: awayWinProb, odds: apiData.odds.awayWin },
|
|
470
|
+
];
|
|
471
|
+
for (const market of markets) {
|
|
472
|
+
if (!market.odds || market.prob <= 0)
|
|
473
|
+
continue;
|
|
474
|
+
const fairOdds = 1 / market.prob;
|
|
475
|
+
const value = (market.odds / fairOdds) - 1;
|
|
476
|
+
// Only include if value > 5%
|
|
477
|
+
if (value >= 0.05) {
|
|
478
|
+
const kelly = (market.prob * market.odds - 1) / (market.odds - 1);
|
|
479
|
+
valueBets.push({
|
|
480
|
+
selection: market.selection,
|
|
481
|
+
market: '1x2',
|
|
482
|
+
odds: market.odds,
|
|
483
|
+
fairOdds,
|
|
484
|
+
value,
|
|
485
|
+
confidence: Math.min(90, 50 + value * 200), // Scale value to confidence
|
|
486
|
+
kellyFraction: Math.max(0, kelly / 2), // Half Kelly
|
|
487
|
+
recommendedStake: Math.max(0, kelly / 2) * 100, // Based on 100 unit bankroll
|
|
488
|
+
reasoning: `Estimated probability ${(market.prob * 100).toFixed(1)}% vs implied ${((1 / market.odds) * 100).toFixed(1)}%`,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return valueBets.sort((a, b) => b.value - a.value);
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Build structured analysis result from API and web data
|
|
496
|
+
*/
|
|
497
|
+
async function buildStructuredAnalysis(homeTeam, awayTeam, league, apiData, coverage) {
|
|
498
|
+
// Calculate base probabilities
|
|
499
|
+
let homeWinProb = 0.35;
|
|
500
|
+
let drawProb = 0.30;
|
|
501
|
+
let awayWinProb = 0.35;
|
|
502
|
+
// Adjust based on form if available
|
|
503
|
+
if (apiData?.homeForm && apiData?.awayForm) {
|
|
504
|
+
const homePoints = apiData.homeForm.reduce((sum, m) => {
|
|
505
|
+
const isHome = m.homeTeam.name.includes(homeTeam);
|
|
506
|
+
const score = m.score;
|
|
507
|
+
if (!score)
|
|
508
|
+
return sum;
|
|
509
|
+
if (isHome) {
|
|
510
|
+
return sum + (score.home > score.away ? 3 : score.home === score.away ? 1 : 0);
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
return sum + (score.away > score.home ? 3 : score.away === score.home ? 1 : 0);
|
|
514
|
+
}
|
|
515
|
+
}, 0);
|
|
516
|
+
const awayPoints = apiData.awayForm.reduce((sum, m) => {
|
|
517
|
+
const isAway = m.awayTeam.name.includes(awayTeam);
|
|
518
|
+
const score = m.score;
|
|
519
|
+
if (!score)
|
|
520
|
+
return sum;
|
|
521
|
+
if (isAway) {
|
|
522
|
+
return sum + (score.away > score.home ? 3 : score.away === score.home ? 1 : 0);
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
return sum + (score.home > score.away ? 3 : score.home === score.away ? 1 : 0);
|
|
526
|
+
}
|
|
527
|
+
}, 0);
|
|
528
|
+
const totalPoints = homePoints + awayPoints + 5; // +5 for draw possibility
|
|
529
|
+
homeWinProb = (homePoints + 1.5) / totalPoints;
|
|
530
|
+
awayWinProb = (awayPoints + 1.5) / totalPoints;
|
|
531
|
+
drawProb = 1 - homeWinProb - awayWinProb;
|
|
532
|
+
}
|
|
533
|
+
// Calculate probability ranges
|
|
534
|
+
const predictions = {
|
|
535
|
+
matchResult: {
|
|
536
|
+
homeWin: calculateProbabilityRange(homeWinProb, coverage),
|
|
537
|
+
draw: calculateProbabilityRange(drawProb, coverage),
|
|
538
|
+
awayWin: calculateProbabilityRange(awayWinProb, coverage),
|
|
539
|
+
},
|
|
540
|
+
overUnder25: {
|
|
541
|
+
over: calculateProbabilityRange(0.50, coverage), // Placeholder
|
|
542
|
+
under: calculateProbabilityRange(0.50, coverage),
|
|
543
|
+
},
|
|
544
|
+
btts: {
|
|
545
|
+
yes: calculateProbabilityRange(0.50, coverage), // Placeholder
|
|
546
|
+
no: calculateProbabilityRange(0.50, coverage),
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
// Detect value bets
|
|
550
|
+
const valueBets = await detectValueBets(apiData, homeTeam, awayTeam);
|
|
551
|
+
// Build six-persona analysis
|
|
552
|
+
const analysis = {
|
|
553
|
+
dataScientist: {
|
|
554
|
+
homeWinProbability: predictions.matchResult.homeWin,
|
|
555
|
+
drawProbability: predictions.matchResult.draw,
|
|
556
|
+
awayWinProbability: predictions.matchResult.awayWin,
|
|
557
|
+
expectedGoals: { home: 1.3, away: 1.2 }, // Placeholder - would need xG data
|
|
558
|
+
keyStats: [],
|
|
559
|
+
},
|
|
560
|
+
tacticalScout: {
|
|
561
|
+
styleMatchup: 'Analysis requires detailed tactical data',
|
|
562
|
+
keyBattles: [],
|
|
563
|
+
tacticalAdvantage: 'neutral',
|
|
564
|
+
},
|
|
565
|
+
physio: {
|
|
566
|
+
homeFatigue: {
|
|
567
|
+
score: 50,
|
|
568
|
+
daysRest: 3,
|
|
569
|
+
squadDepth: 5,
|
|
570
|
+
keyInjuries: [],
|
|
571
|
+
},
|
|
572
|
+
awayFatigue: {
|
|
573
|
+
score: 50,
|
|
574
|
+
daysRest: 3,
|
|
575
|
+
squadDepth: 5,
|
|
576
|
+
keyInjuries: [],
|
|
577
|
+
},
|
|
578
|
+
injuryImpact: 'Analysis requires injury data',
|
|
579
|
+
},
|
|
580
|
+
setPieceAnalyst: {
|
|
581
|
+
homeStrength: 5,
|
|
582
|
+
awayStrength: 5,
|
|
583
|
+
predictedCorners: 10,
|
|
584
|
+
aerialAdvantage: 'neutral',
|
|
585
|
+
},
|
|
586
|
+
insider: {
|
|
587
|
+
marketMovement: apiData?.odds ? `Home: ${apiData.odds.homeWin.toFixed(2)}, Draw: ${apiData.odds.draw.toFixed(2)}, Away: ${apiData.odds.awayWin.toFixed(2)}` : 'No odds data',
|
|
588
|
+
},
|
|
589
|
+
skeptic: {
|
|
590
|
+
upsetRisk: 0.25,
|
|
591
|
+
trapIndicators: [],
|
|
592
|
+
gameScenarios: [],
|
|
593
|
+
},
|
|
594
|
+
};
|
|
595
|
+
// Determine best bet
|
|
596
|
+
const bestBet = valueBets.length > 0
|
|
597
|
+
? valueBets[0].selection
|
|
598
|
+
: `${homeTeam} Win or Draw (Double Chance)`;
|
|
599
|
+
return {
|
|
600
|
+
match: {
|
|
601
|
+
homeTeam,
|
|
602
|
+
awayTeam,
|
|
603
|
+
league,
|
|
604
|
+
matchId: apiData?.match?.id,
|
|
605
|
+
},
|
|
606
|
+
dataQuality: coverage,
|
|
607
|
+
analysis,
|
|
608
|
+
predictions,
|
|
609
|
+
valueBets,
|
|
610
|
+
verdict: {
|
|
611
|
+
summary: `Analysis based on ${coverage.score}/100 data quality. ${apiData?.h2h ? `H2H: ${apiData.h2h.homeWins}-${apiData.h2h.draws}-${apiData.h2h.awayWins}` : 'No H2H data'}`,
|
|
612
|
+
bestBet,
|
|
613
|
+
confidence: coverage.score,
|
|
614
|
+
riskLevel: coverage.score >= 70 ? 'low' : coverage.score >= 40 ? 'medium' : 'high',
|
|
615
|
+
},
|
|
616
|
+
metadata: {
|
|
617
|
+
generatedAt: new Date(),
|
|
618
|
+
dataSources: apiData ? ['api-football'] : ['web-search'],
|
|
619
|
+
},
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Perform structured match analysis with JSON output
|
|
624
|
+
*/
|
|
625
|
+
export async function performStructuredMatchAnalysis(homeTeam, awayTeam, league, context) {
|
|
626
|
+
// Fetch API data
|
|
627
|
+
const api = createAPIProvider();
|
|
628
|
+
const matchId = await findMatchId(homeTeam, awayTeam, league);
|
|
629
|
+
let apiData = null;
|
|
630
|
+
if (matchId) {
|
|
631
|
+
apiData = await fetchAPIMatchData(api, matchId, homeTeam, awayTeam);
|
|
632
|
+
}
|
|
633
|
+
// Assess data coverage
|
|
634
|
+
const coverage = assessDataCoverage(apiData);
|
|
635
|
+
// Build structured analysis
|
|
636
|
+
const result = await buildStructuredAnalysis(homeTeam, awayTeam, league, apiData, coverage);
|
|
637
|
+
return result;
|
|
638
|
+
}
|
|
167
639
|
// ============= Tool Registration =============
|
|
168
640
|
export function registerMatchTools(server) {
|
|
169
641
|
/**
|
|
@@ -211,7 +683,60 @@ Market Intelligence, Referee stats, Weather, Fatigue analysis, Set Pieces.`, {
|
|
|
211
683
|
};
|
|
212
684
|
});
|
|
213
685
|
/**
|
|
214
|
-
* Tool 2:
|
|
686
|
+
* Tool 2: analyze_football_match_structured
|
|
687
|
+
* Structured JSON output for programmatic use
|
|
688
|
+
*/
|
|
689
|
+
server.tool('analyze_football_match_structured', `Comprehensive football match analysis with structured JSON output.
|
|
690
|
+
Returns: Probabilities with confidence ranges, 6-persona analysis, value bets, data quality score.
|
|
691
|
+
Use this for programmatic access to match analysis.`, {
|
|
692
|
+
homeTeam: z.string().describe('Name of the home team'),
|
|
693
|
+
awayTeam: z.string().describe('Name of the away team'),
|
|
694
|
+
league: z.string().optional().describe('League name (optional, helps with accuracy)'),
|
|
695
|
+
includeOdds: z.boolean().optional().default(true).describe('Include current odds in analysis'),
|
|
696
|
+
}, async ({ homeTeam, awayTeam, league, includeOdds }) => {
|
|
697
|
+
try {
|
|
698
|
+
const result = await performStructuredMatchAnalysis(homeTeam, awayTeam, league);
|
|
699
|
+
// Format as readable JSON with summary
|
|
700
|
+
const output = {
|
|
701
|
+
summary: `${homeTeam} vs ${awayTeam}`,
|
|
702
|
+
dataQuality: `${result.dataQuality.score}/100`,
|
|
703
|
+
predictions: {
|
|
704
|
+
homeWin: `${(result.predictions.matchResult.homeWin.mid * 100).toFixed(1)}% (${(result.predictions.matchResult.homeWin.low * 100).toFixed(1)}%-${(result.predictions.matchResult.homeWin.high * 100).toFixed(1)}%)`,
|
|
705
|
+
draw: `${(result.predictions.matchResult.draw.mid * 100).toFixed(1)}% (${(result.predictions.matchResult.draw.low * 100).toFixed(1)}%-${(result.predictions.matchResult.draw.high * 100).toFixed(1)}%)`,
|
|
706
|
+
awayWin: `${(result.predictions.matchResult.awayWin.mid * 100).toFixed(1)}% (${(result.predictions.matchResult.awayWin.low * 100).toFixed(1)}%-${(result.predictions.matchResult.awayWin.high * 100).toFixed(1)}%)`,
|
|
707
|
+
},
|
|
708
|
+
valueBets: result.valueBets.length > 0
|
|
709
|
+
? result.valueBets.map(vb => ({
|
|
710
|
+
selection: vb.selection,
|
|
711
|
+
odds: vb.odds.toFixed(2),
|
|
712
|
+
value: `${(vb.value * 100).toFixed(1)}%`,
|
|
713
|
+
stake: `${vb.recommendedStake.toFixed(1)}u`
|
|
714
|
+
}))
|
|
715
|
+
: 'No value bets detected',
|
|
716
|
+
bestBet: result.verdict.bestBet,
|
|
717
|
+
confidence: `${result.verdict.confidence}/100`,
|
|
718
|
+
riskLevel: result.verdict.riskLevel,
|
|
719
|
+
fullAnalysis: result,
|
|
720
|
+
};
|
|
721
|
+
return {
|
|
722
|
+
content: [{
|
|
723
|
+
type: 'text',
|
|
724
|
+
text: JSON.stringify(output, null, 2)
|
|
725
|
+
}],
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
return {
|
|
730
|
+
content: [{
|
|
731
|
+
type: 'text',
|
|
732
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
733
|
+
}],
|
|
734
|
+
isError: true,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
/**
|
|
739
|
+
* Tool 3: get_live_scores
|
|
215
740
|
* Get live football scores for a league or team
|
|
216
741
|
*/
|
|
217
742
|
server.tool('get_live_scores', `Get live football scores.
|
package/package.json
CHANGED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ALERT MANAGER
|
|
3
|
-
* Real-time alert system with rule engine
|
|
4
|
-
*/
|
|
5
|
-
import { EventEmitter } from 'events';
|
|
6
|
-
import type { LiveEvent } from './realtime-manager.js';
|
|
7
|
-
export interface AlertRule {
|
|
8
|
-
id: string;
|
|
9
|
-
name: string;
|
|
10
|
-
type: AlertType;
|
|
11
|
-
condition: AlertCondition;
|
|
12
|
-
channels: NotificationChannel[];
|
|
13
|
-
cooldown: number;
|
|
14
|
-
enabled: boolean;
|
|
15
|
-
createdAt: number;
|
|
16
|
-
lastTriggered?: number;
|
|
17
|
-
triggerCount?: number;
|
|
18
|
-
}
|
|
19
|
-
export type AlertType = 'odds_drop' | 'odds_value' | 'goal' | 'red_card' | 'lineup_change' | 'match_start' | 'match_end' | 'custom';
|
|
20
|
-
export type AlertCondition = OddsDropCondition | OddsValueCondition | EventCondition | CompositeCondition;
|
|
21
|
-
export interface OddsDropCondition {
|
|
22
|
-
type: 'odds_drop';
|
|
23
|
-
matchId?: string;
|
|
24
|
-
team?: string;
|
|
25
|
-
threshold: number;
|
|
26
|
-
percentage: number;
|
|
27
|
-
}
|
|
28
|
-
export interface OddsValueCondition {
|
|
29
|
-
type: 'odds_value';
|
|
30
|
-
matchId?: string;
|
|
31
|
-
minValue: number;
|
|
32
|
-
maxOdds?: number;
|
|
33
|
-
}
|
|
34
|
-
export interface EventCondition {
|
|
35
|
-
type: 'event';
|
|
36
|
-
matchId?: string;
|
|
37
|
-
eventTypes: string[];
|
|
38
|
-
}
|
|
39
|
-
export interface CompositeCondition {
|
|
40
|
-
type: 'composite';
|
|
41
|
-
conditions: AlertCondition[];
|
|
42
|
-
operator: 'AND' | 'OR';
|
|
43
|
-
}
|
|
44
|
-
export interface NotificationChannel {
|
|
45
|
-
type: 'webhook' | 'email' | 'slack' | 'discord' | 'console';
|
|
46
|
-
config: Record<string, any>;
|
|
47
|
-
}
|
|
48
|
-
export interface AlertMessage {
|
|
49
|
-
ruleId: string;
|
|
50
|
-
ruleName: string;
|
|
51
|
-
type: AlertType;
|
|
52
|
-
severity: 'info' | 'warning' | 'critical';
|
|
53
|
-
title: string;
|
|
54
|
-
body: string;
|
|
55
|
-
data: any;
|
|
56
|
-
timestamp: number;
|
|
57
|
-
}
|
|
58
|
-
export declare class AlertManager extends EventEmitter {
|
|
59
|
-
private rules;
|
|
60
|
-
private checkInterval?;
|
|
61
|
-
private alertHistory;
|
|
62
|
-
private maxHistorySize;
|
|
63
|
-
constructor();
|
|
64
|
-
start(): void;
|
|
65
|
-
stop(): void;
|
|
66
|
-
addRule(rule: Omit<AlertRule, 'id' | 'createdAt' | 'triggerCount'>): AlertRule;
|
|
67
|
-
removeRule(ruleId: string): boolean;
|
|
68
|
-
getRules(): AlertRule[];
|
|
69
|
-
getRule(ruleId: string): AlertRule | undefined;
|
|
70
|
-
toggleRule(ruleId: string, enabled: boolean): boolean;
|
|
71
|
-
processEvent(event: LiveEvent): void;
|
|
72
|
-
private evaluateCondition;
|
|
73
|
-
private evaluateOddsDrop;
|
|
74
|
-
private evaluateOddsValue;
|
|
75
|
-
private evaluateEventCondition;
|
|
76
|
-
private evaluateCompositeCondition;
|
|
77
|
-
private triggerAlert;
|
|
78
|
-
private createAlertMessage;
|
|
79
|
-
private determineSeverity;
|
|
80
|
-
private formatAlertTitle;
|
|
81
|
-
private formatAlertBody;
|
|
82
|
-
private sendNotification;
|
|
83
|
-
private sendWebhook;
|
|
84
|
-
private sendSlack;
|
|
85
|
-
private sendDiscord;
|
|
86
|
-
private checkScheduledAlerts;
|
|
87
|
-
getAlertHistory(limit?: number): AlertMessage[];
|
|
88
|
-
clearHistory(): void;
|
|
89
|
-
getStats(): {
|
|
90
|
-
totalRules: number;
|
|
91
|
-
enabledRules: number;
|
|
92
|
-
totalAlerts: number;
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
export declare function getAlertManager(): AlertManager;
|
|
96
|
-
export declare function resetAlertManager(): void;
|