"""
Command-line interface for the SlickBet betting screener.
Entry points for screening fixtures, listing competitions, backtesting,
hyperparameter tuning, PDF/JSON export, and API debug. Invoked as ``slickbet``.
"""
import argparse
import sys
from datetime import date, datetime, time
from slickbet.api import LivescoreAPIError, LivescoreClient
from slickbet.backtest import Backtester, BacktestResults, format_backtest_report
from slickbet.pdf_export import (
export_backtest_all_to_pdf,
export_backtest_to_pdf,
export_screener_to_pdf,
)
from slickbet.screener import (
BetPrediction,
BettingScreener,
ScreenerConfig,
ScreenerResult,
format_prediction,
format_summary,
)
from slickbet.tune import format_best_weights, tune
[docs]
def create_parser() -> argparse.ArgumentParser:
"""
Create the argument parser for the CLI.
Returns
-------
argparse.ArgumentParser
Configured parser with screener, backtest, tune, and debug subcommands.
"""
parser = argparse.ArgumentParser(
prog="slickbet",
description="⚽ SlickBet - Soccer Betting Screener",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
slickbet # Screen tomorrow's games, show top 5
slickbet --top 10 # Show top 10 betting opportunities
slickbet --date 2025-02-15 # Screen games for specific date
slickbet --min-prob 0.60 # Only show bets with >60% probability
slickbet --country England # Filter by country
slickbet --no-stats # Fast mode (skip detailed stats)
slickbet backtest # Backtest on Premier League (4 weeks)
slickbet backtest --weeks 8 # Backtest on 8 weeks of data
slickbet backtest --competition 3 # Backtest on La Liga
Environment Variables:
LIVESCORE_API_KEY Your Livescore API key
LIVESCORE_API_SECRET Your Livescore API secret
""",
)
# Subcommands
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# Competitions subcommand
comp_parser = subparsers.add_parser(
"competitions",
help="List available competitions and their IDs",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
slickbet competitions # List all competitions
slickbet competitions --country England # English competitions
slickbet competitions --search "Premier" # Search by name
""",
)
comp_parser.add_argument("--country", type=str, metavar="NAME", help="Filter by country name")
comp_parser.add_argument(
"-s", "--search", type=str, metavar="TERM", help="Search competition names"
)
# Backtest subcommand
backtest_parser = subparsers.add_parser(
"backtest",
help="Backtest the model against historical data",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Competition IDs:
1 = Bundesliga (Germany)
2 = Premier League (England)
3 = La Liga (Spain)
4 = Serie A (Italy)
5 = Ligue 1 (France)
Examples:
slickbet backtest # Premier League, 4 weeks
slickbet backtest --weeks 8 # 8 weeks of data
slickbet backtest --competition 3 # La Liga
slickbet backtest --min-prob 0.60 # Only predictions ≥60%
""",
)
backtest_parser.add_argument(
"-c",
"--competition",
type=str,
default="2",
metavar="ID",
help="Competition ID to backtest (default: 2 = Premier League)",
)
backtest_parser.add_argument(
"-w",
"--weeks",
type=int,
default=4,
metavar="N",
help="Number of weeks of historical data (default: 4)",
)
backtest_parser.add_argument(
"--from",
type=str,
dest="from_date",
metavar="YYYY-MM-DD",
help="Start date for backtest (overrides --weeks)",
)
backtest_parser.add_argument(
"--to",
type=str,
dest="to_date",
metavar="YYYY-MM-DD",
help="End date for backtest (default: yesterday)",
)
backtest_parser.add_argument(
"--min-prob",
type=float,
default=0.0,
metavar="PROB",
help="Minimum probability threshold (default: 0.0 = all)",
)
backtest_parser.add_argument("--json", action="store_true", help="Output results as JSON")
backtest_parser.add_argument(
"--pdf",
type=str,
metavar="PATH",
nargs="?",
const="",
help="Export results to PDF file (optional: specify filename, otherwise auto-generated)",
)
backtest_parser.add_argument(
"--debug",
action="store_true",
help="Show detailed match-by-match predictions and results",
)
backtest_parser.add_argument(
"--cache-dir",
type=str,
metavar="DIR",
default=None,
help="Cache API responses under DIR for fast reruns and hyperparameter tuning",
)
backtest_parser.add_argument(
"--cache-only",
action="store_true",
help="Use only cached data (no API calls). Use after a run with --cache-dir.",
)
# Backtest-all subcommand (all major leagues)
backtest_all_parser = subparsers.add_parser(
"backtest-all",
help="Backtest model on ALL major leagues with aggregated results",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Runs backtests on all 5 major European leagues and shows aggregated results.
Leagues:
1 = Bundesliga (Germany)
2 = Premier League (England)
3 = La Liga (Spain)
4 = Serie A (Italy)
5 = Ligue 1 (France)
Examples:
slickbet backtest-all # All leagues, 4 weeks each
slickbet backtest-all --weeks 17 # All leagues, ~120 days each
""",
)
backtest_all_parser.add_argument(
"-w",
"--weeks",
type=int,
default=4,
metavar="N",
help="Number of weeks of historical data (default: 4)",
)
backtest_all_parser.add_argument(
"--min-prob",
type=float,
default=0.0,
metavar="PROB",
help="Minimum probability threshold (default: 0.0 = all)",
)
backtest_all_parser.add_argument(
"--include-asia",
action="store_true",
help="Include Asia leagues (Saudi, Australia) in backtest",
)
backtest_all_parser.add_argument(
"--asia-only",
action="store_true",
help="Only test Asia leagues (Saudi, Australia)",
)
backtest_all_parser.add_argument(
"--include-african",
action="store_true",
help="Include African leagues in backtest",
)
backtest_all_parser.add_argument(
"--african-only",
action="store_true",
help="Only test African leagues",
)
backtest_all_parser.add_argument(
"--include-americas",
action="store_true",
help="Include Americas leagues (Argentina, Brazil, Mexico) in backtest",
)
backtest_all_parser.add_argument(
"--americas-only",
action="store_true",
help="Only test Americas leagues (Argentina, Brazil, Mexico)",
)
backtest_all_parser.add_argument(
"--pdf",
type=str,
metavar="PATH",
nargs="?",
const="",
help="Export results to PDF file (optional: specify filename, otherwise auto-generated)",
)
backtest_all_parser.add_argument(
"--debug",
action="store_true",
help="Show detailed match-by-match predictions and results",
)
backtest_all_parser.add_argument(
"--cache-dir",
type=str,
metavar="DIR",
default=None,
help="Cache API responses under DIR for fast reruns",
)
backtest_all_parser.add_argument(
"--cache-only",
action="store_true",
help="Use only cached data (no API calls)",
)
# Tune subcommand (hyperparameter search using cached data)
tune_parser = subparsers.add_parser(
"tune",
help="Hyperparameter tuning using cached backtest data",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
slickbet tune --cache-dir data/12_weeks_cache
slickbet tune --cache-dir data/12_weeks_cache --trials 30 --metric best_dc
slickbet tune --cache-dir data/12_weeks_cache --strategy near_default
First populate the cache:
slickbet backtest-all --weeks 12 --cache-dir data/12_weeks_cache
""",
)
tune_parser.add_argument(
"--cache-dir",
type=str,
required=True,
metavar="DIR",
help="Cache directory (from prior backtest with --cache-dir)",
)
tune_parser.add_argument(
"--weeks",
type=int,
default=12,
metavar="N",
help="Weeks of data (must match cache, default: 12)",
)
tune_parser.add_argument(
"--trials",
type=int,
default=20,
metavar="N",
help="Number of weight configurations to try (default: 20)",
)
tune_parser.add_argument(
"--strategy",
type=str,
choices=["random", "near_default"],
default="random",
help="Weight search strategy (default: random)",
)
tune_parser.add_argument(
"--metric",
type=str,
choices=["accuracy", "accuracy_excl_draws", "best_dc"],
default="accuracy_excl_draws",
help="Metric to optimize (default: accuracy_excl_draws)",
)
tune_parser.add_argument(
"--min-prob",
type=float,
default=0.0,
metavar="PROB",
help="Minimum probability threshold (default: 0.0)",
)
tune_parser.add_argument(
"-q",
"--quiet",
action="store_true",
help="Less output",
)
# Main screener options (default command)
parser.add_argument(
"-k",
"--top",
type=int,
default=5,
metavar="K",
help="Number of top betting opportunities to show (default: 5)",
)
parser.add_argument(
"-d",
"--date",
type=str,
metavar="YYYY-MM-DD",
help="Date to screen (default: tomorrow)",
)
parser.add_argument(
"--days",
type=int,
default=1,
metavar="N",
help="Number of days to screen ahead (0 = today, default: 1 = tomorrow only)",
)
# Filtering options
filter_group = parser.add_argument_group("Filtering Options")
filter_group.add_argument(
"--min-prob",
type=float,
default=0.55,
metavar="PROB",
help="Minimum probability threshold (default: 0.55)",
)
filter_group.add_argument(
"--min-conf",
type=float,
default=0.10,
metavar="CONF",
help="Minimum confidence threshold (default: 0.10)",
)
filter_group.add_argument(
"--country",
type=str,
action="append",
metavar="NAME",
help="Filter by country (can be used multiple times)",
)
filter_group.add_argument(
"--competition",
type=str,
action="append",
metavar="NAME",
help="Filter by competition name (can be used multiple times)",
)
filter_group.add_argument(
"--major-only",
action="store_true",
help="Only show major European leagues (Bundesliga, PL, La Liga, Serie A, Ligue 1)",
)
filter_group.add_argument(
"--asia-only",
action="store_true",
help="Only show Asia leagues (Saudi, Australia, J. League Japan)",
)
filter_group.add_argument(
"--americas-only",
action="store_true",
help="Only show Americas leagues (Liga Professional/Argentina, Serie A/Brazil, Liga MX/Mexico)",
)
filter_group.add_argument(
"--all-leagues",
action="store_true",
help="Show all supported leagues (Major European + Minor European + Asia + Americas)",
)
filter_group.add_argument(
"--league",
type=str,
action="append",
metavar="ID",
help="Filter by league ID: 1=Bundesliga, 2=PL, 3=LaLiga, 4=SerieA, 5=Ligue1, 313=Saudi, 67=Australia, 28=Japan J.League, 23=Argentina, 24=Brazil, 45=Mexico, 17=Croatia, 60=Poland, 75=Scotland, 9=Greece",
)
# Output options
output_group = parser.add_argument_group("Output Options")
output_group.add_argument(
"--home-only", action="store_true", help="Only show home win predictions"
)
output_group.add_argument(
"--away-only", action="store_true", help="Only show away win predictions"
)
output_group.add_argument(
"--summary-only",
action="store_true",
help="Only show summary, not individual predictions",
)
output_group.add_argument("--json", action="store_true", help="Output results as JSON")
output_group.add_argument(
"--pdf",
type=str,
metavar="PATH",
nargs="?",
const="",
help="Export results to PDF file (optional: specify filename, otherwise auto-generated)",
)
# Performance options
perf_group = parser.add_argument_group("Performance Options")
perf_group.add_argument(
"--no-stats",
action="store_true",
help="Skip fetching detailed stats (faster but less accurate)",
)
perf_group.add_argument(
"--workers",
type=int,
default=5,
metavar="N",
help="Number of parallel workers for API calls (default: 5)",
)
# Debug subcommand
debug_parser = subparsers.add_parser("debug", help="Debug API responses")
debug_parser.add_argument(
"--date",
type=str,
default=None,
help="Date to check fixtures (YYYY-MM-DD, default: tomorrow)",
)
return parser
[docs]
def run_competitions(args: argparse.Namespace) -> int:
"""
List available competitions.
Parameters
----------
args : argparse.Namespace
Parsed CLI arguments (``country``, ``search``).
Returns
-------
int
Exit code (0 for success, 1 for error)
"""
print()
print("🏆 SlickBet - Available Competitions")
print("=" * 60)
print()
try:
client = LivescoreClient()
# Get countries if filtering by country
country_id = None
if args.country:
print(f"🔍 Finding country: {args.country}")
countries = client.get_countries()
for c in countries:
if args.country.lower() in c["name"].lower():
country_id = c["id"]
print(f" Found: {c['name']} (ID: {c['id']})")
break
if not country_id:
print(f" ❌ Country '{args.country}' not found")
print()
print("Available countries:")
for c in sorted(countries, key=lambda x: x["name"])[:30]:
print(f" - {c['name']}")
return 1
print()
# Get competitions
competitions = client.get_competitions(country_id=country_id)
if not competitions:
print("❌ No competitions found")
return 1
# Filter by search term if provided
if args.search:
search_lower = args.search.lower()
competitions = [c for c in competitions if search_lower in c["name"].lower()]
print(f"🔍 Searching for: {args.search}")
print()
if not competitions:
print(f"❌ No competitions matching '{args.search}'")
return 1
# Sort by country and name
competitions = sorted(
competitions, key=lambda x: (x.get("country_name", ""), x.get("name", ""))
)
# Print competitions
print(f"{'ID':<6} {'Competition':<35} {'Country':<20}")
print("-" * 60)
current_country = None
for comp in competitions:
country = comp.get("country_name", "International")
if country != current_country:
if current_country is not None:
print()
current_country = country
print(f"{comp['id']:<6} {comp['name']:<35} {country:<20}")
print()
print(f"Total: {len(competitions)} competitions")
print()
print("Use competition ID with: slickbet backtest --competition <ID>")
return 0
except LivescoreAPIError as e:
print(f"❌ API Error: {e}")
return 1
except Exception as e:
print(f"❌ Unexpected error: {e}")
return 1
[docs]
def run_backtest(args: argparse.Namespace) -> int:
"""
Run the backtest with the given arguments.
Parameters
----------
args : argparse.Namespace
Parsed CLI arguments (competition, weeks, dates, output flags).
Returns
-------
int
Exit code (0 for success, 1 for error)
"""
print()
print("⚽ SlickBet - Model Backtesting")
print("=" * 40)
print()
# Parse dates
to_date = None
from_date = None
if args.to_date:
try:
to_date = datetime.strptime(args.to_date, "%Y-%m-%d")
except ValueError:
print(f"❌ Invalid date format: {args.to_date}")
return 1
if args.from_date:
try:
from_date = datetime.strptime(args.from_date, "%Y-%m-%d")
except ValueError:
print(f"❌ Invalid date format: {args.from_date}")
return 1
try:
backtester = Backtester(
cache_dir=getattr(args, "cache_dir", None),
cache_only=getattr(args, "cache_only", False),
)
results = backtester.run(
competition_id=args.competition,
weeks=args.weeks,
from_date=from_date,
to_date=to_date,
min_probability=args.min_prob,
verbose=True,
debug=getattr(args, "debug", False),
)
if args.json:
output_backtest_json(results)
elif args.pdf is not None:
# Export to PDF
pdf_path = export_backtest_to_pdf(results, output_path=args.pdf if args.pdf else None)
print(f"✅ PDF exported to: {pdf_path}")
else:
print(format_backtest_report(results))
return 0
except LivescoreAPIError as e:
print(f"❌ API Error: {e}")
print(" Check your LIVESCORE_API_KEY and LIVESCORE_API_SECRET environment variables.")
return 1
except Exception as e:
print(f"❌ Unexpected error: {e}")
import traceback
traceback.print_exc()
return 1
[docs]
def run_backtest_all(args: argparse.Namespace) -> int:
"""
Run backtest on all major leagues and show aggregated results.
Parameters
----------
args : argparse.Namespace
Parsed CLI arguments (weeks, league filters, cache, pdf).
Returns
-------
int
Exit code (0 for success, 1 for error)
"""
# Major European leagues
MAJOR_LEAGUES = [
("1", "🇩🇪 Bundesliga", "Germany"),
("2", "🇬🇧 Premier League", "England"),
("3", "🇪🇸 La Liga", "Spain"),
("4", "🇮🇹 Serie A", "Italy"),
("5", "🇫🇷 Ligue 1", "France"),
]
# Minor European leagues
MINOR_LEAGUES = [
("68", "🇧🇪 Belgian Pro League", "Belgium"),
("8", "🇵🇹 Primeira Liga", "Portugal"),
("6", "🇹🇷 Super Lig", "Turkey"),
("196", "🇳🇱 Eredivisie", "Netherlands"),
("17", "🇭🇷 1. HNL", "Croatia"),
("60", "🇵🇱 Ekstraklasa", "Poland"),
("75", "🏴 Premiership", "Scotland"),
("9", "🇬🇷 Super League", "Greece"),
]
# Asia leagues
ASIA_LEAGUES = [
("313", "🇸🇦 Saudi Pro League", "Saudi Arabia"),
("67", "🇦🇺 Hyundai A-League", "Australia"),
("28", "🇯🇵 J. League", "Japan"),
]
# Americas leagues
AMERICAS_LEAGUES = [
("23", "🇦🇷 Liga Professional", "Argentina"),
("24", "🇧🇷 Serie A", "Brazil"),
("45", "🇲🇽 Liga MX", "Mexico"),
]
# Determine which leagues to test
include_asia = getattr(args, "include_asia", False)
asia_only = getattr(args, "asia_only", False)
include_americas = getattr(args, "include_americas", False)
americas_only = getattr(args, "americas_only", False)
if asia_only:
LEAGUES = ASIA_LEAGUES
league_type = "Asia"
elif americas_only:
LEAGUES = AMERICAS_LEAGUES
league_type = "Americas"
else:
# Build league list based on what's included
LEAGUES = MAJOR_LEAGUES + MINOR_LEAGUES
league_type_parts = ["Major + Minor European"]
if include_asia:
LEAGUES += ASIA_LEAGUES
league_type_parts.append("Asia")
if include_americas:
LEAGUES += AMERICAS_LEAGUES
league_type_parts.append("Americas")
league_type = "ALL (" + " + ".join(league_type_parts) + ")"
print()
print("🏆 SlickBet - ALL LEAGUES Backtesting")
print("=" * 70)
print(f" Testing {len(LEAGUES)} {league_type} leagues")
print(f" Period: {args.weeks} weeks")
print("=" * 70)
print()
# Collect results from all leagues
all_results = []
league_summaries = []
backtester = Backtester(
cache_dir=getattr(args, "cache_dir", None),
cache_only=getattr(args, "cache_only", False),
)
for comp_id, league_name, country in LEAGUES:
print(f"\n{league_name}")
print("-" * 40)
try:
results = backtester.run(
competition_id=comp_id,
weeks=args.weeks,
min_probability=args.min_prob,
verbose=False,
debug=getattr(args, "debug", False),
)
# Store results
all_results.extend(results.results)
# Print league summary
print(f" Matches: {results.total_predictions}")
print(f" Accuracy: {results.accuracy:.1%}")
print(f" Accuracy (excl. draws): {results.accuracy_excluding_draws:.1%}")
print(
f" Draws: {results.draws_encountered} ({results.draws_encountered / max(results.total_predictions, 1) * 100:.1f}%)"
)
print(f" 1X (Home or Draw): {results.home_or_draw_accuracy:.1%}")
print(f" X2 (Away or Draw): {results.away_or_draw_accuracy:.1%}")
league_summaries.append(
{
"name": league_name,
"country": country,
"total": results.total_predictions,
"correct": results.correct_predictions,
"accuracy": results.accuracy,
"accuracy_excl_draws": results.accuracy_excluding_draws,
"draws": results.draws_encountered,
"home_or_draw": results.home_or_draw_accuracy,
"away_or_draw": results.away_or_draw_accuracy,
"best_dc": results.best_double_chance_accuracy,
}
)
except Exception as e:
print(f" ❌ Error: {e}")
continue
# Calculate and display aggregated results
print()
print("=" * 70)
print("📊 AGGREGATED RESULTS (ALL LEAGUES)")
print("=" * 70)
total_matches = len(all_results)
if total_matches == 0:
print("❌ No results collected")
return 1
correct = sum(1 for r in all_results if r.is_correct)
draws = sum(1 for r in all_results if r.actual_outcome == "D")
non_draws = [r for r in all_results if r.actual_outcome != "D"]
home_or_draw_correct = sum(1 for r in all_results if r.home_or_draw_correct)
away_or_draw_correct = sum(1 for r in all_results if r.away_or_draw_correct)
best_dc_correct = sum(1 for r in all_results if r.best_double_chance_correct)
print()
print(f"Total Matches: {total_matches}")
print(f"Correct Predictions: {correct}")
print(f"Overall Accuracy: {correct / total_matches:.1%}")
print()
print(f"Draws Encountered: {draws} ({draws / total_matches * 100:.1f}%)")
print(
f"Accuracy (excl. draws): {sum(1 for r in non_draws if r.is_correct) / max(len(non_draws), 1):.1%}"
)
print()
print("=" * 70)
print("🎲 DOUBLE CHANCE (AGGREGATED)")
print("=" * 70)
print(f"1X (Home or Draw): {home_or_draw_correct / total_matches:.1%}")
print(f"X2 (Away or Draw): {away_or_draw_correct / total_matches:.1%}")
print(f"Best Recommended DC: {best_dc_correct / total_matches:.1%}")
print()
# League comparison table
print("=" * 70)
print("📈 LEAGUE COMPARISON")
print("=" * 70)
print(f"{'League':<25} {'Matches':>8} {'Accuracy':>10} {'Excl.Draws':>12} {'Best DC':>10}")
print("-" * 70)
for league in sorted(
league_summaries,
key=lambda x: float(x["accuracy_excl_draws"]), # type: ignore[arg-type]
reverse=True,
):
print(
f"{league['name']:<25} "
f"{league['total']:>8} "
f"{league['accuracy']:>9.1%} "
f"{league['accuracy_excl_draws']:>11.1%} "
f"{league['best_dc']:>9.1%}"
)
print("=" * 70)
# Export to PDF if requested
if args.pdf is not None:
pdf_path = export_backtest_all_to_pdf(
league_summaries=league_summaries,
all_results=all_results,
league_type=league_type,
weeks=args.weeks,
output_path=args.pdf if args.pdf else None,
)
print()
print(f"✅ PDF exported to: {pdf_path}")
return 0
[docs]
def run_tune(args: argparse.Namespace) -> int:
"""
Run hyperparameter tuning using cached data.
Parameters
----------
args : argparse.Namespace
Parsed CLI arguments (cache_dir, weeks, trials, strategy, metric).
Returns
-------
int
Exit code (0 for success, 1 for error).
"""
print()
print("🎯 SlickBet - Hyperparameter Tuning")
print("=" * 50)
print(f" Cache: {args.cache_dir}")
print(f" Weeks: {args.weeks}")
print(f" Trials: {args.trials}")
print(f" Strategy: {args.strategy}")
print(f" Metric: {args.metric}")
print("=" * 50)
print()
try:
best = tune(
cache_dir=args.cache_dir,
weeks=args.weeks,
trials=args.trials,
strategy=args.strategy,
min_probability=args.min_prob,
metric=args.metric,
verbose=not args.quiet,
)
except Exception as e:
print(f"❌ Error: {e}")
return 1
if best is None:
print("❌ No valid trials. Ensure cache is populated:")
print(" slickbet backtest-all --weeks 12 --cache-dir", args.cache_dir)
return 1
print()
print("=" * 50)
print("📊 BEST CONFIGURATION")
print("=" * 50)
print(f" Accuracy: {best.accuracy:.1%}")
print(f" Accuracy (excl. draws): {best.accuracy_excl_draws:.1%}")
print(f" Best DC: {best.best_dc_accuracy:.1%}")
print(f" Matches: {best.correct}/{best.total_matches}")
print()
print(" Weights (for BettingModel):")
print(" " + "-" * 40)
for k, v in sorted(best.weights.items()):
print(f" {k}: {v:.4f}")
print()
print(" Copy into model.py WEIGHTS or BettingModel(__init__):")
print()
for line in format_best_weights(best.weights).split("\n"):
print(" " + line)
print()
return 0
[docs]
def output_backtest_json(results: BacktestResults) -> None:
"""
Output backtest results as JSON to stdout.
Parameters
----------
results : BacktestResults
Aggregated backtest results to serialize.
"""
import json
output = {
"summary": {
"competition": results.competition,
"from_date": results.from_date.isoformat() if results.from_date else None,
"to_date": results.to_date.isoformat() if results.to_date else None,
"total_predictions": results.total_predictions,
"correct_predictions": results.correct_predictions,
"accuracy": round(results.accuracy, 4),
"accuracy_excluding_draws": round(results.accuracy_excluding_draws, 4),
"draws_encountered": results.draws_encountered,
"home_predictions": len(results.home_predictions),
"home_accuracy": round(results.home_accuracy, 4),
"away_predictions": len(results.away_predictions),
"away_accuracy": round(results.away_accuracy, 4),
"high_confidence_predictions": len(results.high_confidence_results),
"high_confidence_accuracy": round(results.high_confidence_accuracy, 4),
},
"predictions": [
{
"match": {
"home_team": r.match.home_team.name,
"away_team": r.match.away_team.name,
"date": r.match.date.isoformat(),
"score": r.match.scores.final,
},
"predicted_outcome": r.predicted_outcome,
"actual_outcome": r.actual_outcome,
"probability": round(r.probability, 4),
"confidence": round(r.confidence, 4),
"is_correct": r.is_correct,
}
for r in results.results
],
}
print(json.dumps(output, indent=2))
[docs]
def run_screener(args: argparse.Namespace) -> int:
"""
Run the betting screener with the given arguments.
Parameters
----------
args : argparse.Namespace
Parsed CLI arguments (date/days, filters, output flags).
Returns
-------
int
Exit code (0 for success, 1 for error)
"""
# Create configuration
config = ScreenerConfig(
min_probability=args.min_prob,
min_confidence=args.min_conf,
fetch_detailed_stats=not args.no_stats,
max_workers=args.workers,
countries=args.country or [],
competitions=args.competition or [],
competition_ids=args.league or [],
major_leagues_only=args.major_only,
asia_leagues_only=getattr(args, "asia_only", False),
americas_leagues_only=getattr(args, "americas_only", False),
all_leagues=getattr(args, "all_leagues", False),
)
try:
# Initialize screener
screener = BettingScreener(config=config)
# Determine which date(s) to screen
if args.date:
# Specific date provided
try:
target_date = datetime.strptime(args.date, "%Y-%m-%d")
except ValueError:
print(f"❌ Invalid date format: {args.date}")
print(" Use YYYY-MM-DD format (e.g., 2025-02-15)")
return 1
result = screener.screen_date(target_date)
elif args.days == 0:
# Screen matches for today (calendar day)
result = screener.screen_date(datetime.combine(date.today(), time.min))
else:
# days >= 1: calendar-day logic; same day shows same games as first day of --days=2
result = screener.screen_days(days=args.days)
# Filter by outcome type if requested
predictions = result.predictions
if args.home_only:
predictions = result.get_home_wins()
elif args.away_only:
predictions = result.get_away_wins()
# Helper to get best double chance probability
def get_best_dc_prob(p: BetPrediction) -> float:
if p.double_chance:
_, prob = p.double_chance.best_double_chance
return prob
return p.probability
# Sort by best double chance probability (highest first)
predictions = sorted(predictions, key=get_best_dc_prob, reverse=True)[: args.top]
# Output results
if args.json:
output_json(predictions, result)
elif args.pdf is not None:
# Export to PDF
pdf_path = export_screener_to_pdf(
result, predictions, output_path=args.pdf if args.pdf else None
)
print(f"✅ PDF exported to: {pdf_path}")
elif args.summary_only:
print(format_summary(result))
else:
print(format_summary(result))
print()
if not predictions:
print("😕 No betting opportunities found matching your criteria.")
print(" Try adjusting --min-prob or --min-conf thresholds.")
else:
print(f"⚽ TOP {min(args.top, len(predictions))} BETTING OPPORTUNITIES")
print()
for i, pred in enumerate(predictions, 1):
print(format_prediction(pred, rank=i))
print()
return 0
except LivescoreAPIError as e:
print(f"❌ API Error: {e}")
print(" Check your LIVESCORE_API_KEY and LIVESCORE_API_SECRET environment variables.")
return 1
except Exception as e:
print(f"❌ Unexpected error: {e}")
return 1
[docs]
def output_json(predictions: list[BetPrediction], result: ScreenerResult) -> None:
"""
Output screener results as JSON to stdout.
Parameters
----------
predictions : list
Predictions to include (already filtered/sorted).
result : ScreenerResult
Full screener result for summary fields.
"""
import json
output = {
"summary": {
"total_scanned": result.total_matches_scanned,
"filtered": result.matches_filtered,
"predictions": len(result.predictions),
"timestamp": result.timestamp.isoformat(),
},
"predictions": [
{
"match": {
"home_team": pred.match.home_team.name,
"away_team": pred.match.away_team.name,
"competition": pred.match.competition,
"country": pred.match.country,
"kickoff_time": pred.match.kickoff_time.isoformat(),
},
"recommendation": {
"outcome": pred.recommended_outcome.value,
"bet_on": (
pred.match.home_team.name
if pred.recommended_outcome.value == "home_win"
else pred.match.away_team.name
),
"probability": round(pred.probability, 4),
"confidence": round(pred.confidence, 4),
},
"analysis": {
"form_score": round(pred.form_score, 4),
"position_score": round(pred.position_score, 4),
"home_advantage_score": round(pred.home_advantage_score, 4),
"h2h_score": round(pred.h2h_score, 4),
"xg_score": round(pred.xg_score, 4),
},
"reasoning": pred.reasoning,
}
for pred in predictions
],
}
print(json.dumps(output, indent=2))
[docs]
def run_debug(args: argparse.Namespace) -> int:
"""
Debug API responses to understand data structure.
Parameters
----------
args : argparse.Namespace
Parsed CLI arguments (optional ``date``).
Returns
-------
int
Exit code (0 for success, 1 for error)
"""
import json
print()
print("🔧 SlickBet - API Debug")
print("=" * 60)
print()
try:
client = LivescoreClient()
# Determine date
if args.date:
from datetime import datetime
date = datetime.strptime(args.date, "%Y-%m-%d")
else:
from datetime import datetime, timedelta
date = datetime.now() + timedelta(days=1)
print(f"📅 Fetching fixtures for: {date.strftime('%Y-%m-%d')}")
print()
# Make raw API request
date_str = date.strftime("%Y-%m-%d")
data = client._make_request("fixtures/matches.json", params={"date": date_str})
print("📦 RAW API RESPONSE STRUCTURE:")
print("-" * 60)
# Show top-level keys
if isinstance(data, dict):
print(f"Top-level keys: {list(data.keys())}")
if "data" in data:
data_section = data["data"]
if isinstance(data_section, dict):
print(f"data keys: {list(data_section.keys())}")
# Try to find fixtures
fixtures = None
if "fixtures" in data_section:
fixtures = data_section["fixtures"]
print(
f"Found 'fixtures': {len(fixtures) if isinstance(fixtures, list) else type(fixtures)}"
)
elif "match" in data_section:
fixtures = data_section["match"]
print(
f"Found 'match': {len(fixtures) if isinstance(fixtures, list) else type(fixtures)}"
)
# Show sample fixture structure
if fixtures and isinstance(fixtures, list) and len(fixtures) > 0:
print()
print("📋 SAMPLE FIXTURE (first item):")
print("-" * 60)
sample = fixtures[0]
print(json.dumps(sample, indent=2, default=str))
print()
print("📋 FIXTURE KEYS:")
if isinstance(sample, dict):
for key in sample.keys():
value = sample[key]
value_type = type(value).__name__
if isinstance(value, dict):
print(f" {key}: dict with keys {list(value.keys())}")
elif isinstance(value, str) and len(value) > 50:
print(f" {key}: {value_type} = '{value[:50]}...'")
else:
print(f" {key}: {value_type} = {value}")
elif isinstance(data_section, list) and len(data_section) > 0:
print(f"data is a list with {len(data_section)} items")
print()
print("📋 SAMPLE FIXTURE (first item):")
print("-" * 60)
sample = data_section[0]
print(json.dumps(sample, indent=2, default=str))
return 0
except LivescoreAPIError as e:
print(f"❌ API Error: {e}")
return 1
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
return 1
[docs]
def main() -> int:
"""
Main entry point for the CLI.
Returns
-------
int
Process exit code from the selected subcommand or screener.
"""
parser = create_parser()
args = parser.parse_args()
# Handle subcommands
if args.command == "competitions":
return run_competitions(args)
if args.command == "backtest":
return run_backtest(args)
if args.command == "backtest-all":
return run_backtest_all(args)
if args.command == "tune":
return run_tune(args)
if args.command == "debug":
return run_debug(args)
# Default: run screener
print()
print("⚽ SlickBet - Soccer Betting Screener")
print("=" * 40)
print()
return run_screener(args)
if __name__ == "__main__":
sys.exit(main())