Source code for slickbet.cli

"""
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())