@gotza02/sequential-thinking 2026.2.39 → 2026.2.41
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 +7 -4
- package/SYSTEM_INSTRUCTION.md +25 -50
- package/dist/tools/sports/core/base.d.ts +169 -0
- package/dist/tools/sports/core/base.js +289 -0
- package/dist/tools/sports/core/cache.d.ts +106 -0
- package/dist/tools/sports/core/cache.js +305 -0
- package/dist/tools/sports/core/constants.d.ts +179 -0
- package/dist/tools/sports/core/constants.js +149 -0
- package/dist/tools/sports/core/types.d.ts +379 -0
- package/dist/tools/sports/core/types.js +5 -0
- package/dist/tools/sports/index.d.ts +34 -0
- package/dist/tools/sports/index.js +50 -0
- package/dist/tools/sports/providers/api.d.ts +73 -0
- package/dist/tools/sports/providers/api.js +517 -0
- package/dist/tools/sports/providers/scraper.d.ts +66 -0
- package/dist/tools/sports/providers/scraper.js +186 -0
- package/dist/tools/sports/providers/search.d.ts +54 -0
- package/dist/tools/sports/providers/search.js +224 -0
- package/dist/tools/sports/tools/betting.d.ts +6 -0
- package/dist/tools/sports/tools/betting.js +251 -0
- package/dist/tools/sports/tools/league.d.ts +11 -0
- package/dist/tools/sports/tools/league.js +12 -0
- package/dist/tools/sports/tools/live.d.ts +9 -0
- package/dist/tools/sports/tools/live.js +235 -0
- package/dist/tools/sports/tools/match.d.ts +6 -0
- package/dist/tools/sports/tools/match.js +323 -0
- package/dist/tools/sports/tools/player.d.ts +6 -0
- package/dist/tools/sports/tools/player.js +152 -0
- package/dist/tools/sports/tools/team.d.ts +6 -0
- package/dist/tools/sports/tools/team.js +370 -0
- package/dist/tools/sports/utils/calculator.d.ts +69 -0
- package/dist/tools/sports/utils/calculator.js +156 -0
- package/dist/tools/sports/utils/formatter.d.ts +57 -0
- package/dist/tools/sports/utils/formatter.js +206 -0
- package/dist/tools/sports.d.ts +7 -0
- package/dist/tools/sports.js +27 -6
- package/package.json +1 -1
- package/system_instruction.md +155 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPORTS MODULE TOOLS - Team Tools
|
|
3
|
+
* Tools for team analysis, comparison, and league standings
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { createAPIProvider } from '../providers/api.js';
|
|
7
|
+
import { getSearchProvider } from '../providers/search.js';
|
|
8
|
+
import { getGlobalCache, CacheService } from '../core/cache.js';
|
|
9
|
+
import { formatStandingsTable, formatComparisonResult } from '../utils/formatter.js';
|
|
10
|
+
import { CACHE_CONFIG, LEAGUES } from '../core/constants.js';
|
|
11
|
+
// ============= Helper Functions =============
|
|
12
|
+
/**
|
|
13
|
+
* Get league ID from league name
|
|
14
|
+
*/
|
|
15
|
+
function getLeagueId(leagueName) {
|
|
16
|
+
const normalized = leagueName.toLowerCase();
|
|
17
|
+
for (const [key, value] of Object.entries(LEAGUES)) {
|
|
18
|
+
if (value.name.toLowerCase().includes(normalized) ||
|
|
19
|
+
normalized.includes(value.name.toLowerCase()) ||
|
|
20
|
+
value.code?.toLowerCase() === normalized.replace(' ', '')) {
|
|
21
|
+
return value.id;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Try direct name match
|
|
25
|
+
for (const [key, value] of Object.entries(LEAGUES)) {
|
|
26
|
+
if (value.name.toLowerCase() === normalized) {
|
|
27
|
+
return value.id;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Search for team by name using web search
|
|
34
|
+
*/
|
|
35
|
+
async function searchTeamByName(teamName) {
|
|
36
|
+
try {
|
|
37
|
+
const searchProvider = getSearchProvider();
|
|
38
|
+
const results = await searchProvider.search(`${teamName} football team stats`, undefined, 3);
|
|
39
|
+
if (results.length > 0) {
|
|
40
|
+
// Return a basic team object from search results
|
|
41
|
+
return {
|
|
42
|
+
id: `search_${Date.now()}`,
|
|
43
|
+
name: teamName,
|
|
44
|
+
country: 'Unknown',
|
|
45
|
+
stats: {
|
|
46
|
+
matchesPlayed: 0,
|
|
47
|
+
wins: 0,
|
|
48
|
+
draws: 0,
|
|
49
|
+
losses: 0,
|
|
50
|
+
goalsFor: 0,
|
|
51
|
+
goalsAgainst: 0,
|
|
52
|
+
goalDifference: 0,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Ignore search errors
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Compare two teams and generate analysis
|
|
64
|
+
*/
|
|
65
|
+
async function compareTeamsAnalysis(teamAName, teamBName) {
|
|
66
|
+
const metrics = [];
|
|
67
|
+
const errors = [];
|
|
68
|
+
try {
|
|
69
|
+
const api = createAPIProvider();
|
|
70
|
+
// Try to get team data from API
|
|
71
|
+
const [teamAResult, teamBResult] = await Promise.all([
|
|
72
|
+
api.searchTeams(teamAName),
|
|
73
|
+
api.searchTeams(teamBName),
|
|
74
|
+
]);
|
|
75
|
+
const teamA = teamAResult.success && teamAResult.data && teamAResult.data.length > 0
|
|
76
|
+
? teamAResult.data[0]
|
|
77
|
+
: { id: 'a', name: teamAName, country: '', stats: undefined };
|
|
78
|
+
const teamB = teamBResult.success && teamBResult.data && teamBResult.data.length > 0
|
|
79
|
+
? teamBResult.data[0]
|
|
80
|
+
: { id: 'b', name: teamBName, country: '', stats: undefined };
|
|
81
|
+
// Generate comparison metrics
|
|
82
|
+
// Note: These would be populated with real API data
|
|
83
|
+
metrics.push({
|
|
84
|
+
name: 'Team Name',
|
|
85
|
+
teamAValue: teamA.name,
|
|
86
|
+
teamBValue: teamB.name,
|
|
87
|
+
importance: 'low',
|
|
88
|
+
});
|
|
89
|
+
if (teamA.stats && teamB.stats) {
|
|
90
|
+
metrics.push({
|
|
91
|
+
name: 'League Position',
|
|
92
|
+
teamAValue: teamA.stats.position || 'N/A',
|
|
93
|
+
teamBValue: teamB.stats.position || 'N/A',
|
|
94
|
+
winner: teamA.stats.position && teamB.stats.position
|
|
95
|
+
? teamA.stats.position < teamB.stats.position ? 'A' : teamA.stats.position > teamB.stats.position ? 'B' : 'draw'
|
|
96
|
+
: undefined,
|
|
97
|
+
importance: 'high',
|
|
98
|
+
});
|
|
99
|
+
metrics.push({
|
|
100
|
+
name: 'Form (Last 5)',
|
|
101
|
+
teamAValue: teamA.stats.form
|
|
102
|
+
? Array.isArray(teamA.stats.form)
|
|
103
|
+
? teamA.stats.form.join('')
|
|
104
|
+
: teamA.stats.form
|
|
105
|
+
: 'N/A',
|
|
106
|
+
teamBValue: teamB.stats.form
|
|
107
|
+
? Array.isArray(teamB.stats.form)
|
|
108
|
+
? teamB.stats.form.join('')
|
|
109
|
+
: teamB.stats.form
|
|
110
|
+
: 'N/A',
|
|
111
|
+
importance: 'medium',
|
|
112
|
+
});
|
|
113
|
+
metrics.push({
|
|
114
|
+
name: 'Goals Per Game',
|
|
115
|
+
teamAValue: teamA.stats.matchesPlayed > 0
|
|
116
|
+
? (teamA.stats.goalsFor / teamA.stats.matchesPlayed).toFixed(2)
|
|
117
|
+
: 'N/A',
|
|
118
|
+
teamBValue: teamB.stats.matchesPlayed > 0
|
|
119
|
+
? (teamB.stats.goalsFor / teamB.stats.matchesPlayed).toFixed(2)
|
|
120
|
+
: 'N/A',
|
|
121
|
+
winner: teamA.stats.matchesPlayed > 0 && teamB.stats.matchesPlayed > 0
|
|
122
|
+
? (teamA.stats.goalsFor / teamA.stats.matchesPlayed) > (teamB.stats.goalsFor / teamB.stats.matchesPlayed) ? 'A' : 'B'
|
|
123
|
+
: undefined,
|
|
124
|
+
importance: 'high',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// Generate summary
|
|
128
|
+
let summary = `Comparison between **${teamA.name}** and **${teamB.name}**.\n\n`;
|
|
129
|
+
if (teamA.stats && teamB.stats) {
|
|
130
|
+
const aPoints = teamA.stats.points || 0;
|
|
131
|
+
const bPoints = teamB.stats.points || 0;
|
|
132
|
+
if (aPoints > bPoints + 10) {
|
|
133
|
+
summary += `${teamA.name} has a significantly better league position. `;
|
|
134
|
+
}
|
|
135
|
+
else if (bPoints > aPoints + 10) {
|
|
136
|
+
summary += `${teamB.name} has a significantly better league position. `;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
summary += `The teams are relatively evenly matched in terms of league standing. `;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
summary += `Detailed statistics not available via API. `;
|
|
144
|
+
}
|
|
145
|
+
summary += `\n\n**Note:** Full comparison requires API provider configuration.`;
|
|
146
|
+
return {
|
|
147
|
+
teamA,
|
|
148
|
+
teamB,
|
|
149
|
+
metrics,
|
|
150
|
+
summary,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
155
|
+
// Fallback to basic comparison
|
|
156
|
+
return {
|
|
157
|
+
teamA: { id: 'a', name: teamAName, country: '' },
|
|
158
|
+
teamB: { id: 'b', name: teamBName, country: '' },
|
|
159
|
+
metrics: [
|
|
160
|
+
{
|
|
161
|
+
name: 'Team Name',
|
|
162
|
+
teamAValue: teamAName,
|
|
163
|
+
teamBValue: teamBName,
|
|
164
|
+
importance: 'low',
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
summary: `Basic comparison between ${teamAName} and ${teamBName}. API providers not configured.\n\nErrors: ${errors.join(', ')}`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// ============= Tool Registration =============
|
|
172
|
+
export function registerTeamTools(server) {
|
|
173
|
+
/**
|
|
174
|
+
* Tool 1: compare_teams
|
|
175
|
+
* Compare two teams across multiple metrics
|
|
176
|
+
*/
|
|
177
|
+
server.tool('compare_teams', `Compare two football teams across multiple metrics.
|
|
178
|
+
Analyzes: form, head-to-head, xG, possession, goals, defense, home/away records.`, {
|
|
179
|
+
teamA: z.string().describe('First team name'),
|
|
180
|
+
teamB: z.string().describe('Second team name'),
|
|
181
|
+
metrics: z.array(z.enum(['form', 'h2h', 'xg', 'possession', 'goals', 'defense', 'home-away']))
|
|
182
|
+
.optional().default(['form', 'h2h', 'goals'])
|
|
183
|
+
.describe('Metrics to compare'),
|
|
184
|
+
}, async ({ teamA, teamB, metrics }) => {
|
|
185
|
+
const result = await compareTeamsAnalysis(teamA, teamB);
|
|
186
|
+
const output = formatComparisonResult(result);
|
|
187
|
+
return {
|
|
188
|
+
content: [{ type: 'text', text: output }],
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
/**
|
|
192
|
+
* Tool 2: get_league_standings
|
|
193
|
+
* Get current league standings table
|
|
194
|
+
*/
|
|
195
|
+
server.tool('get_league_standings', `Get current league standings table.
|
|
196
|
+
Includes: position, played, won, drawn, lost, goals for/against, goal difference, points.
|
|
197
|
+
Supports home/away splits.`, {
|
|
198
|
+
league: z.string().describe('League name (e.g., "Premier League", "La Liga")'),
|
|
199
|
+
season: z.string().optional().describe('Season year (default: current)'),
|
|
200
|
+
}, async ({ league, season }) => {
|
|
201
|
+
const cache = getGlobalCache();
|
|
202
|
+
const currentYear = new Date().getFullYear();
|
|
203
|
+
const seasonYear = season || currentYear.toString();
|
|
204
|
+
const cacheKey = CacheService.generateKey('standings', league, seasonYear);
|
|
205
|
+
// Check cache first (30min TTL)
|
|
206
|
+
const cached = cache.get(cacheKey);
|
|
207
|
+
if (cached) {
|
|
208
|
+
return {
|
|
209
|
+
content: [{ type: 'text', text: cached + '\n\n*(cached data)*' }],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const leagueId = getLeagueId(league);
|
|
213
|
+
let output = '';
|
|
214
|
+
const errors = [];
|
|
215
|
+
if (!leagueId) {
|
|
216
|
+
errors.push(`League "${league}" not found. Available leagues: ${Object.values(LEAGUES).map(l => l.name).join(', ')}`);
|
|
217
|
+
}
|
|
218
|
+
if (leagueId) {
|
|
219
|
+
try {
|
|
220
|
+
const api = createAPIProvider();
|
|
221
|
+
const result = await api.getStandings(leagueId.toString(), seasonYear);
|
|
222
|
+
if (result.success && result.data) {
|
|
223
|
+
output = `## 🏆 ${league} - Standings ${seasonYear}\n\n`;
|
|
224
|
+
output += formatStandingsTable(result.data);
|
|
225
|
+
// Cache for 30 minutes
|
|
226
|
+
cache.set(cacheKey, output, CACHE_CONFIG.TTL.STANDINGS);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
errors.push(result.error || 'Unknown API error');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Fallback to web search if API failed
|
|
237
|
+
if (!output && errors.length > 0) {
|
|
238
|
+
try {
|
|
239
|
+
const searchQuery = `${league} standings table ${seasonYear}`;
|
|
240
|
+
const searchProvider = getSearchProvider();
|
|
241
|
+
const searchResults = await searchProvider.search(searchQuery, undefined, 3);
|
|
242
|
+
const fallbackText = searchResults.map(r => `- [${r.title}](${r.url}): ${r.snippet}`).join('\n');
|
|
243
|
+
output = `## ${league} - Standings ${seasonYear}\n\n`;
|
|
244
|
+
output += `*API providers not available. Click links for standings:*\n\n${fallbackText}`;
|
|
245
|
+
}
|
|
246
|
+
catch (searchError) {
|
|
247
|
+
output = `Error: ${errors.join(', ')}`;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
content: [{ type: 'text', text: output }],
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
/**
|
|
255
|
+
* Tool 3: get_team_info
|
|
256
|
+
* Get detailed information about a team
|
|
257
|
+
*/
|
|
258
|
+
server.tool('get_team_info', `Get detailed team information including stats, squad, and recent form.`, {
|
|
259
|
+
team: z.string().describe('Team name'),
|
|
260
|
+
}, async ({ team }) => {
|
|
261
|
+
const cache = getGlobalCache();
|
|
262
|
+
const cacheKey = CacheService.generateKey('team_info', team);
|
|
263
|
+
// Check cache first (1 hour TTL)
|
|
264
|
+
const cached = cache.get(cacheKey);
|
|
265
|
+
if (cached) {
|
|
266
|
+
return {
|
|
267
|
+
content: [{ type: 'text', text: cached + '\n\n*(cached data)*' }],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
let output = '';
|
|
271
|
+
const errors = [];
|
|
272
|
+
try {
|
|
273
|
+
const api = createAPIProvider();
|
|
274
|
+
const searchResult = await api.searchTeams(team);
|
|
275
|
+
if (searchResult.success && searchResult.data && searchResult.data.length > 0) {
|
|
276
|
+
const teamData = searchResult.data[0];
|
|
277
|
+
output = `## ⚽ ${teamData.name}\n\n`;
|
|
278
|
+
output += `**Country:** ${teamData.country}\n`;
|
|
279
|
+
if (teamData.founded)
|
|
280
|
+
output += `**Founded:** ${teamData.founded}\n`;
|
|
281
|
+
if (teamData.venue)
|
|
282
|
+
output += `**Stadium:** ${teamData.venue}\n`;
|
|
283
|
+
if (teamData.logo)
|
|
284
|
+
output += `**Logo:** ${teamData.logo}\n`;
|
|
285
|
+
if (teamData.stats) {
|
|
286
|
+
output += `\n### Season Stats\n`;
|
|
287
|
+
output += `| Metric | Value |\n|--------|-------|\n`;
|
|
288
|
+
output += `| Played | ${teamData.stats.matchesPlayed} |\n`;
|
|
289
|
+
output += `| Wins | ${teamData.stats.wins} |\n`;
|
|
290
|
+
output += `| Draws | ${teamData.stats.draws} |\n`;
|
|
291
|
+
output += `| Losses | ${teamData.stats.losses} |\n`;
|
|
292
|
+
output += `| Goals For/Against | ${teamData.stats.goalsFor}/${teamData.stats.goalsAgainst} |\n`;
|
|
293
|
+
output += `| Goal Difference | ${teamData.stats.goalDifference} |\n`;
|
|
294
|
+
if (teamData.stats.points !== undefined)
|
|
295
|
+
output += `| Points | ${teamData.stats.points} |\n`;
|
|
296
|
+
}
|
|
297
|
+
// Cache for 1 hour
|
|
298
|
+
cache.set(cacheKey, output, CACHE_CONFIG.TTL.TEAM_STATS);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
errors.push('Team not found via API');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
306
|
+
}
|
|
307
|
+
// Fallback to web search
|
|
308
|
+
if (!output) {
|
|
309
|
+
try {
|
|
310
|
+
const searchQuery = `${team} football team stats squad`;
|
|
311
|
+
const searchProvider = getSearchProvider();
|
|
312
|
+
const searchResults = await searchProvider.search(searchQuery, undefined, 3);
|
|
313
|
+
const fallbackText = searchResults.map(r => `- [${r.title}](${r.url}): ${r.snippet}`).join('\n');
|
|
314
|
+
output = `## ⚽ ${team}\n\n`;
|
|
315
|
+
output += `*API data not available. Click links for team info:*\n\n${fallbackText}`;
|
|
316
|
+
}
|
|
317
|
+
catch (searchError) {
|
|
318
|
+
output = `Error: ${errors.join(', ')}`;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
content: [{ type: 'text', text: output }],
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
/**
|
|
326
|
+
* Tool 4: get_top_scorers
|
|
327
|
+
* Get top scorers and assists for a league
|
|
328
|
+
*/
|
|
329
|
+
server.tool('get_top_scorers', `Get top scorers and assists for a league.`, {
|
|
330
|
+
league: z.string().describe('League name'),
|
|
331
|
+
type: z.enum(['scorers', 'assists', 'both']).optional().default('both').describe('Statistics type'),
|
|
332
|
+
limit: z.number().optional().default(20).describe('Number of players to return'),
|
|
333
|
+
}, async ({ league, type, limit }) => {
|
|
334
|
+
const leagueId = getLeagueId(league);
|
|
335
|
+
let output = '';
|
|
336
|
+
const errors = [];
|
|
337
|
+
if (!leagueId) {
|
|
338
|
+
errors.push(`League "${league}" not found`);
|
|
339
|
+
}
|
|
340
|
+
// Try API first
|
|
341
|
+
if (leagueId) {
|
|
342
|
+
try {
|
|
343
|
+
const api = createAPIProvider();
|
|
344
|
+
// Note: This would require a dedicated API endpoint for top scorers
|
|
345
|
+
// For now, use web search as fallback
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Use web search
|
|
352
|
+
try {
|
|
353
|
+
const searchQuery = `${league} top ${type === 'assists' ? 'assists' : 'scorers'} ${new Date().getFullYear()}`;
|
|
354
|
+
const searchProvider = getSearchProvider();
|
|
355
|
+
const searchResults = await searchProvider.search(searchQuery, undefined, limit);
|
|
356
|
+
output = `## 🥇 ${league} - Top ${type === 'assists' ? 'Assists' : 'Scorers'}\n\n`;
|
|
357
|
+
const items = searchResults.map((r, i) => `${i + 1}. [${r.title}](${r.url}): ${r.snippet}`).join('\n');
|
|
358
|
+
output += items;
|
|
359
|
+
if (searchResults.length === 0) {
|
|
360
|
+
output += `\nNo results found.`;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
output = `Error: ${errors.join(', ')}`;
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
content: [{ type: 'text', text: output }],
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPORTS MODULE UTILS - Calculator
|
|
3
|
+
* Betting calculators and mathematical utilities
|
|
4
|
+
*/
|
|
5
|
+
import { BetType, KellyVariant, ValueBet } from '../core/types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Calculate value of a bet
|
|
8
|
+
* @param odds - Decimal odds
|
|
9
|
+
* @param probability - Estimated probability (0-1)
|
|
10
|
+
* @returns Value as percentage (e.g., 0.15 = 15%)
|
|
11
|
+
*/
|
|
12
|
+
export declare function calculateValue(odds: number, probability: number): number;
|
|
13
|
+
/**
|
|
14
|
+
* Calculate Kelly Criterion stake
|
|
15
|
+
* @param odds - Decimal odds
|
|
16
|
+
* @param probability - Estimated probability (0-1)
|
|
17
|
+
* @param variant - Kelly variant (full, half, quarter)
|
|
18
|
+
* @returns Fraction of bankroll to stake (0-1)
|
|
19
|
+
*/
|
|
20
|
+
export declare function calculateKelly(odds: number, probability: number, variant?: KellyVariant): number;
|
|
21
|
+
/**
|
|
22
|
+
* Calculate recommended stake based on bankroll
|
|
23
|
+
* @param odds - Decimal odds
|
|
24
|
+
* @param probability - Estimated probability (0-1)
|
|
25
|
+
* @param bankroll - Total bankroll
|
|
26
|
+
* @param kellyVariant - Kelly variant
|
|
27
|
+
* @param maxStakePct - Maximum stake as percentage of bankroll
|
|
28
|
+
* @returns Recommended stake amount
|
|
29
|
+
*/
|
|
30
|
+
export declare function calculateRecommendedStake(odds: number, probability: number, bankroll?: number, kellyVariant?: KellyVariant, maxStakePct?: number): number;
|
|
31
|
+
/**
|
|
32
|
+
* Create a value bet analysis
|
|
33
|
+
* @param odds - Decimal odds
|
|
34
|
+
* @param probability - Estimated probability (0-1)
|
|
35
|
+
* @param betType - Type of bet
|
|
36
|
+
* @param bankroll - Total bankroll
|
|
37
|
+
* @returns Value bet analysis
|
|
38
|
+
*/
|
|
39
|
+
export declare function analyzeValueBet(odds: number, probability: number, betType: BetType, bankroll?: number): Omit<ValueBet, 'match' | 'reasoning'>;
|
|
40
|
+
/**
|
|
41
|
+
* Convert fractional odds to decimal
|
|
42
|
+
*/
|
|
43
|
+
export declare function fractionalToDecimal(fractional: string): number;
|
|
44
|
+
/**
|
|
45
|
+
* Convert decimal odds to fractional
|
|
46
|
+
*/
|
|
47
|
+
export declare function decimalToFractional(decimal: number): string;
|
|
48
|
+
/**
|
|
49
|
+
* Convert American odds to decimal
|
|
50
|
+
*/
|
|
51
|
+
export declare function americanToDecimal(american: number): number;
|
|
52
|
+
/**
|
|
53
|
+
* Convert decimal odds to American
|
|
54
|
+
*/
|
|
55
|
+
export declare function decimalToAmerican(decimal: number): number;
|
|
56
|
+
/**
|
|
57
|
+
* Calculate implied probability from decimal odds
|
|
58
|
+
*/
|
|
59
|
+
export declare function oddsToProbability(odds: number): number;
|
|
60
|
+
/**
|
|
61
|
+
* Calculate overround (bookmaker margin)
|
|
62
|
+
* @param odds - Array of decimal odds for all outcomes
|
|
63
|
+
* @returns Overround as percentage
|
|
64
|
+
*/
|
|
65
|
+
export declare function calculateOverround(odds: number[]): number;
|
|
66
|
+
/**
|
|
67
|
+
* Calculate true probability after removing overround
|
|
68
|
+
*/
|
|
69
|
+
export declare function removeOverround(odds: number[]): number[];
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPORTS MODULE UTILS - Calculator
|
|
3
|
+
* Betting calculators and mathematical utilities
|
|
4
|
+
*/
|
|
5
|
+
import { BETTING } from '../core/constants.js';
|
|
6
|
+
/**
|
|
7
|
+
* Calculate value of a bet
|
|
8
|
+
* @param odds - Decimal odds
|
|
9
|
+
* @param probability - Estimated probability (0-1)
|
|
10
|
+
* @returns Value as percentage (e.g., 0.15 = 15%)
|
|
11
|
+
*/
|
|
12
|
+
export function calculateValue(odds, probability) {
|
|
13
|
+
return (odds * probability) - 1;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Calculate Kelly Criterion stake
|
|
17
|
+
* @param odds - Decimal odds
|
|
18
|
+
* @param probability - Estimated probability (0-1)
|
|
19
|
+
* @param variant - Kelly variant (full, half, quarter)
|
|
20
|
+
* @returns Fraction of bankroll to stake (0-1)
|
|
21
|
+
*/
|
|
22
|
+
export function calculateKelly(odds, probability, variant = 'half') {
|
|
23
|
+
const kelly = (probability * odds - 1) / (odds - 1);
|
|
24
|
+
// Kelly can be negative (no value), return 0 in that case
|
|
25
|
+
if (kelly < 0)
|
|
26
|
+
return 0;
|
|
27
|
+
switch (variant) {
|
|
28
|
+
case 'full':
|
|
29
|
+
return Math.min(kelly, 1);
|
|
30
|
+
case 'half':
|
|
31
|
+
return Math.min(kelly / 2, 1);
|
|
32
|
+
case 'quarter':
|
|
33
|
+
return Math.min(kelly / 4, 1);
|
|
34
|
+
default:
|
|
35
|
+
return Math.min(kelly / 2, 1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Calculate recommended stake based on bankroll
|
|
40
|
+
* @param odds - Decimal odds
|
|
41
|
+
* @param probability - Estimated probability (0-1)
|
|
42
|
+
* @param bankroll - Total bankroll
|
|
43
|
+
* @param kellyVariant - Kelly variant
|
|
44
|
+
* @param maxStakePct - Maximum stake as percentage of bankroll
|
|
45
|
+
* @returns Recommended stake amount
|
|
46
|
+
*/
|
|
47
|
+
export function calculateRecommendedStake(odds, probability, bankroll = BETTING.DEFAULT_BANKROLL, kellyVariant = 'half', maxStakePct = BETTING.MAX_STAKE_PERCENTAGE) {
|
|
48
|
+
const kelly = calculateKelly(odds, probability, kellyVariant);
|
|
49
|
+
const kellyStake = bankroll * kelly;
|
|
50
|
+
const maxStake = bankroll * maxStakePct;
|
|
51
|
+
return Math.min(kellyStake, maxStake);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create a value bet analysis
|
|
55
|
+
* @param odds - Decimal odds
|
|
56
|
+
* @param probability - Estimated probability (0-1)
|
|
57
|
+
* @param betType - Type of bet
|
|
58
|
+
* @param bankroll - Total bankroll
|
|
59
|
+
* @returns Value bet analysis
|
|
60
|
+
*/
|
|
61
|
+
export function analyzeValueBet(odds, probability, betType, bankroll = BETTING.DEFAULT_BANKROLL) {
|
|
62
|
+
const value = calculateValue(odds, probability);
|
|
63
|
+
const kellyFraction = calculateKelly(odds, probability, 'half');
|
|
64
|
+
const recommendedStake = calculateRecommendedStake(odds, probability, bankroll);
|
|
65
|
+
// Determine confidence based on value and probability
|
|
66
|
+
let confidence;
|
|
67
|
+
if (value >= BETTING.HIGH_VALUE_THRESHOLD && probability >= 0.5) {
|
|
68
|
+
confidence = BETTING.CONFIDENCE.VERY_HIGH;
|
|
69
|
+
}
|
|
70
|
+
else if (value >= BETTING.VALUE_THRESHOLD && probability >= 0.4) {
|
|
71
|
+
confidence = BETTING.CONFIDENCE.HIGH;
|
|
72
|
+
}
|
|
73
|
+
else if (value >= 0 && probability >= 0.3) {
|
|
74
|
+
confidence = BETTING.CONFIDENCE.MEDIUM;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
confidence = BETTING.CONFIDENCE.LOW;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
betType,
|
|
81
|
+
fairOdds: 1 / probability,
|
|
82
|
+
marketOdds: odds,
|
|
83
|
+
value,
|
|
84
|
+
confidence,
|
|
85
|
+
kellyFraction,
|
|
86
|
+
recommendedStake,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Convert fractional odds to decimal
|
|
91
|
+
*/
|
|
92
|
+
export function fractionalToDecimal(fractional) {
|
|
93
|
+
const [numerator, denominator] = fractional.split('/').map(Number);
|
|
94
|
+
if (denominator === 0)
|
|
95
|
+
return 0;
|
|
96
|
+
return (numerator / denominator) + 1;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Convert decimal odds to fractional
|
|
100
|
+
*/
|
|
101
|
+
export function decimalToFractional(decimal) {
|
|
102
|
+
const numerator = decimal - 1;
|
|
103
|
+
const denominator = 1;
|
|
104
|
+
// Find common denominator
|
|
105
|
+
const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
|
|
106
|
+
const num = Math.round(numerator * 100);
|
|
107
|
+
const den = 100;
|
|
108
|
+
const divisor = gcd(num, den);
|
|
109
|
+
return `${num / divisor}/${den / divisor}`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Convert American odds to decimal
|
|
113
|
+
*/
|
|
114
|
+
export function americanToDecimal(american) {
|
|
115
|
+
if (american > 0) {
|
|
116
|
+
return (american / 100) + 1;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
return (100 / Math.abs(american)) + 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Convert decimal odds to American
|
|
124
|
+
*/
|
|
125
|
+
export function decimalToAmerican(decimal) {
|
|
126
|
+
if (decimal >= 2) {
|
|
127
|
+
return Math.round((decimal - 1) * 100);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
return Math.round(-100 / (decimal - 1));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Calculate implied probability from decimal odds
|
|
135
|
+
*/
|
|
136
|
+
export function oddsToProbability(odds) {
|
|
137
|
+
return 1 / odds;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Calculate overround (bookmaker margin)
|
|
141
|
+
* @param odds - Array of decimal odds for all outcomes
|
|
142
|
+
* @returns Overround as percentage
|
|
143
|
+
*/
|
|
144
|
+
export function calculateOverround(odds) {
|
|
145
|
+
const impliedProbability = odds.reduce((sum, odd) => sum + oddsToProbability(odd), 0);
|
|
146
|
+
return impliedProbability - 1;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Calculate true probability after removing overround
|
|
150
|
+
*/
|
|
151
|
+
export function removeOverround(odds) {
|
|
152
|
+
const overround = calculateOverround(odds);
|
|
153
|
+
const probabilities = odds.map(o => oddsToProbability(o));
|
|
154
|
+
const total = probabilities.reduce((sum, p) => sum + p, 0);
|
|
155
|
+
return probabilities.map(p => p / total);
|
|
156
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPORTS MODULE UTILS - Formatter
|
|
3
|
+
* Output formatting utilities for sports data
|
|
4
|
+
*/
|
|
5
|
+
import { Match, TableEntry, PlayerStats, ComparisonResult, HeadToHead } from '../core/types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Format a match as markdown table row
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatMatchRow(match: Match): string;
|
|
10
|
+
/**
|
|
11
|
+
* Format standings as markdown table
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatStandingsTable(standings: TableEntry[]): string;
|
|
14
|
+
/**
|
|
15
|
+
* Format player stats as markdown table
|
|
16
|
+
*/
|
|
17
|
+
export declare function formatPlayerStats(stats: PlayerStats, playerName: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Format comparison result as markdown
|
|
20
|
+
*/
|
|
21
|
+
export declare function formatComparisonResult(result: ComparisonResult): string;
|
|
22
|
+
/**
|
|
23
|
+
* Format head to head record as markdown
|
|
24
|
+
*/
|
|
25
|
+
export declare function formatHeadToHead(h2h: HeadToHead): string;
|
|
26
|
+
/**
|
|
27
|
+
* Format match status for display
|
|
28
|
+
*/
|
|
29
|
+
export declare function formatMatchStatus(status: Match['status']): string;
|
|
30
|
+
/**
|
|
31
|
+
* Format score with context
|
|
32
|
+
*/
|
|
33
|
+
export declare function formatScore(match: Match): string;
|
|
34
|
+
/**
|
|
35
|
+
* Format odds as decimal/fractional
|
|
36
|
+
*/
|
|
37
|
+
export declare function formatOdds(decimalOdds: number): string;
|
|
38
|
+
/**
|
|
39
|
+
* Format probability as percentage
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatProbability(probability: number): string;
|
|
42
|
+
/**
|
|
43
|
+
* Format value bet recommendation
|
|
44
|
+
*/
|
|
45
|
+
export declare function formatValueBet(match: string, odds: number, probability: number, value: number, confidence: number): string;
|
|
46
|
+
/**
|
|
47
|
+
* Format a list of matches as markdown table
|
|
48
|
+
*/
|
|
49
|
+
export declare function formatMatchesTable(matches: Match[], title: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Truncate text to max length with ellipsis
|
|
52
|
+
*/
|
|
53
|
+
export declare function truncate(text: string, maxLength?: number): string;
|
|
54
|
+
/**
|
|
55
|
+
* Format date relative to now
|
|
56
|
+
*/
|
|
57
|
+
export declare function formatRelativeDate(date: Date): string;
|