@gotza02/sequential-thinking 10000.1.2 → 10000.1.3
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/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 +1 -40
- 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
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ML PREDICTION ENGINE
|
|
3
|
-
*/
|
|
4
|
-
import { ML_CONFIG } from './constants.js';
|
|
5
|
-
// Simple logger fallback
|
|
6
|
-
const logger = {
|
|
7
|
-
info: (...args) => console.error('[INFO]', ...args),
|
|
8
|
-
warn: (...args) => console.warn('[WARN]', ...args),
|
|
9
|
-
error: (...args) => console.error('[ERROR]', ...args),
|
|
10
|
-
debug: (...args) => { }
|
|
11
|
-
};
|
|
12
|
-
export class MLPredictionEngine {
|
|
13
|
-
weights = ML_CONFIG.FEATURE_WEIGHTS;
|
|
14
|
-
historicalData = new Map();
|
|
15
|
-
async predict(input) {
|
|
16
|
-
const factors = [];
|
|
17
|
-
const formScore = this.calculateFormScore(input);
|
|
18
|
-
factors.push({
|
|
19
|
-
name: 'Recent Form',
|
|
20
|
-
weight: this.weights.form,
|
|
21
|
-
impact: formScore > 0 ? 'positive' : formScore < 0 ? 'negative' : 'neutral',
|
|
22
|
-
description: this.describeFormImpact(input),
|
|
23
|
-
});
|
|
24
|
-
const h2hScore = this.calculateH2HScore(input);
|
|
25
|
-
factors.push({
|
|
26
|
-
name: 'Head-to-Head',
|
|
27
|
-
weight: this.weights.h2h,
|
|
28
|
-
impact: h2hScore > 0 ? 'positive' : h2hScore < 0 ? 'negative' : 'neutral',
|
|
29
|
-
description: this.describeH2HImpact(input),
|
|
30
|
-
});
|
|
31
|
-
const homeAdvantageScore = this.calculateHomeAdvantage(input);
|
|
32
|
-
factors.push({
|
|
33
|
-
name: 'Home Advantage',
|
|
34
|
-
weight: this.weights.home_advantage,
|
|
35
|
-
impact: homeAdvantageScore > 0 ? 'positive' : 'neutral',
|
|
36
|
-
description: `Home advantage factor: ${(homeAdvantageScore * 100).toFixed(1)}%`,
|
|
37
|
-
});
|
|
38
|
-
const xgScore = this.calculateXgScore(input);
|
|
39
|
-
factors.push({
|
|
40
|
-
name: 'Expected Goals (xG)',
|
|
41
|
-
weight: this.weights.xg,
|
|
42
|
-
impact: xgScore > 0 ? 'positive' : xgScore < 0 ? 'negative' : 'neutral',
|
|
43
|
-
description: `xG differential: ${(xgScore * 100).toFixed(1)}%`,
|
|
44
|
-
});
|
|
45
|
-
const injuryScore = this.calculateInjuryImpact(input);
|
|
46
|
-
factors.push({
|
|
47
|
-
name: 'Injuries',
|
|
48
|
-
weight: this.weights.injuries,
|
|
49
|
-
impact: injuryScore > 0 ? 'positive' : injuryScore < 0 ? 'negative' : 'neutral',
|
|
50
|
-
description: this.describeInjuryImpact(input),
|
|
51
|
-
});
|
|
52
|
-
const fatigueScore = this.calculateFatigueImpact(input);
|
|
53
|
-
factors.push({
|
|
54
|
-
name: 'Fatigue',
|
|
55
|
-
weight: this.weights.fatigue,
|
|
56
|
-
impact: fatigueScore > 0 ? 'positive' : fatigueScore < 0 ? 'negative' : 'neutral',
|
|
57
|
-
description: `Fatigue differential: ${(fatigueScore * 100).toFixed(1)}%`,
|
|
58
|
-
});
|
|
59
|
-
const weatherScore = this.calculateWeatherImpact(input);
|
|
60
|
-
factors.push({
|
|
61
|
-
name: 'Weather',
|
|
62
|
-
weight: this.weights.weather,
|
|
63
|
-
impact: weatherScore !== 0 ? (weatherScore > 0 ? 'positive' : 'negative') : 'neutral',
|
|
64
|
-
description: this.describeWeatherImpact(input),
|
|
65
|
-
});
|
|
66
|
-
const combinedScore = this.combineScores([
|
|
67
|
-
{ score: formScore, weight: this.weights.form },
|
|
68
|
-
{ score: h2hScore, weight: this.weights.h2h },
|
|
69
|
-
{ score: homeAdvantageScore, weight: this.weights.home_advantage },
|
|
70
|
-
{ score: xgScore, weight: this.weights.xg },
|
|
71
|
-
{ score: injuryScore, weight: this.weights.injuries },
|
|
72
|
-
{ score: fatigueScore, weight: this.weights.fatigue },
|
|
73
|
-
{ score: weatherScore, weight: this.weights.weather },
|
|
74
|
-
]);
|
|
75
|
-
const probabilities = this.scoreToProbabilities(combinedScore);
|
|
76
|
-
const confidence = this.calculateConfidence(factors, input);
|
|
77
|
-
const over25Prob = this.predictOver25(input);
|
|
78
|
-
const bttsProb = this.predictBTTS(input);
|
|
79
|
-
logger.info(`[MLPrediction] ${input.match.homeTeam.name} vs ${input.match.awayTeam.name}: ` +
|
|
80
|
-
`H:${(probabilities.home * 100).toFixed(1)}% D:${(probabilities.draw * 100).toFixed(1)}% A:${(probabilities.away * 100).toFixed(1)}%`);
|
|
81
|
-
return {
|
|
82
|
-
homeWin: probabilities.home,
|
|
83
|
-
draw: probabilities.draw,
|
|
84
|
-
awayWin: probabilities.away,
|
|
85
|
-
over25: over25Prob,
|
|
86
|
-
btts: bttsProb,
|
|
87
|
-
confidence,
|
|
88
|
-
factors,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
calculateFormScore(input) {
|
|
92
|
-
const homePoints = this.formToPoints(input.homeForm.last5);
|
|
93
|
-
const awayPoints = this.formToPoints(input.awayForm.last5);
|
|
94
|
-
const maxPoints = 15;
|
|
95
|
-
const homeNormalized = (homePoints / maxPoints) * 2 - 1;
|
|
96
|
-
const awayNormalized = (awayPoints / maxPoints) * 2 - 1;
|
|
97
|
-
return homeNormalized - awayNormalized;
|
|
98
|
-
}
|
|
99
|
-
formToPoints(form) {
|
|
100
|
-
return form.reduce((sum, result) => {
|
|
101
|
-
if (result === 'W')
|
|
102
|
-
return sum + 3;
|
|
103
|
-
if (result === 'D')
|
|
104
|
-
return sum + 1;
|
|
105
|
-
return sum;
|
|
106
|
-
}, 0);
|
|
107
|
-
}
|
|
108
|
-
calculateH2HScore(input) {
|
|
109
|
-
const h2h = input.h2h;
|
|
110
|
-
const total = h2h.totalMatches;
|
|
111
|
-
if (total === 0)
|
|
112
|
-
return 0;
|
|
113
|
-
const homeWinRate = h2h.homeWins / total;
|
|
114
|
-
const awayWinRate = h2h.awayWins / total;
|
|
115
|
-
const recentScore = h2h.recentMatches.slice(0, 5).reduce((score, match, index) => {
|
|
116
|
-
const weight = (5 - index) / 5;
|
|
117
|
-
if (match.score) {
|
|
118
|
-
if (match.score.home > match.score.away)
|
|
119
|
-
return score + weight;
|
|
120
|
-
if (match.score.home < match.score.away)
|
|
121
|
-
return score - weight;
|
|
122
|
-
}
|
|
123
|
-
return score;
|
|
124
|
-
}, 0);
|
|
125
|
-
const historicalScore = homeWinRate - awayWinRate;
|
|
126
|
-
return Math.max(-1, Math.min(1, (historicalScore * 0.4) + (recentScore / 5 * 0.6)));
|
|
127
|
-
}
|
|
128
|
-
calculateHomeAdvantage(input) {
|
|
129
|
-
const homeStats = input.homeStats;
|
|
130
|
-
const awayStats = input.awayStats;
|
|
131
|
-
if (!homeStats?.homeStats || !awayStats?.awayStats)
|
|
132
|
-
return 0.1;
|
|
133
|
-
const homeWinRate = homeStats.homeStats.wins / (homeStats.homeStats.matchesPlayed || 1);
|
|
134
|
-
const awayWinRate = awayStats.awayStats.wins / (awayStats.awayStats.matchesPlayed || 1);
|
|
135
|
-
return Math.max(0, homeWinRate - awayWinRate);
|
|
136
|
-
}
|
|
137
|
-
calculateXgScore(input) {
|
|
138
|
-
const homeXgDiff = input.homeForm.xG - input.homeForm.xGA;
|
|
139
|
-
const awayXgDiff = input.awayForm.xG - input.awayForm.xGA;
|
|
140
|
-
return Math.max(-1, Math.min(1, (homeXgDiff - awayXgDiff) / 2));
|
|
141
|
-
}
|
|
142
|
-
calculateInjuryImpact(input) {
|
|
143
|
-
const impact = (input.injuries.away.keyPlayers - input.injuries.home.keyPlayers) * 0.1;
|
|
144
|
-
return Math.max(-1, Math.min(1, impact));
|
|
145
|
-
}
|
|
146
|
-
calculateFatigueImpact(input) {
|
|
147
|
-
const diff = input.awayStats.matchesPlayed - input.homeStats.matchesPlayed;
|
|
148
|
-
return Math.max(-1, Math.min(1, diff * 0.02));
|
|
149
|
-
}
|
|
150
|
-
calculateWeatherImpact(input) {
|
|
151
|
-
if (!input.weather)
|
|
152
|
-
return 0;
|
|
153
|
-
let impact = 0;
|
|
154
|
-
if (input.weather.precipitation)
|
|
155
|
-
impact -= 0.1;
|
|
156
|
-
if (input.weather.windSpeed > 20)
|
|
157
|
-
impact -= 0.1;
|
|
158
|
-
if (input.weather.temperature > 30 || input.weather.temperature < 0)
|
|
159
|
-
impact -= 0.05;
|
|
160
|
-
return impact;
|
|
161
|
-
}
|
|
162
|
-
combineScores(scores) {
|
|
163
|
-
const totalWeight = scores.reduce((sum, s) => sum + s.weight, 0);
|
|
164
|
-
const weightedSum = scores.reduce((sum, s) => sum + (s.score * s.weight), 0);
|
|
165
|
-
return weightedSum / totalWeight;
|
|
166
|
-
}
|
|
167
|
-
scoreToProbabilities(score) {
|
|
168
|
-
const homeExp = Math.exp(score * 2);
|
|
169
|
-
const drawExp = Math.exp(0);
|
|
170
|
-
const awayExp = Math.exp(-score * 2);
|
|
171
|
-
const total = homeExp + drawExp + awayExp;
|
|
172
|
-
return { home: homeExp / total, draw: drawExp / total, away: awayExp / total };
|
|
173
|
-
}
|
|
174
|
-
calculateConfidence(factors, input) {
|
|
175
|
-
let confidence = 50;
|
|
176
|
-
if (input.homeForm.last5.length >= 5)
|
|
177
|
-
confidence += 10;
|
|
178
|
-
if (input.awayForm.last5.length >= 5)
|
|
179
|
-
confidence += 10;
|
|
180
|
-
if (input.h2h.totalMatches >= 3)
|
|
181
|
-
confidence += 10;
|
|
182
|
-
const highImpactFactors = factors.filter(f => Math.abs(f.weight) > 0.15 && f.impact !== 'neutral').length;
|
|
183
|
-
confidence += highImpactFactors * 5;
|
|
184
|
-
return Math.min(95, confidence);
|
|
185
|
-
}
|
|
186
|
-
predictOver25(input) {
|
|
187
|
-
const expectedGoals = (input.homeForm.xG / 5 + input.awayForm.xGA / 5 + input.awayForm.xG / 5 + input.homeForm.xGA / 5) / 2;
|
|
188
|
-
return Math.min(0.9, Math.max(0.1, expectedGoals / 3));
|
|
189
|
-
}
|
|
190
|
-
predictBTTS(input) {
|
|
191
|
-
const homeScoring = input.homeForm.goalsFor / 5;
|
|
192
|
-
const awayScoring = input.awayForm.goalsFor / 5;
|
|
193
|
-
const homeLikelyToScore = homeScoring > 1 ? 0.7 : homeScoring > 0.5 ? 0.5 : 0.3;
|
|
194
|
-
const awayLikelyToScore = awayScoring > 1 ? 0.7 : awayScoring > 0.5 ? 0.5 : 0.3;
|
|
195
|
-
return homeLikelyToScore * awayLikelyToScore;
|
|
196
|
-
}
|
|
197
|
-
describeFormImpact(input) {
|
|
198
|
-
const homePoints = this.formToPoints(input.homeForm.last5);
|
|
199
|
-
const awayPoints = this.formToPoints(input.awayForm.last5);
|
|
200
|
-
if (homePoints > awayPoints + 4)
|
|
201
|
-
return `${input.match.homeTeam.name} in significantly better form`;
|
|
202
|
-
if (awayPoints > homePoints + 4)
|
|
203
|
-
return `${input.match.awayTeam.name} in significantly better form`;
|
|
204
|
-
if (Math.abs(homePoints - awayPoints) <= 2)
|
|
205
|
-
return 'Teams in similar form';
|
|
206
|
-
return 'Form advantage detected';
|
|
207
|
-
}
|
|
208
|
-
describeH2HImpact(input) {
|
|
209
|
-
const h2h = input.h2h;
|
|
210
|
-
if (h2h.totalMatches === 0)
|
|
211
|
-
return 'No recent head-to-head data';
|
|
212
|
-
const homeWinRate = (h2h.homeWins / h2h.totalMatches * 100).toFixed(0);
|
|
213
|
-
return `${input.match.homeTeam.name} won ${homeWinRate}% of ${h2h.totalMatches} meetings`;
|
|
214
|
-
}
|
|
215
|
-
describeInjuryImpact(input) {
|
|
216
|
-
const homeKey = input.injuries.home.keyPlayers;
|
|
217
|
-
const awayKey = input.injuries.away.keyPlayers;
|
|
218
|
-
if (homeKey === 0 && awayKey === 0)
|
|
219
|
-
return 'No key injuries for either team';
|
|
220
|
-
if (homeKey > awayKey)
|
|
221
|
-
return `${input.match.homeTeam.name} has ${homeKey} key player(s) injured`;
|
|
222
|
-
if (awayKey > homeKey)
|
|
223
|
-
return `${input.match.awayTeam.name} has ${awayKey} key player(s) injured`;
|
|
224
|
-
return 'Both teams have similar injury concerns';
|
|
225
|
-
}
|
|
226
|
-
describeWeatherImpact(input) {
|
|
227
|
-
if (!input.weather)
|
|
228
|
-
return 'Weather data not available';
|
|
229
|
-
const conditions = [];
|
|
230
|
-
if (input.weather.precipitation)
|
|
231
|
-
conditions.push('rain');
|
|
232
|
-
if (input.weather.windSpeed > 20)
|
|
233
|
-
conditions.push('high wind');
|
|
234
|
-
if (input.weather.temperature > 30)
|
|
235
|
-
conditions.push('hot');
|
|
236
|
-
if (input.weather.temperature < 5)
|
|
237
|
-
conditions.push('cold');
|
|
238
|
-
if (conditions.length === 0)
|
|
239
|
-
return 'Good weather conditions';
|
|
240
|
-
return `Adverse conditions: ${conditions.join(', ')}`;
|
|
241
|
-
}
|
|
242
|
-
learnFromResult(prediction, actualResult, odds) {
|
|
243
|
-
const predictedOutcome = prediction.homeWin > prediction.draw && prediction.homeWin > prediction.awayWin
|
|
244
|
-
? 'home' : prediction.awayWin > prediction.draw ? 'away' : 'draw';
|
|
245
|
-
const wasCorrect = predictedOutcome === actualResult;
|
|
246
|
-
if (wasCorrect) {
|
|
247
|
-
logger.info(`[MLPrediction] Correct prediction at ${odds}x`);
|
|
248
|
-
}
|
|
249
|
-
else {
|
|
250
|
-
logger.info(`[MLPrediction] Incorrect prediction. Predicted: ${predictedOutcome}, Actual: ${actualResult}`);
|
|
251
|
-
}
|
|
252
|
-
this.historicalData.set(`result-${Date.now()}`, { prediction, actualResult, odds, wasCorrect, timestamp: Date.now() });
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
let globalPredictionEngine = null;
|
|
256
|
-
export function getPredictionEngine() {
|
|
257
|
-
if (!globalPredictionEngine)
|
|
258
|
-
globalPredictionEngine = new MLPredictionEngine();
|
|
259
|
-
return globalPredictionEngine;
|
|
260
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* REALTIME DATA MANAGER
|
|
3
|
-
* Polling-based real-time data streaming
|
|
4
|
-
*/
|
|
5
|
-
import { EventEmitter } from 'events';
|
|
6
|
-
import type { Match } from './types.js';
|
|
7
|
-
export interface LiveEvent {
|
|
8
|
-
type: 'goal' | 'card' | 'substitution' | 'var' | 'injury' | 'whistle' | 'odds_change' | 'status_change';
|
|
9
|
-
matchId: string;
|
|
10
|
-
timestamp: number;
|
|
11
|
-
minute?: number;
|
|
12
|
-
data: any;
|
|
13
|
-
}
|
|
14
|
-
export interface OddsChangeEvent extends LiveEvent {
|
|
15
|
-
type: 'odds_change';
|
|
16
|
-
data: {
|
|
17
|
-
bookmaker: string;
|
|
18
|
-
market: string;
|
|
19
|
-
selection: string;
|
|
20
|
-
oldOdds: number;
|
|
21
|
-
newOdds: number;
|
|
22
|
-
change: number;
|
|
23
|
-
timestamp: number;
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
export declare class RealtimeDataManager extends EventEmitter {
|
|
27
|
-
private cache;
|
|
28
|
-
private pollingIntervals;
|
|
29
|
-
private isRunning;
|
|
30
|
-
constructor();
|
|
31
|
-
start(): Promise<void>;
|
|
32
|
-
stop(): void;
|
|
33
|
-
subscribeToMatch(matchId: string, callback: (event: LiveEvent) => void): () => void;
|
|
34
|
-
subscribeToOdds(matchId: string, callback: (event: OddsChangeEvent) => void): () => void;
|
|
35
|
-
subscribeToAllEvents(callback: (event: LiveEvent) => void): () => void;
|
|
36
|
-
processEvent(event: LiveEvent): void;
|
|
37
|
-
getLiveMatches(): Match[];
|
|
38
|
-
getMatchEvents(matchId: string): LiveEvent[];
|
|
39
|
-
private unsubscribeFromMatch;
|
|
40
|
-
private startLiveScoresPolling;
|
|
41
|
-
private startOddsPolling;
|
|
42
|
-
private startMatchEventsPolling;
|
|
43
|
-
private detectScoreChanges;
|
|
44
|
-
private detectOddsChanges;
|
|
45
|
-
private updateCache;
|
|
46
|
-
private fetchLiveScores;
|
|
47
|
-
private fetchOdds;
|
|
48
|
-
private fetchMatchEvents;
|
|
49
|
-
}
|
|
50
|
-
export declare function getRealtimeManager(): RealtimeDataManager;
|
|
51
|
-
export declare function resetRealtimeManager(): void;
|
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* REALTIME DATA MANAGER
|
|
3
|
-
* Polling-based real-time data streaming
|
|
4
|
-
*/
|
|
5
|
-
import { EventEmitter } from 'events';
|
|
6
|
-
import { CacheService } from './cache.js';
|
|
7
|
-
import { REALTIME_CONFIG, CACHE_CONFIG } from './constants.js';
|
|
8
|
-
// Simple logger fallback
|
|
9
|
-
const logger = {
|
|
10
|
-
info: (...args) => console.error('[INFO]', ...args),
|
|
11
|
-
warn: (...args) => console.warn('[WARN]', ...args),
|
|
12
|
-
error: (...args) => console.error('[ERROR]', ...args),
|
|
13
|
-
debug: (...args) => { }
|
|
14
|
-
};
|
|
15
|
-
export class RealtimeDataManager extends EventEmitter {
|
|
16
|
-
cache;
|
|
17
|
-
pollingIntervals = new Map();
|
|
18
|
-
isRunning = false;
|
|
19
|
-
constructor() {
|
|
20
|
-
super();
|
|
21
|
-
this.cache = new CacheService();
|
|
22
|
-
this.setMaxListeners(100);
|
|
23
|
-
}
|
|
24
|
-
async start() {
|
|
25
|
-
if (this.isRunning)
|
|
26
|
-
return;
|
|
27
|
-
this.isRunning = true;
|
|
28
|
-
logger.info('[RealtimeManager] Started');
|
|
29
|
-
this.startLiveScoresPolling();
|
|
30
|
-
this.startOddsPolling();
|
|
31
|
-
}
|
|
32
|
-
stop() {
|
|
33
|
-
this.isRunning = false;
|
|
34
|
-
for (const [key, interval] of this.pollingIntervals.entries()) {
|
|
35
|
-
clearInterval(interval);
|
|
36
|
-
}
|
|
37
|
-
this.pollingIntervals.clear();
|
|
38
|
-
logger.info('[RealtimeManager] Stopped');
|
|
39
|
-
}
|
|
40
|
-
subscribeToMatch(matchId, callback) {
|
|
41
|
-
const eventName = `match:${matchId}`;
|
|
42
|
-
this.on(eventName, callback);
|
|
43
|
-
this.startMatchEventsPolling(matchId);
|
|
44
|
-
return () => {
|
|
45
|
-
this.off(eventName, callback);
|
|
46
|
-
this.unsubscribeFromMatch(matchId);
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
subscribeToOdds(matchId, callback) {
|
|
50
|
-
const eventName = `odds:${matchId}`;
|
|
51
|
-
this.on(eventName, callback);
|
|
52
|
-
return () => this.off(eventName, callback);
|
|
53
|
-
}
|
|
54
|
-
subscribeToAllEvents(callback) {
|
|
55
|
-
this.on('event', callback);
|
|
56
|
-
return () => this.off('event', callback);
|
|
57
|
-
}
|
|
58
|
-
processEvent(event) {
|
|
59
|
-
this.updateCache(event);
|
|
60
|
-
this.emit(`match:${event.matchId}`, event);
|
|
61
|
-
if (event.type === 'odds_change') {
|
|
62
|
-
this.emit(`odds:${event.matchId}`, event);
|
|
63
|
-
}
|
|
64
|
-
this.emit('event', event);
|
|
65
|
-
}
|
|
66
|
-
getLiveMatches() {
|
|
67
|
-
const cached = this.cache.get('live:all');
|
|
68
|
-
return cached || [];
|
|
69
|
-
}
|
|
70
|
-
getMatchEvents(matchId) {
|
|
71
|
-
const cached = this.cache.get(`events:${matchId}`);
|
|
72
|
-
return cached || [];
|
|
73
|
-
}
|
|
74
|
-
unsubscribeFromMatch(matchId) {
|
|
75
|
-
const listenerCount = this.listenerCount(`match:${matchId}`);
|
|
76
|
-
if (listenerCount === 0) {
|
|
77
|
-
const interval = this.pollingIntervals.get(`events:${matchId}`);
|
|
78
|
-
if (interval) {
|
|
79
|
-
clearInterval(interval);
|
|
80
|
-
this.pollingIntervals.delete(`events:${matchId}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
startLiveScoresPolling() {
|
|
85
|
-
const interval = setInterval(async () => {
|
|
86
|
-
if (!this.isRunning)
|
|
87
|
-
return;
|
|
88
|
-
try {
|
|
89
|
-
const liveScores = await this.fetchLiveScores();
|
|
90
|
-
this.cache.set('live:all', liveScores, CACHE_CONFIG.TTL.LIVE_SCORES);
|
|
91
|
-
this.emit('live_scores', liveScores);
|
|
92
|
-
this.detectScoreChanges(liveScores);
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
logger.error('[Realtime] Failed to fetch live scores:', error);
|
|
96
|
-
}
|
|
97
|
-
}, REALTIME_CONFIG.POLLING_INTERVALS.LIVE_SCORES);
|
|
98
|
-
this.pollingIntervals.set('live_scores', interval);
|
|
99
|
-
}
|
|
100
|
-
startOddsPolling() {
|
|
101
|
-
const interval = setInterval(async () => {
|
|
102
|
-
if (!this.isRunning)
|
|
103
|
-
return;
|
|
104
|
-
try {
|
|
105
|
-
const liveMatches = this.getLiveMatches();
|
|
106
|
-
for (const match of liveMatches) {
|
|
107
|
-
const odds = await this.fetchOdds(match.id);
|
|
108
|
-
if (odds) {
|
|
109
|
-
const cachedOdds = this.cache.get(`odds:${match.id}`);
|
|
110
|
-
if (cachedOdds) {
|
|
111
|
-
this.detectOddsChanges(match.id, cachedOdds, odds);
|
|
112
|
-
}
|
|
113
|
-
this.cache.set(`odds:${match.id}`, odds, CACHE_CONFIG.TTL.ODDS);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
logger.error('[Realtime] Failed to fetch odds:', error);
|
|
119
|
-
}
|
|
120
|
-
}, REALTIME_CONFIG.POLLING_INTERVALS.ODDS);
|
|
121
|
-
this.pollingIntervals.set('odds', interval);
|
|
122
|
-
}
|
|
123
|
-
startMatchEventsPolling(matchId) {
|
|
124
|
-
if (this.pollingIntervals.has(`events:${matchId}`))
|
|
125
|
-
return;
|
|
126
|
-
const interval = setInterval(async () => {
|
|
127
|
-
if (!this.isRunning)
|
|
128
|
-
return;
|
|
129
|
-
try {
|
|
130
|
-
const events = await this.fetchMatchEvents(matchId);
|
|
131
|
-
const cachedEvents = this.getMatchEvents(matchId);
|
|
132
|
-
const newEvents = events.filter(e => !cachedEvents.some(ce => ce.timestamp === e.timestamp && ce.type === e.type));
|
|
133
|
-
for (const event of newEvents) {
|
|
134
|
-
this.processEvent(event);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
catch (error) {
|
|
138
|
-
logger.error(`[Realtime] Failed to fetch events for match ${matchId}:`, error);
|
|
139
|
-
}
|
|
140
|
-
}, REALTIME_CONFIG.POLLING_INTERVALS.MATCH_EVENTS);
|
|
141
|
-
this.pollingIntervals.set(`events:${matchId}`, interval);
|
|
142
|
-
}
|
|
143
|
-
detectScoreChanges(matches) {
|
|
144
|
-
for (const match of matches) {
|
|
145
|
-
const cachedMatch = this.cache.get(`match:${match.id}`);
|
|
146
|
-
if (cachedMatch && cachedMatch.score && match.score) {
|
|
147
|
-
if (match.score.home > cachedMatch.score.home) {
|
|
148
|
-
this.processEvent({
|
|
149
|
-
type: 'goal',
|
|
150
|
-
matchId: match.id,
|
|
151
|
-
timestamp: Date.now(),
|
|
152
|
-
minute: match.minute,
|
|
153
|
-
data: { team: 'home', score: match.score, previousScore: cachedMatch.score },
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
if (match.score.away > cachedMatch.score.away) {
|
|
157
|
-
this.processEvent({
|
|
158
|
-
type: 'goal',
|
|
159
|
-
matchId: match.id,
|
|
160
|
-
timestamp: Date.now(),
|
|
161
|
-
minute: match.minute,
|
|
162
|
-
data: { team: 'away', score: match.score, previousScore: cachedMatch.score },
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
this.cache.set(`match:${match.id}`, match, CACHE_CONFIG.TTL.MATCH_DETAILS);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
detectOddsChanges(matchId, oldOdds, newOdds) {
|
|
170
|
-
const markets = [
|
|
171
|
-
{ key: 'homeWin', name: 'Home Win' },
|
|
172
|
-
{ key: 'draw', name: 'Draw' },
|
|
173
|
-
{ key: 'awayWin', name: 'Away Win' },
|
|
174
|
-
];
|
|
175
|
-
for (const market of markets) {
|
|
176
|
-
const oldValue = oldOdds[market.key];
|
|
177
|
-
const newValue = newOdds[market.key];
|
|
178
|
-
if (oldValue && newValue && oldValue !== newValue) {
|
|
179
|
-
const change = ((newValue - oldValue) / oldValue) * 100;
|
|
180
|
-
if (Math.abs(change) > 5) {
|
|
181
|
-
this.processEvent({
|
|
182
|
-
type: 'odds_change',
|
|
183
|
-
matchId,
|
|
184
|
-
timestamp: Date.now(),
|
|
185
|
-
data: {
|
|
186
|
-
bookmaker: newOdds.provider,
|
|
187
|
-
market: market.name,
|
|
188
|
-
selection: market.key,
|
|
189
|
-
oldOdds: oldValue,
|
|
190
|
-
newOdds: newValue,
|
|
191
|
-
change,
|
|
192
|
-
timestamp: Date.now(),
|
|
193
|
-
},
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
updateCache(event) {
|
|
200
|
-
this.cache.invalidatePattern(`match:${event.matchId}*`);
|
|
201
|
-
const eventsKey = `events:${event.matchId}`;
|
|
202
|
-
const existingEvents = this.cache.get(eventsKey) || [];
|
|
203
|
-
existingEvents.push(event);
|
|
204
|
-
if (existingEvents.length > 100)
|
|
205
|
-
existingEvents.shift();
|
|
206
|
-
this.cache.set(eventsKey, existingEvents, CACHE_CONFIG.TTL.LIVE_EVENTS);
|
|
207
|
-
}
|
|
208
|
-
async fetchLiveScores() { return []; }
|
|
209
|
-
async fetchOdds(matchId) { return null; }
|
|
210
|
-
async fetchMatchEvents(matchId) { return []; }
|
|
211
|
-
}
|
|
212
|
-
let globalRealtimeManager = null;
|
|
213
|
-
export function getRealtimeManager() {
|
|
214
|
-
if (!globalRealtimeManager)
|
|
215
|
-
globalRealtimeManager = new RealtimeDataManager();
|
|
216
|
-
return globalRealtimeManager;
|
|
217
|
-
}
|
|
218
|
-
export function resetRealtimeManager() {
|
|
219
|
-
if (globalRealtimeManager)
|
|
220
|
-
globalRealtimeManager.stop();
|
|
221
|
-
globalRealtimeManager = null;
|
|
222
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RETRY MECHANISM
|
|
3
|
-
* Exponential backoff with jitter for resilient API calls
|
|
4
|
-
*/
|
|
5
|
-
export interface RetryConfig {
|
|
6
|
-
maxAttempts: number;
|
|
7
|
-
initialDelay: number;
|
|
8
|
-
maxDelay: number;
|
|
9
|
-
backoffMultiplier: number;
|
|
10
|
-
retryableErrors?: string[];
|
|
11
|
-
onRetry?: (attempt: number, error: Error, delay: number) => void;
|
|
12
|
-
}
|
|
13
|
-
export declare const DEFAULT_RETRY_CONFIG: RetryConfig;
|
|
14
|
-
export declare const AGGRESSIVE_RETRY_CONFIG: RetryConfig;
|
|
15
|
-
export declare const LIVE_DATA_RETRY_CONFIG: RetryConfig;
|
|
16
|
-
/**
|
|
17
|
-
* Execute a function with retry logic
|
|
18
|
-
*/
|
|
19
|
-
export declare function withRetry<T>(fn: () => Promise<T>, config?: Partial<RetryConfig>, context?: string): Promise<T>;
|
|
20
|
-
/**
|
|
21
|
-
* Retry with circuit breaker combination
|
|
22
|
-
*/
|
|
23
|
-
export declare function withRetryAndCircuitBreaker<T>(fn: () => Promise<T>, circuitBreaker: {
|
|
24
|
-
execute: <U>(fn: () => Promise<U>) => Promise<U>;
|
|
25
|
-
}, config?: Partial<RetryConfig>, context?: string): Promise<T>;
|
|
26
|
-
/**
|
|
27
|
-
* Create a retryable wrapper for a function
|
|
28
|
-
*/
|
|
29
|
-
export declare function createRetryable<T extends (...args: any[]) => Promise<any>>(fn: T, config?: Partial<RetryConfig>, context?: string): T;
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RETRY MECHANISM
|
|
3
|
-
* Exponential backoff with jitter for resilient API calls
|
|
4
|
-
*/
|
|
5
|
-
import { logger } from '../../../utils.js';
|
|
6
|
-
export const DEFAULT_RETRY_CONFIG = {
|
|
7
|
-
maxAttempts: 3,
|
|
8
|
-
initialDelay: 1000,
|
|
9
|
-
maxDelay: 30000,
|
|
10
|
-
backoffMultiplier: 2,
|
|
11
|
-
retryableErrors: ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', '503', '502', '504', '429'],
|
|
12
|
-
};
|
|
13
|
-
export const AGGRESSIVE_RETRY_CONFIG = {
|
|
14
|
-
maxAttempts: 5,
|
|
15
|
-
initialDelay: 500,
|
|
16
|
-
maxDelay: 60000,
|
|
17
|
-
backoffMultiplier: 2,
|
|
18
|
-
retryableErrors: ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', '503', '502', '504', '429', '500'],
|
|
19
|
-
};
|
|
20
|
-
export const LIVE_DATA_RETRY_CONFIG = {
|
|
21
|
-
maxAttempts: 3,
|
|
22
|
-
initialDelay: 200,
|
|
23
|
-
maxDelay: 5000,
|
|
24
|
-
backoffMultiplier: 1.5,
|
|
25
|
-
retryableErrors: ['ECONNRESET', 'ETIMEDOUT', '503', '504'],
|
|
26
|
-
};
|
|
27
|
-
function sleep(ms) {
|
|
28
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Execute a function with retry logic
|
|
32
|
-
*/
|
|
33
|
-
export async function withRetry(fn, config = {}, context) {
|
|
34
|
-
const fullConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
|
|
35
|
-
let lastError = null;
|
|
36
|
-
for (let attempt = 1; attempt <= fullConfig.maxAttempts; attempt++) {
|
|
37
|
-
try {
|
|
38
|
-
return await fn();
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
42
|
-
// Check if error is retryable
|
|
43
|
-
if (fullConfig.retryableErrors && !fullConfig.retryableErrors.some(e => lastError.message.includes(e) ||
|
|
44
|
-
lastError.code?.includes(e))) {
|
|
45
|
-
throw lastError;
|
|
46
|
-
}
|
|
47
|
-
if (attempt === fullConfig.maxAttempts) {
|
|
48
|
-
break;
|
|
49
|
-
}
|
|
50
|
-
// Calculate delay with exponential backoff and jitter
|
|
51
|
-
const delay = Math.min(fullConfig.initialDelay * Math.pow(fullConfig.backoffMultiplier, attempt - 1), fullConfig.maxDelay);
|
|
52
|
-
const jitteredDelay = delay * (0.8 + Math.random() * 0.4); // ±20% jitter
|
|
53
|
-
const contextStr = context ? `[${context}]` : '';
|
|
54
|
-
logger.warn(`[Retry${contextStr}] Attempt ${attempt}/${fullConfig.maxAttempts} failed: ${lastError.message}. Retrying in ${Math.round(jitteredDelay)}ms...`);
|
|
55
|
-
if (fullConfig.onRetry) {
|
|
56
|
-
fullConfig.onRetry(attempt, lastError, jitteredDelay);
|
|
57
|
-
}
|
|
58
|
-
await sleep(jitteredDelay);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
const contextStr = context ? `[${context}]` : '';
|
|
62
|
-
throw new Error(`[Retry${contextStr}] All ${fullConfig.maxAttempts} attempts failed. Last error: ${lastError?.message}`);
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Retry with circuit breaker combination
|
|
66
|
-
*/
|
|
67
|
-
export async function withRetryAndCircuitBreaker(fn, circuitBreaker, config, context) {
|
|
68
|
-
return circuitBreaker.execute(() => withRetry(fn, config, context));
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Create a retryable wrapper for a function
|
|
72
|
-
*/
|
|
73
|
-
export function createRetryable(fn, config, context) {
|
|
74
|
-
return (async (...args) => {
|
|
75
|
-
return withRetry(() => fn(...args), config, context);
|
|
76
|
-
});
|
|
77
|
-
}
|