@gotza02/sequential-thinking 10000.2.0 → 10000.2.2
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.bak +197 -0
- package/dist/.gemini_graph_cache.json.bak +1641 -0
- package/dist/graph.d.ts +7 -0
- package/dist/graph.js +136 -113
- package/dist/intelligent-code.d.ts +8 -0
- package/dist/intelligent-code.js +152 -125
- package/dist/intelligent-code.test.d.ts +1 -0
- package/dist/intelligent-code.test.js +104 -0
- package/dist/lib/formatters.d.ts +9 -0
- package/dist/lib/formatters.js +119 -0
- package/dist/lib/validators.d.ts +45 -0
- package/dist/lib/validators.js +232 -0
- package/dist/lib.js +23 -265
- package/dist/tools/sports/core/base.d.ts +3 -2
- package/dist/tools/sports/core/base.js +12 -10
- package/dist/tools/sports/core/cache.d.ts +9 -0
- package/dist/tools/sports/core/cache.js +25 -3
- package/dist/tools/sports/core/types.d.ts +6 -2
- package/dist/tools/sports/providers/api.d.ts +4 -0
- package/dist/tools/sports/providers/api.js +110 -27
- package/dist/tools/sports/tools/betting.js +16 -16
- package/dist/tools/sports/tools/league.d.ts +2 -7
- package/dist/tools/sports/tools/league.js +198 -8
- package/dist/tools/sports/tools/live.js +80 -38
- package/dist/tools/sports/tools/match-calculations.d.ts +51 -0
- package/dist/tools/sports/tools/match-calculations.js +171 -0
- package/dist/tools/sports/tools/match-helpers.d.ts +21 -0
- package/dist/tools/sports/tools/match-helpers.js +57 -0
- package/dist/tools/sports/tools/match.js +227 -125
- package/dist/tools/sports.js +3 -3
- package/dist/utils.d.ts +111 -44
- package/dist/utils.js +510 -305
- package/dist/utils.test.js +3 -3
- package/package.json +1 -1
- package/CLAUDE.md +0 -231
|
@@ -10,26 +10,10 @@ 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
12
|
import { logger } from '../../../utils.js';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
function getDateContext() {
|
|
18
|
-
const now = new Date();
|
|
19
|
-
const month = now.toLocaleDateString('en-US', { month: 'long' });
|
|
20
|
-
const year = now.getFullYear();
|
|
21
|
-
return ` ${month} ${year}`;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Normalize team names for better matching
|
|
25
|
-
*/
|
|
26
|
-
function normalizeTeamName(name) {
|
|
27
|
-
return name
|
|
28
|
-
.toLowerCase()
|
|
29
|
-
.replace(/\b(fc|afc|sc|cf|united|utd|city|town|athletic|albion|rovers|wanderers|olympic|real)\b/g, '')
|
|
30
|
-
.replace(/\s+/g, ' ')
|
|
31
|
-
.trim();
|
|
32
|
-
}
|
|
13
|
+
import { getDateContext, isTeamMatch, getAnalysisInstructions } from './match-helpers.js';
|
|
14
|
+
// Note: calculateAdaptiveTTL, calculateProbabilityRange, calculateFormPoints,
|
|
15
|
+
// calculateExpectedGoals, calculateProbabilitiesFromForm, and getLeagueId
|
|
16
|
+
// are defined locally in this file with specific implementations for match analysis
|
|
33
17
|
/**
|
|
34
18
|
* Try to find match ID from API by team names and league
|
|
35
19
|
*/
|
|
@@ -48,9 +32,9 @@ async function findMatchId(homeTeam, awayTeam, league) {
|
|
|
48
32
|
if (liveResult.success && liveResult.data) {
|
|
49
33
|
const match = liveResult.data.find(m => {
|
|
50
34
|
const isHomeMatch = homeIds.includes(m.homeTeam.id) ||
|
|
51
|
-
|
|
35
|
+
isTeamMatch(homeTeam, m.homeTeam.name);
|
|
52
36
|
const isAwayMatch = awayIds.includes(m.awayTeam.id) ||
|
|
53
|
-
|
|
37
|
+
isTeamMatch(awayTeam, m.awayTeam.name);
|
|
54
38
|
return isHomeMatch && isAwayMatch;
|
|
55
39
|
});
|
|
56
40
|
if (match)
|
|
@@ -248,7 +232,7 @@ function formatH2HData(h2h, homeTeam, awayTeam) {
|
|
|
248
232
|
* Get match result character (W/D/L) for a team
|
|
249
233
|
*/
|
|
250
234
|
function getMatchResultChar(match, teamName) {
|
|
251
|
-
const isHome = match.homeTeam.name
|
|
235
|
+
const isHome = isTeamMatch(teamName, match.homeTeam.name);
|
|
252
236
|
const homeScore = match.score?.home ?? 0;
|
|
253
237
|
const awayScore = match.score?.away ?? 0;
|
|
254
238
|
if (isHome) {
|
|
@@ -265,7 +249,8 @@ function formatTeamForm(form, teamName, isHomeTeam) {
|
|
|
265
249
|
const label = isHomeTeam ? 'Home' : 'Away';
|
|
266
250
|
let output = `### ${teamName} Recent Form (Last ${form.length})\n`;
|
|
267
251
|
output += form.map(m => {
|
|
268
|
-
const
|
|
252
|
+
const isHomeInMatch = isTeamMatch(teamName, m.homeTeam.name);
|
|
253
|
+
const opponent = isHomeInMatch ? m.awayTeam.name : m.homeTeam.name;
|
|
269
254
|
const score = m.score ? `${m.score.home}-${m.score.away}` : '?';
|
|
270
255
|
const result = getMatchResultChar(m, teamName);
|
|
271
256
|
return `- ${result} vs ${opponent} (${score})`;
|
|
@@ -345,65 +330,6 @@ async function scrapeContentWithErrorHandling(url, type) {
|
|
|
345
330
|
return `${type.charAt(0).toUpperCase() + type.slice(1)} scrape failed: ${errorMsg}`;
|
|
346
331
|
}
|
|
347
332
|
}
|
|
348
|
-
/**
|
|
349
|
-
* Get analysis framework instructions
|
|
350
|
-
*/
|
|
351
|
-
function getAnalysisInstructions() {
|
|
352
|
-
return `INSTRUCTIONS: Act as a World-Class Football Analysis Panel. Provide a deep, non-obvious analysis using this framework:\n\n` +
|
|
353
|
-
`1. 📊 THE DATA SCIENTIST (Quantitative):\n - Analyze xG trends & Possession stats.\n - Assess Home/Away variance.\n\n` +
|
|
354
|
-
`2. 🧠 THE TACTICAL SCOUT (Qualitative):\n - Stylistic Matchup (Press vs Block).\n - Key Battles.\n\n` +
|
|
355
|
-
`3. 🚑 THE PHYSIO (Physical Condition):\n - FATIGUE CHECK: Days rest? Travel distance?\n - Squad Depth: Who has the better bench?\n\n` +
|
|
356
|
-
`4. 🎯 SET PIECE ANALYST:\n - Corners/Free Kicks: Strong vs Weak?\n - Who is most likely to score from a header?\n\n` +
|
|
357
|
-
`5. 💎 THE INSIDER (External Factors):\n - Market Movements (Odds dropping?).\n - Referee & Weather impact.\n\n` +
|
|
358
|
-
`6. 🕵️ THE SKEPTIC & SCENARIOS:\n - Why might the favorite LOSE?\n - Game Script: "If Team A scores first..."\n\n` +
|
|
359
|
-
`🏆 FINAL VERDICT:\n - Asian Handicap Leans\n - Goal Line (Over/Under)\n - The "Value Pick"`;
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Calculate adaptive TTL based on data type and match timing
|
|
363
|
-
*/
|
|
364
|
-
function calculateAdaptiveTTL(dataType, matchDate) {
|
|
365
|
-
const now = Date.now();
|
|
366
|
-
const hoursUntilMatch = matchDate
|
|
367
|
-
? (matchDate.getTime() - now) / (1000 * 60 * 60)
|
|
368
|
-
: Infinity;
|
|
369
|
-
switch (dataType) {
|
|
370
|
-
case 'odds':
|
|
371
|
-
// Odds change frequently - short TTL
|
|
372
|
-
if (hoursUntilMatch < 1)
|
|
373
|
-
return 60 * 1000; // 1 minute
|
|
374
|
-
if (hoursUntilMatch < 24)
|
|
375
|
-
return 5 * 60 * 1000; // 5 minutes
|
|
376
|
-
return 15 * 60 * 1000; // 15 minutes
|
|
377
|
-
case 'lineups':
|
|
378
|
-
// Lineups announced ~1 hour before match
|
|
379
|
-
if (hoursUntilMatch < 2)
|
|
380
|
-
return 5 * 60 * 1000; // 5 minutes
|
|
381
|
-
return 60 * 60 * 1000; // 1 hour
|
|
382
|
-
case 'news':
|
|
383
|
-
// News changes throughout the day
|
|
384
|
-
if (hoursUntilMatch < 24)
|
|
385
|
-
return 15 * 60 * 1000; // 15 minutes
|
|
386
|
-
return 60 * 60 * 1000; // 1 hour
|
|
387
|
-
case 'stats':
|
|
388
|
-
case 'h2h':
|
|
389
|
-
// Historical stats don't change
|
|
390
|
-
return 24 * 60 * 60 * 1000; // 24 hours
|
|
391
|
-
case 'form':
|
|
392
|
-
// Form updates after each match
|
|
393
|
-
if (hoursUntilMatch < 0)
|
|
394
|
-
return 60 * 1000; // Live match - 1 minute
|
|
395
|
-
return 6 * 60 * 60 * 1000; // 6 hours
|
|
396
|
-
case 'match':
|
|
397
|
-
// Match data changes based on timing
|
|
398
|
-
if (hoursUntilMatch < 0)
|
|
399
|
-
return 60 * 1000; // Live - 1 minute
|
|
400
|
-
if (hoursUntilMatch < 1)
|
|
401
|
-
return 5 * 60 * 1000; // 5 minutes
|
|
402
|
-
return 30 * 60 * 1000; // 30 minutes
|
|
403
|
-
default:
|
|
404
|
-
return 30 * 60 * 1000; // 30 minutes default
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
333
|
/**
|
|
408
334
|
* Perform comprehensive match analysis using web search
|
|
409
335
|
*/
|
|
@@ -465,24 +391,6 @@ async function scrapeDeepDiveSources(candidateUrls) {
|
|
|
465
391
|
}
|
|
466
392
|
return output;
|
|
467
393
|
}
|
|
468
|
-
/**
|
|
469
|
-
* Calculate probability range with uncertainty quantification
|
|
470
|
-
*/
|
|
471
|
-
function calculateProbabilityRange(baseProbability, dataQuality) {
|
|
472
|
-
// Higher data quality = narrower range
|
|
473
|
-
const qualityFactor = dataQuality.score / 100;
|
|
474
|
-
const baseUncertainty = 0.15; // 15% base uncertainty
|
|
475
|
-
// Adjust uncertainty based on data quality
|
|
476
|
-
const adjustedUncertainty = baseUncertainty * (1 - qualityFactor * 0.7);
|
|
477
|
-
// Calculate range
|
|
478
|
-
const mid = baseProbability;
|
|
479
|
-
const margin = adjustedUncertainty * Math.sqrt(baseProbability * (1 - baseProbability));
|
|
480
|
-
return {
|
|
481
|
-
low: Math.max(0, mid - margin * 1.645), // 5th percentile
|
|
482
|
-
mid,
|
|
483
|
-
high: Math.min(1, mid + margin * 1.645), // 95th percentile
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
394
|
/**
|
|
487
395
|
* Detect value bets from predictions and odds
|
|
488
396
|
*/
|
|
@@ -493,22 +401,33 @@ async function detectValueBets(apiData, homeTeam, awayTeam) {
|
|
|
493
401
|
// Estimate probabilities from API data
|
|
494
402
|
// Simple model: use form and H2H to estimate
|
|
495
403
|
let homeWinProb = 0.33;
|
|
496
|
-
let drawProb = 0.
|
|
404
|
+
let drawProb = 0.34;
|
|
497
405
|
let awayWinProb = 0.33;
|
|
498
|
-
if (apiData.homeForm && apiData.awayForm) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
406
|
+
if (apiData.homeForm?.length && apiData.awayForm?.length) {
|
|
407
|
+
// Calculate form based on points (3 for win, 1 for draw)
|
|
408
|
+
const homePoints = calculateFormPoints(apiData.homeForm, homeTeam);
|
|
409
|
+
const awayPoints = calculateFormPoints(apiData.awayForm, awayTeam);
|
|
410
|
+
// Number of matches played
|
|
411
|
+
const homeMatches = apiData.homeForm.length;
|
|
412
|
+
const awayMatches = apiData.awayForm.length;
|
|
413
|
+
// Points per game
|
|
414
|
+
const homePPG = homePoints / homeMatches;
|
|
415
|
+
const awayPPG = awayPoints / awayMatches;
|
|
416
|
+
// Max PPG is 3, so normalize to probability
|
|
417
|
+
// Give small bonus to home team (home advantage ~5-10%)
|
|
418
|
+
const totalPPG = homePPG + awayPPG;
|
|
419
|
+
if (totalPPG > 0) {
|
|
420
|
+
homeWinProb = Math.min(0.6, (homePPG / totalPPG) * 0.9 + 0.05); // Home advantage
|
|
421
|
+
awayWinProb = Math.min(0.5, (awayPPG / totalPPG) * 0.9);
|
|
422
|
+
// Draw probability based on how close the teams are
|
|
423
|
+
const ppgDiff = Math.abs(homePPG - awayPPG);
|
|
424
|
+
drawProb = Math.max(0.15, 0.35 - ppgDiff * 0.1); // Closer teams = higher draw chance
|
|
425
|
+
// Normalize to ensure sum = 1
|
|
426
|
+
const total = homeWinProb + drawProb + awayWinProb;
|
|
427
|
+
homeWinProb /= total;
|
|
428
|
+
drawProb /= total;
|
|
429
|
+
awayWinProb /= total;
|
|
430
|
+
}
|
|
512
431
|
}
|
|
513
432
|
// Calculate value for each market
|
|
514
433
|
const markets = [
|
|
@@ -537,6 +456,100 @@ async function detectValueBets(apiData, homeTeam, awayTeam) {
|
|
|
537
456
|
});
|
|
538
457
|
}
|
|
539
458
|
}
|
|
459
|
+
// Check Asian Handicap market if available
|
|
460
|
+
if (apiData.odds.asianHandicap) {
|
|
461
|
+
const ah = apiData.odds.asianHandicap;
|
|
462
|
+
// Estimate AH probability based on match probabilities and line
|
|
463
|
+
// Simplified: home wins if they overcome the handicap
|
|
464
|
+
const ahHomeProb = homeWinProb + (drawProb * 0.5); // Approximate
|
|
465
|
+
const ahAwayProb = awayWinProb + (drawProb * 0.5);
|
|
466
|
+
if (ah.home && ahHomeProb > 0) {
|
|
467
|
+
const fairOdds = 1 / ahHomeProb;
|
|
468
|
+
const value = (ah.home / fairOdds) - 1;
|
|
469
|
+
if (value >= 0.05) {
|
|
470
|
+
const kelly = (ahHomeProb * ah.home - 1) / (ah.home - 1);
|
|
471
|
+
valueBets.push({
|
|
472
|
+
selection: `${homeTeam} AH ${ah.line > 0 ? '+' : ''}${ah.line}`,
|
|
473
|
+
market: 'asian_handicap',
|
|
474
|
+
odds: ah.home,
|
|
475
|
+
fairOdds,
|
|
476
|
+
value,
|
|
477
|
+
confidence: Math.min(90, 50 + value * 200),
|
|
478
|
+
kellyFraction: Math.max(0, kelly / 2),
|
|
479
|
+
recommendedStake: Math.max(0, kelly / 2) * 100,
|
|
480
|
+
reasoning: `Line ${ah.line}, estimated prob ${(ahHomeProb * 100).toFixed(1)}%`,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (ah.away && ahAwayProb > 0) {
|
|
485
|
+
const fairOdds = 1 / ahAwayProb;
|
|
486
|
+
const value = (ah.away / fairOdds) - 1;
|
|
487
|
+
if (value >= 0.05) {
|
|
488
|
+
const kelly = (ahAwayProb * ah.away - 1) / (ah.away - 1);
|
|
489
|
+
valueBets.push({
|
|
490
|
+
selection: `${awayTeam} AH ${ah.line > 0 ? '-' : '+'}${Math.abs(ah.line)}`,
|
|
491
|
+
market: 'asian_handicap',
|
|
492
|
+
odds: ah.away,
|
|
493
|
+
fairOdds,
|
|
494
|
+
value,
|
|
495
|
+
confidence: Math.min(90, 50 + value * 200),
|
|
496
|
+
kellyFraction: Math.max(0, kelly / 2),
|
|
497
|
+
recommendedStake: Math.max(0, kelly / 2) * 100,
|
|
498
|
+
reasoning: `Line ${-ah.line}, estimated prob ${(ahAwayProb * 100).toFixed(1)}%`,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// Check Over/Under market if available
|
|
504
|
+
if (apiData.odds.overUnder) {
|
|
505
|
+
const ou = apiData.odds.overUnder;
|
|
506
|
+
// Estimate over probability based on expected goals
|
|
507
|
+
const expectedTotal = (apiData.homeForm?.length && apiData.awayForm?.length)
|
|
508
|
+
? calculateExpectedGoals(apiData, homeTeam, awayTeam)
|
|
509
|
+
: { home: 1.5, away: 1.2 };
|
|
510
|
+
const totalXG = expectedTotal.home + expectedTotal.away;
|
|
511
|
+
// Simple model: over if total xG > line
|
|
512
|
+
const overProb = totalXG > ou.line
|
|
513
|
+
? 0.5 + (totalXG - ou.line) * 0.1
|
|
514
|
+
: 0.5 - (ou.line - totalXG) * 0.1;
|
|
515
|
+
const underProb = 1 - overProb;
|
|
516
|
+
if (ou.over && overProb > 0) {
|
|
517
|
+
const fairOdds = 1 / Math.min(0.9, Math.max(0.1, overProb));
|
|
518
|
+
const value = (ou.over / fairOdds) - 1;
|
|
519
|
+
if (value >= 0.05) {
|
|
520
|
+
const kelly = (overProb * ou.over - 1) / (ou.over - 1);
|
|
521
|
+
valueBets.push({
|
|
522
|
+
selection: `Over ${ou.line}`,
|
|
523
|
+
market: 'over_under',
|
|
524
|
+
odds: ou.over,
|
|
525
|
+
fairOdds,
|
|
526
|
+
value,
|
|
527
|
+
confidence: Math.min(90, 50 + value * 200),
|
|
528
|
+
kellyFraction: Math.max(0, kelly / 2),
|
|
529
|
+
recommendedStake: Math.max(0, kelly / 2) * 100,
|
|
530
|
+
reasoning: `Line ${ou.line}, expected goals ${totalXG.toFixed(1)}`,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (ou.under && underProb > 0) {
|
|
535
|
+
const fairOdds = 1 / Math.min(0.9, Math.max(0.1, underProb));
|
|
536
|
+
const value = (ou.under / fairOdds) - 1;
|
|
537
|
+
if (value >= 0.05) {
|
|
538
|
+
const kelly = (underProb * ou.under - 1) / (ou.under - 1);
|
|
539
|
+
valueBets.push({
|
|
540
|
+
selection: `Under ${ou.line}`,
|
|
541
|
+
market: 'over_under',
|
|
542
|
+
odds: ou.under,
|
|
543
|
+
fairOdds,
|
|
544
|
+
value,
|
|
545
|
+
confidence: Math.min(90, 50 + value * 200),
|
|
546
|
+
kellyFraction: Math.max(0, kelly / 2),
|
|
547
|
+
recommendedStake: Math.max(0, kelly / 2) * 100,
|
|
548
|
+
reasoning: `Line ${ou.line}, expected goals ${totalXG.toFixed(1)}`,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
540
553
|
return valueBets.sort((a, b) => b.value - a.value);
|
|
541
554
|
}
|
|
542
555
|
/**
|
|
@@ -544,7 +557,7 @@ async function detectValueBets(apiData, homeTeam, awayTeam) {
|
|
|
544
557
|
*/
|
|
545
558
|
function calculateFormPoints(form, teamName) {
|
|
546
559
|
return form.reduce((sum, m) => {
|
|
547
|
-
const isHome = m.homeTeam.name
|
|
560
|
+
const isHome = isTeamMatch(teamName, m.homeTeam.name);
|
|
548
561
|
const score = m.score;
|
|
549
562
|
if (!score)
|
|
550
563
|
return sum;
|
|
@@ -558,16 +571,90 @@ function calculateFormPoints(form, teamName) {
|
|
|
558
571
|
}, 0);
|
|
559
572
|
}
|
|
560
573
|
/**
|
|
561
|
-
* Calculate
|
|
574
|
+
* Calculate expected goals from form data
|
|
575
|
+
* Uses average goals scored and conceded to estimate xG
|
|
576
|
+
*/
|
|
577
|
+
function calculateExpectedGoals(apiData, homeTeam, awayTeam) {
|
|
578
|
+
if (!apiData?.homeForm?.length || !apiData?.awayForm?.length) {
|
|
579
|
+
// Default: league average with home advantage
|
|
580
|
+
return { home: 1.5, away: 1.2 };
|
|
581
|
+
}
|
|
582
|
+
// Calculate average goals scored and conceded for home team
|
|
583
|
+
let homeGoalsScored = 0;
|
|
584
|
+
let homeGoalsConceded = 0;
|
|
585
|
+
let homeMatchesWithScores = 0;
|
|
586
|
+
for (const match of apiData.homeForm) {
|
|
587
|
+
if (match.score) {
|
|
588
|
+
const isHomeInMatch = isTeamMatch(homeTeam, match.homeTeam.name);
|
|
589
|
+
homeGoalsScored += isHomeInMatch ? match.score.home : match.score.away;
|
|
590
|
+
homeGoalsConceded += isHomeInMatch ? match.score.away : match.score.home;
|
|
591
|
+
homeMatchesWithScores++;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
// Calculate average goals scored and conceded for away team
|
|
595
|
+
let awayGoalsScored = 0;
|
|
596
|
+
let awayGoalsConceded = 0;
|
|
597
|
+
let awayMatchesWithScores = 0;
|
|
598
|
+
for (const match of apiData.awayForm) {
|
|
599
|
+
if (match.score) {
|
|
600
|
+
const isAwayInMatch = isTeamMatch(awayTeam, match.awayTeam.name);
|
|
601
|
+
awayGoalsScored += isAwayInMatch ? match.score.away : match.score.home;
|
|
602
|
+
awayGoalsConceded += isAwayInMatch ? match.score.home : match.score.away;
|
|
603
|
+
awayMatchesWithScores++;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
// Calculate averages
|
|
607
|
+
const homeScoringAvg = homeMatchesWithScores > 0 ? homeGoalsScored / homeMatchesWithScores : 1.5;
|
|
608
|
+
const homeConcedingAvg = homeMatchesWithScores > 0 ? homeGoalsConceded / homeMatchesWithScores : 1.2;
|
|
609
|
+
const awayScoringAvg = awayMatchesWithScores > 0 ? awayGoalsScored / awayMatchesWithScores : 1.2;
|
|
610
|
+
const awayConcedingAvg = awayMatchesWithScores > 0 ? awayGoalsConceded / awayMatchesWithScores : 1.5;
|
|
611
|
+
// xG estimate using average of "what team scores" and "what opponent concedes"
|
|
612
|
+
// with home advantage adjustment
|
|
613
|
+
const homeAdvantage = 1.15; // ~15% boost for home
|
|
614
|
+
const homeXG = ((homeScoringAvg + awayConcedingAvg) / 2) * homeAdvantage;
|
|
615
|
+
const awayXG = (awayScoringAvg + homeConcedingAvg) / 2;
|
|
616
|
+
// Clamp to reasonable values (0.5 to 3.5 goals)
|
|
617
|
+
return {
|
|
618
|
+
home: Math.min(3.5, Math.max(0.5, homeXG)),
|
|
619
|
+
away: Math.min(3.5, Math.max(0.5, awayXG)),
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Calculate probabilities from form data using proper normalization
|
|
562
624
|
*/
|
|
563
625
|
function calculateProbabilitiesFromForm(homeForm, awayForm, homeTeam, awayTeam) {
|
|
564
626
|
const homePoints = calculateFormPoints(homeForm, homeTeam);
|
|
565
627
|
const awayPoints = calculateFormPoints(awayForm, awayTeam);
|
|
566
|
-
|
|
628
|
+
// Points per game
|
|
629
|
+
const homePPG = homePoints / Math.max(1, homeForm.length);
|
|
630
|
+
const awayPPG = awayPoints / Math.max(1, awayForm.length);
|
|
631
|
+
// Use Bradley-Terry inspired model
|
|
632
|
+
// Convert PPG to relative strength (max 3 points per game)
|
|
633
|
+
const homeStrength = homePPG / 3;
|
|
634
|
+
const awayStrength = awayPPG / 3;
|
|
635
|
+
// Apply home advantage (~10% boost)
|
|
636
|
+
const homeAdvantage = 1.1;
|
|
637
|
+
const effectiveHomeStrength = homeStrength * homeAdvantage;
|
|
638
|
+
// Calculate probabilities
|
|
639
|
+
const totalStrength = effectiveHomeStrength + awayStrength;
|
|
640
|
+
if (totalStrength < 0.1) {
|
|
641
|
+
// Not enough data, return defaults
|
|
642
|
+
return { homeWin: 0.35, draw: 0.30, awayWin: 0.35 };
|
|
643
|
+
}
|
|
644
|
+
// Raw win probabilities (before draw adjustment)
|
|
645
|
+
const rawHomeWin = effectiveHomeStrength / totalStrength;
|
|
646
|
+
const rawAwayWin = awayStrength / totalStrength;
|
|
647
|
+
// Draw probability based on how close teams are
|
|
648
|
+
// When teams are equal, draw probability is higher
|
|
649
|
+
const strengthDiff = Math.abs(homeStrength - awayStrength);
|
|
650
|
+
const drawProb = Math.max(0.15, 0.30 - strengthDiff * 0.3);
|
|
651
|
+
// Adjust win probabilities to account for draw
|
|
652
|
+
const remainingProb = 1 - drawProb;
|
|
653
|
+
const winRatio = rawHomeWin / (rawHomeWin + rawAwayWin);
|
|
567
654
|
return {
|
|
568
|
-
homeWin:
|
|
569
|
-
draw:
|
|
570
|
-
awayWin:
|
|
655
|
+
homeWin: remainingProb * winRatio,
|
|
656
|
+
draw: drawProb,
|
|
657
|
+
awayWin: remainingProb * (1 - winRatio),
|
|
571
658
|
};
|
|
572
659
|
}
|
|
573
660
|
/**
|
|
@@ -609,7 +696,7 @@ async function buildStructuredAnalysis(homeTeam, awayTeam, league, apiData, cove
|
|
|
609
696
|
homeWinProbability: predictions.matchResult.homeWin,
|
|
610
697
|
drawProbability: predictions.matchResult.draw,
|
|
611
698
|
awayWinProbability: predictions.matchResult.awayWin,
|
|
612
|
-
expectedGoals:
|
|
699
|
+
expectedGoals: calculateExpectedGoals(apiData, homeTeam, awayTeam),
|
|
613
700
|
keyStats: [],
|
|
614
701
|
},
|
|
615
702
|
tacticalScout: {
|
|
@@ -821,8 +908,8 @@ Data cached for 1 minute.`, {
|
|
|
821
908
|
matches = result.data;
|
|
822
909
|
// Filter by team if specified
|
|
823
910
|
if (team) {
|
|
824
|
-
matches = matches.filter((m) => m.homeTeam.name
|
|
825
|
-
m.awayTeam.name
|
|
911
|
+
matches = matches.filter((m) => isTeamMatch(team, m.homeTeam.name) ||
|
|
912
|
+
isTeamMatch(team, m.awayTeam.name));
|
|
826
913
|
}
|
|
827
914
|
// Limit results
|
|
828
915
|
matches = matches.slice(0, limit);
|
|
@@ -934,6 +1021,21 @@ Requires API provider with match ID.`, {
|
|
|
934
1021
|
});
|
|
935
1022
|
}
|
|
936
1023
|
// ============= Helper Functions =============
|
|
1024
|
+
/**
|
|
1025
|
+
* Calculate probability range with uncertainty quantification
|
|
1026
|
+
*/
|
|
1027
|
+
function calculateProbabilityRange(baseProbability, dataQuality) {
|
|
1028
|
+
const qualityFactor = dataQuality.score / 100;
|
|
1029
|
+
const baseUncertainty = 0.15;
|
|
1030
|
+
const adjustedUncertainty = baseUncertainty * (1 - qualityFactor * 0.7);
|
|
1031
|
+
const mid = baseProbability;
|
|
1032
|
+
const margin = adjustedUncertainty * Math.sqrt(mid * (1 - mid) + 0.1);
|
|
1033
|
+
return {
|
|
1034
|
+
low: Math.max(0, mid - margin * 1.96),
|
|
1035
|
+
mid: Math.min(1, Math.max(0, mid)),
|
|
1036
|
+
high: Math.min(1, mid + margin * 1.96),
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
937
1039
|
/**
|
|
938
1040
|
* Get league ID from league name
|
|
939
1041
|
*/
|
package/dist/tools/sports.js
CHANGED
|
@@ -91,7 +91,7 @@ function registerLegacyAnalyzeMatch(server) {
|
|
|
91
91
|
const data = await response.json();
|
|
92
92
|
const items = data.web?.results || [];
|
|
93
93
|
items.forEach((item) => candidateUrls.push(item.url));
|
|
94
|
-
resultsText = items.map((item) => `- [${item.title}](${item.url}): ${item.description}`).join('\n');
|
|
94
|
+
resultsText = items.map((item) => `- [${item.title}](${item.url}): ${item.description ?? ''}`).join('\n');
|
|
95
95
|
}
|
|
96
96
|
else if (searchProvider === 'exa') {
|
|
97
97
|
const response = await fetchWithRetry('https://api.exa.ai/search', {
|
|
@@ -107,7 +107,7 @@ function registerLegacyAnalyzeMatch(server) {
|
|
|
107
107
|
const data = await response.json();
|
|
108
108
|
const items = data.results || [];
|
|
109
109
|
items.forEach((item) => candidateUrls.push(item.url));
|
|
110
|
-
resultsText = items.map((item) => `- [${item.title}](${item.url}): ${item.text
|
|
110
|
+
resultsText = items.map((item) => `- [${item.title}](${item.url}): ${item.text ?? item.title}`).join('\n');
|
|
111
111
|
}
|
|
112
112
|
else if (searchProvider === 'google') {
|
|
113
113
|
const response = await fetchWithRetry(`https://www.googleapis.com/customsearch/v1?key=${process.env.GOOGLE_SEARCH_API_KEY}&cx=${process.env.GOOGLE_SEARCH_CX}&q=${encodeURIComponent(q.query)}&num=4`);
|
|
@@ -116,7 +116,7 @@ function registerLegacyAnalyzeMatch(server) {
|
|
|
116
116
|
const data = await response.json();
|
|
117
117
|
const items = data.items || [];
|
|
118
118
|
items.forEach((item) => candidateUrls.push(item.link));
|
|
119
|
-
resultsText = items.map((item) => `- [${item.title}](${item.link}): ${item.snippet}`).join('\n');
|
|
119
|
+
resultsText = items.map((item) => `- [${item.title}](${item.link}): ${item.snippet ?? ''}`).join('\n');
|
|
120
120
|
}
|
|
121
121
|
return `### ${q.type}\n${resultsText}\n`;
|
|
122
122
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,63 +1,130 @@
|
|
|
1
|
+
declare const CONFIG: {
|
|
2
|
+
readonly COMMAND_TIMEOUT: 60000;
|
|
3
|
+
readonly MAX_BUFFER: number;
|
|
4
|
+
readonly MAX_PATTERN_LENGTH: 500;
|
|
5
|
+
readonly MAX_QUANTIFIERS: 20;
|
|
6
|
+
readonly MAX_LOCKS: 1000;
|
|
7
|
+
readonly DNS_TIMEOUT: 5000;
|
|
8
|
+
readonly MAX_INPUT_LENGTH: 10000;
|
|
9
|
+
readonly RETRY: {
|
|
10
|
+
readonly MAX_RETRIES: 3;
|
|
11
|
+
readonly BASE_DELAY: 1000;
|
|
12
|
+
readonly BACKOFF_MULTIPLIER: 2;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Whitelist of safe commands. Consider loading from config file for flexibility.
|
|
17
|
+
*/
|
|
18
|
+
declare const SAFE_COMMANDS: ReadonlySet<string>;
|
|
19
|
+
export declare class SecurityError extends Error {
|
|
20
|
+
readonly code: string;
|
|
21
|
+
constructor(message: string, code: string);
|
|
22
|
+
}
|
|
23
|
+
export declare class ValidationError extends Error {
|
|
24
|
+
readonly field?: string | undefined;
|
|
25
|
+
constructor(message: string, field?: string | undefined);
|
|
26
|
+
}
|
|
27
|
+
export declare class TimeoutError extends Error {
|
|
28
|
+
readonly timeout: number;
|
|
29
|
+
constructor(message: string, timeout: number);
|
|
30
|
+
}
|
|
31
|
+
interface ParsedCommand {
|
|
32
|
+
command: string;
|
|
33
|
+
args: string[];
|
|
34
|
+
}
|
|
1
35
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* @param command - The command string to execute
|
|
6
|
-
* @param options - Execution options (timeout, maxBuffer)
|
|
7
|
-
* @returns Promise with stdout and stderr
|
|
8
|
-
* @throws Error if command contains shell metacharacters or is not whitelisted
|
|
36
|
+
* Robust command parser supporting quotes and escapes.
|
|
37
|
+
* State machine implementation for correctness.
|
|
9
38
|
*/
|
|
10
|
-
|
|
39
|
+
declare function parseCommand(command: string): ParsedCommand;
|
|
40
|
+
export interface ExecOptions {
|
|
11
41
|
timeout?: number;
|
|
12
42
|
maxBuffer?: number;
|
|
13
|
-
|
|
43
|
+
cwd?: string;
|
|
44
|
+
env?: NodeJS.ProcessEnv;
|
|
45
|
+
}
|
|
46
|
+
export interface ExecResult {
|
|
14
47
|
stdout: string;
|
|
15
48
|
stderr: string;
|
|
16
|
-
|
|
49
|
+
exitCode: number;
|
|
50
|
+
executionTime: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Execute command securely using spawn (no shell interpretation).
|
|
54
|
+
*/
|
|
55
|
+
export declare function execAsync(command: string, options?: ExecOptions): Promise<ExecResult>;
|
|
17
56
|
export declare class AsyncMutex {
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
|
|
57
|
+
private queue;
|
|
58
|
+
private locked;
|
|
59
|
+
acquire(): Promise<() => void>;
|
|
60
|
+
private release;
|
|
61
|
+
dispatch<T>(fn: () => T | PromiseLike<T>): Promise<T>;
|
|
62
|
+
}
|
|
63
|
+
export declare class FileLock {
|
|
64
|
+
private locks;
|
|
65
|
+
private queue;
|
|
66
|
+
private readonly maxLocks;
|
|
67
|
+
constructor(maxLocks?: 1000);
|
|
68
|
+
acquire(filePath: string, timeout?: number): Promise<{
|
|
69
|
+
release: () => void;
|
|
70
|
+
}>;
|
|
71
|
+
private waitForLock;
|
|
72
|
+
private release;
|
|
73
|
+
private cleanupStaleLocks;
|
|
74
|
+
getLockCount(): number;
|
|
75
|
+
getQueueCount(): number;
|
|
21
76
|
}
|
|
77
|
+
export declare const fileLock: FileLock;
|
|
22
78
|
export declare function validatePath(requestedPath: string): string;
|
|
79
|
+
declare function isPrivateIPv4(ip: string): boolean;
|
|
80
|
+
declare function isPrivateIPv6(ip: string): boolean;
|
|
23
81
|
export declare function validatePublicUrl(urlString: string): Promise<void>;
|
|
24
82
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
83
|
+
interface LoggerOptions {
|
|
84
|
+
level?: LogLevel;
|
|
85
|
+
useColors?: boolean;
|
|
86
|
+
output?: 'stdout' | 'stderr';
|
|
87
|
+
}
|
|
25
88
|
declare class Logger {
|
|
26
89
|
private level;
|
|
27
|
-
|
|
90
|
+
private useColors;
|
|
91
|
+
private output;
|
|
92
|
+
private static readonly LEVELS;
|
|
93
|
+
private static readonly COLORS;
|
|
94
|
+
constructor(options?: LoggerOptions);
|
|
28
95
|
private shouldLog;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
96
|
+
private formatMessage;
|
|
97
|
+
private print;
|
|
98
|
+
debug(msg: string, ...args: unknown[]): void;
|
|
99
|
+
info(msg: string, ...args: unknown[]): void;
|
|
100
|
+
warn(msg: string, ...args: unknown[]): void;
|
|
101
|
+
error(msg: string, ...args: unknown[]): void;
|
|
102
|
+
setLevel(level: LogLevel): void;
|
|
33
103
|
}
|
|
34
104
|
export declare const logger: Logger;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*/
|
|
105
|
+
export declare class ReDoSError extends SecurityError {
|
|
106
|
+
constructor(message: string);
|
|
107
|
+
}
|
|
39
108
|
export declare function validateRegexPattern(pattern: string): void;
|
|
40
|
-
/**
|
|
41
|
-
* Create a RegExp object with ReDoS protection
|
|
42
|
-
* @param pattern - The regex pattern
|
|
43
|
-
* @param flags - Regex flags (g, i, m, etc.)
|
|
44
|
-
* @returns Safe RegExp object
|
|
45
|
-
* @throws Error if pattern is unsafe
|
|
46
|
-
*/
|
|
47
109
|
export declare function createSafeRegex(pattern: string, flags?: string): RegExp;
|
|
48
|
-
export declare const DEFAULT_HEADERS:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
export declare function fetchWithRetry(url: string, options?: any, retries?: number, backoff?: number): Promise<Response>;
|
|
54
|
-
export declare function withRetry(fn: () => Promise<any>, maxRetries?: number, baseDelay?: number): Promise<any>;
|
|
55
|
-
export declare class FileLock {
|
|
56
|
-
locks: Map<any, any>;
|
|
57
|
-
acquire(filePath: string, timeout?: number): Promise<{
|
|
58
|
-
release: () => void;
|
|
59
|
-
}>;
|
|
110
|
+
export declare const DEFAULT_HEADERS: Readonly<Record<string, string>>;
|
|
111
|
+
export interface FetchOptions extends RequestInit {
|
|
112
|
+
retries?: number;
|
|
113
|
+
backoff?: number;
|
|
114
|
+
validateUrl?: boolean;
|
|
60
115
|
}
|
|
61
|
-
export declare
|
|
62
|
-
export declare function
|
|
63
|
-
|
|
116
|
+
export declare function fetchWithRetry(url: string, options?: FetchOptions): Promise<Response>;
|
|
117
|
+
export declare function withRetry<T>(fn: () => Promise<T>, options?: {
|
|
118
|
+
maxRetries?: number;
|
|
119
|
+
baseDelay?: number;
|
|
120
|
+
onRetry?: (error: Error, attempt: number) => void;
|
|
121
|
+
shouldRetry?: (error: Error) => boolean;
|
|
122
|
+
}): Promise<T>;
|
|
123
|
+
export interface SanitizeOptions {
|
|
124
|
+
maxLength?: number;
|
|
125
|
+
allowedPattern?: RegExp;
|
|
126
|
+
removeNull?: boolean;
|
|
127
|
+
trim?: boolean;
|
|
128
|
+
}
|
|
129
|
+
export declare function sanitizeInput(input: unknown, options?: SanitizeOptions): string;
|
|
130
|
+
export { CONFIG, SAFE_COMMANDS, parseCommand, isPrivateIPv4, isPrivateIPv6, Logger, };
|