Skill: nandichi/football-value-bets (version 1.0.0)

--- name: football-value-bets description: >- Professional football bet analysis skill. Generates data-driven bet slips based on form, H2H, standings, injuries and value analysis. Includes result tracking with hitrate and ROI. Use when the user asks for a football bet, bet slip, accumulator, tips, or picks. metadata: openclaw: requires: bins: ["python3"] env: ["FOOTBALL_DATA_API_KEY"] primaryEnv: "FOOTBALL_DATA_API_KEY" --- # Football Value Bets Generate professional, data-driven football bet slips. No gut feelings, purely statistics and value analysis. ## When to activate Activate this skill when the user: - Asks for a football bet, bet slip, accumulator, or picks - Asks for match analysis for betting purposes - Asks for bet statistics, results, or ROI - Asks for a pick or tip for football ## Prerequisites Install dependencies (one-time): ```bash pip install requests ``` Set API key (free via football-data.org/client/register): ```bash export FOOTBALL_DATA_API_KEY="your-key-here" ``` Or fill in the key at `{baseDir}/config/settings.json` under `api_key`. --- ## Mode 1: Generate Bet Slip ### Step 1 -- Fetch data Fetch current match data: ```bash python3 {baseDir}/scripts/match_fetcher.py --mode full ``` This returns a JSON package with: - All scheduled matches for today and tomorrow - League standings and form data per team - Head-to-head history (last 5 encounters) Coverage: Premier League, Bundesliga, Serie A, La Liga, Ligue 1, Eredivisie, Champions League. If the user wants a specific date: ```bash python3 {baseDir}/scripts/match_fetcher.py --mode full --date-from 2026-02-15 --date-to 2026-02-15 ``` ### Step 2 -- Analyse each match Analyse EVERY match from the data on these factors (in order of importance): **A. Form (last 5-10 matches)** - Use the `form` field from the standings data (e.g. "W,W,D,L,W") - Calculate win percentage, goals per match - Pay attention to home form vs away form trends **B. Home/away statistics** - Home team: goals for/against at home - Away team: goals for/against away - Compare goal difference at home vs on the road **C. Head-to-head (H2H)** - Review the `h2h` data: who wins historically? - Look for patterns: does one team always score? Are these low- or high-scoring games? **D. Table position and motivation** - Top of the table (title/CL spots): high motivation, consistent - Mid-table: unpredictable, be cautious - Bottom (relegation): can be surprisingly strong or weak - Difference in table position as an indicator **E. Additional factors (via web search)** After the data analysis, search the web for EVERY shortlisted match: - Injuries and suspensions of key players (FotMob, Transfermarkt) - Recent news (manager change, internal issues, transfer saga) - xG (expected goals) data from recent matches - Weather conditions for outdoor matches in winter months Use search terms like: "[Team A] vs [Team B] team news injuries" and "[Team A] xG stats 2025-26" **F. Odds estimation** - Estimate realistic odds per outcome (based on your analysis) - Compare with common bookmaker odds (search the web if needed) - VALUE = when your estimated probability is higher than the implied probability of the odds - Implied probability = 1 / odds (e.g. odds 2.00 = 50% implied) ### Step 3 -- Pick selection Select ONLY picks that meet all criteria: 1. Estimated win probability > 45% 2. Odds per pick: 1.70 - 2.50 (value range) 3. Clear motivation based on data 4. No more than 1 pick per match Allowed pick types: - **1 / X / 2**: Home win, draw, away win - **1X / X2**: Double chance - **BTTS Yes/No**: Both Teams To Score - **Over/Under 2.5 goals** - **Handicap -1**: Team wins by 2+ goals ### Step 4 -- Honesty check If you find FEWER than 3 picks with real value: **Do NOT generate a bet slip.** Say honestly: "No strong bet slip possible today. The matches offer insufficient value based on the data. Reasons: [brief explanation]. Wait for better odds or the next matchday." This is MANDATORY. Never force it. Honesty > revenue. ### Step 5 -- Generate output Generate TWO variants: **Variant A: Main slip (3-4 picks, total odds 8x-15x)** | Match | Pick | Odds | Motivation | Prob % | Risk | |-------|------|------|------------|--------|------| | Ajax - PSV | BTTS Yes | 1.85 | Both teams scored in 8/10 recent matches, H2H always goals | 52% | Low | | Arsenal - Chelsea | Over 2.5 | 1.90 | Avg 3.2 goals per match this season, both attack-minded | 48% | Medium | | ... | ... | ... | ... | ... | ... | Total odds: XX.XX Recommended stake: 5-10 EUR Potential payout: XX.XX EUR **Variant B: Backup (2-3 picks, total odds 4x-8x)** Same table format but with safer picks (higher probabilities, lower odds). **Always include at the bottom:** - Estimated hitrate per variant (e.g. "Main slip: ~15% chance of all picks hitting, individual picks ~50% average") - Alternative advice: "Consider singles if you want to play it safer" - Disclaimer: "Gamble responsibly. Max stake 5-10 EUR per slip, monthly limit 75 EUR." ### Step 6 -- Save bet Automatically save the bet slip for tracking: ```bash python3 {baseDir}/scripts/bet_tracker.py --mode save --data '{ "type": "main", "stake": 5.0, "picks": [ { "match": "Ajax - PSV", "competition": "Eredivisie", "pick": "BTTS Yes", "odds": 1.85, "estimated_probability": 0.52, "motivation": "Both teams scored in 8/10 recent matches", "risk": "low" } ], "total_odds": 12.50, "notes": "" }' ``` Repeat for the backup slip with `"type": "backup"`. --- ## Mode 2: Record results When the user says a bet has won or lost: ```bash # Mark entire slip python3 {baseDir}/scripts/bet_tracker.py --mode result --date 2026-02-15 --slip-idx 1 --result win # Mark specific pick python3 {baseDir}/scripts/bet_tracker.py --mode result --date 2026-02-15 --slip-idx 1 --pick-idx 2 --result loss ``` Result options: `win`, `loss`, `void` Always ask the user for the date and which slip (main or backup) it concerns. --- ## Mode 3: View statistics When the user asks about results, stats, ROI, or hitrate: ```bash python3 {baseDir}/scripts/bet_tracker.py --mode stats ``` Present the JSON output as a readable summary: - All-time hitrate and ROI - Current month: staked, returned, profit/loss, budget remaining - Best and worst month Warn if the monthly budget is almost spent (>80% used). --- ## Mode 4: View history ```bash python3 {baseDir}/scripts/bet_tracker.py --mode history --days 30 ``` Show an overview of all bets from the last N days. --- ## Analysis quality rules These rules are HARD. Always follow them: 1. **Never more than 5 picks per slip.** More picks = exponentially lower chance. 2. **Never odds below 1.50 or above 3.00 per pick.** Too low = no value, too high = too risky. 3. **Never 2 picks from the same match.** Correlation destroys value. 4. **Never a pick without data backing.** "Gut feeling" is not an argument. 5. **Always run the honesty check.** No slip is better than a bad slip. 6. **Avoid derbies and cup matches** unless the data is overwhelming. Too unpredictable. 7. **Always check injuries.** A team without key players is a different team. 8. **Factor in the season phase.** Early season = unreliable data. End of season = varying motivation. ## Budget and responsible gambling Configuration is in `{baseDir}/config/settings.json`: - Per bet: 5-10 EUR - Monthly limit: 75 EUR - Always check the stats for monthly spending before generating a new slip If the monthly budget is at or nearly at its limit (>90%): - Inform the user - Advise waiting until next month - Do NOT generate a new slip unless the user explicitly states they want to proceed ## Tone and style - Output in English - Professional but accessible - No hype, no promises - Data and percentages front and center - Honest about risks

path: config/settings.json
content: {
  "api_key": "",
  "competitions": ["PL", "BL1", "SA", "PD", "FL1", "DED", "CL"],
  "competition_names": {
    "PL": "Premier League",
    "BL1": "Bundesliga",
    "SA": "Serie A",
    "PD": "La Liga",
    "FL1": "Ligue 1",
    "DED": "Eredivisie",
    "CL": "Champions League"
  },
  "budget": {
    "per_bet_min": 5,
    "per_bet_max": 10,
    "monthly_limit": 75
  },
  "analysis": {
    "min_value_probability": 0.45,
    "max_picks_main": 4,
    "max_picks_backup": 3,
    "target_odds_main": [8, 15],
    "target_odds_backup": [4, 8],
    "min_single_odds": 1.70,
    "max_single_odds": 2.50
  }
}


path: scripts/match_fetcher.py
content: #!/usr/bin/env python3
"""
Fetch football match data from football-data.org API.

Modes:
  fixtures   -- Today's and tomorrow's matches for configured competitions
  standings  -- League tables with form data
  h2h        -- Head-to-head record for a specific match
  full       -- Combined analysis package (fixtures + standings + h2h for each match)

Output: structured JSON to stdout.
"""
from __future__ import annotations

import argparse
import json
import os
import sys
import time
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, List, Optional

try:
    import requests
except ImportError:
    print(json.dumps({
        "status": "error",
        "code": "missing_dependency",
        "message": "requests library not installed. Run: pip install requests"
    }))
    sys.exit(1)

BASE_URL = "https://api.football-data.org/v4"
CONFIG_PATH = Path(__file__).resolve().parents[1] / "config" / "settings.json"

COMPETITION_CODES = ["PL", "BL1", "SA", "PD", "FL1", "DED", "CL"]

RATE_LIMIT_DELAY = 6.5


def _load_config() -> Dict[str, Any]:
    if not CONFIG_PATH.exists():
        return {}
    with open(CONFIG_PATH, "r", encoding="utf-8") as fh:
        return json.load(fh)


def _get_api_key() -> str:
    key = os.environ.get("FOOTBALL_DATA_API_KEY", "")
    if not key:
        cfg = _load_config()
        key = cfg.get("api_key", "")
    return key


def _get_competitions() -> List[str]:
    cfg = _load_config()
    return cfg.get("competitions", COMPETITION_CODES)


def _get_competition_names() -> Dict[str, str]:
    cfg = _load_config()
    return cfg.get("competition_names", {})


def _api_request(endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
    [REDACTED_SECRET])
    if not [REDACTED_SECRET] {
            "status": "error",
            "code": "no_api_key",
            "message": "No FOOTBALL_DATA_API_KEY set. Get a free key at football-data.org/client/register"
        }

    headers = {"X-Auth-Token": api_key}
    url = f"{BASE_URL}{endpoint}"

    for attempt in range(3):
        try:
            resp = requests.get(url, headers=headers, params=params, timeout=15)

            if resp.status_code == 429:
                retry_after = int(resp.headers.get("Retry-After", "10"))
                time.sleep(retry_after)
                continue

            if resp.status_code == 200:
                return resp.json()

            return {
                "status": "error",
                "code": f"http_{resp.status_code}",
                "message": resp.text[:500]
            }
        except requests.RequestException as exc:
            if attempt < 2:
                time.sleep(2 ** attempt)
                continue
            return {
                "status": "error",
                "code": "request_failed",
                "message": str(exc)
            }

    return {"status": "error", "code": "max_retries", "message": "Max retries exceeded"}


def fetch_fixtures(date_from: Optional[str] = None, date_to: Optional[str] = None) -> Dict[str, Any]:
    """Fetch matches for today and tomorrow across configured competitions."""
    today = datetime.utcnow().date()
    tomorrow = today + timedelta(days=1)

    if not date_from:
        date_from = today.isoformat()
    if not date_to:
        date_to = tomorrow.isoformat()

    competitions = _get_competitions()
    comp_names = _get_competition_names()
    all_matches = []

    for code in competitions:
        data = _api_request(f"/competitions/{code}/matches", {
            "dateFrom": date_from,
            "dateTo": date_to,
        })

        if "status" in data and data.get("status") == "error":
            continue

        matches = data.get("matches", [])
        for match in matches:
            all_matches.append({
                "id": match.get("id"),
                "competition": code,
                "competition_name": comp_names.get(code, code),
                "matchday": match.get("matchday"),
                "date": match.get("utcDate"),
                "status": match.get("status"),
                "home_team": match.get("homeTeam", {}).get("name"),
                "home_team_id": match.get("homeTeam", {}).get("id"),
                "away_team": match.get("awayTeam", {}).get("name"),
                "away_team_id": match.get("awayTeam", {}).get("id"),
                "score": {
                    "home": match.get("score", {}).get("fullTime", {}).get("home"),
                    "away": match.get("score", {}).get("fullTime", {}).get("away"),
                },
            })

        time.sleep(RATE_LIMIT_DELAY)

    scheduled = [m for m in all_matches if m["status"] in ("SCHEDULED", "TIMED")]
    scheduled.sort(key=lambda m: m.get("date", ""))

    return {
        "mode": "fixtures",
        "date_from": date_from,
        "date_to": date_to,
        "total_matches": len(all_matches),
        "scheduled_matches": len(scheduled),
        "matches": scheduled,
        "completed_matches": [m for m in all_matches if m["status"] == "FINISHED"],
    }


def fetch_standings(competition: Optional[str] = None) -> Dict[str, Any]:
    """Fetch league standings with form data for one or all competitions."""
    competitions = [competition] if competition else _get_competitions()
    comp_names = _get_competition_names()
    all_standings = {}

    for code in competitions:
        if code == "CL":
            continue

        data = _api_request(f"/competitions/{code}/standings")

        if "status" in data and data.get("status") == "error":
            all_standings[code] = {"error": data.get("message", "Unknown error")}
            time.sleep(RATE_LIMIT_DELAY)
            continue

        standings_list = data.get("standings", [])
        total_standing = None
        for s in standings_list:
            if s.get("type") == "TOTAL":
                total_standing = s
                break

        if not total_standing:
            total_standing = standings_list[0] if standings_list else {"table": []}

        table = []
        for entry in total_standing.get("table", []):
            team = entry.get("team", {})
            table.append({
                "position": entry.get("position"),
                "team": team.get("name"),
                "team_id": team.get("id"),
                "played": entry.get("playedGames"),
                "won": entry.get("won"),
                "draw": entry.get("draw"),
                "lost": entry.get("lost"),
                "goals_for": entry.get("goalsFor"),
                "goals_against": entry.get("goalsAgainst"),
                "goal_difference": entry.get("goalDifference"),
                "points": entry.get("points"),
                "form": entry.get("form"),
            })

        all_standings[code] = {
            "competition_name": comp_names.get(code, code),
            "season": data.get("season", {}).get("id"),
            "matchday": data.get("season", {}).get("currentMatchday"),
            "table": table,
        }

        time.sleep(RATE_LIMIT_DELAY)

    return {
        "mode": "standings",
        "standings": all_standings,
    }


def fetch_h2h(match_id: int) -> Dict[str, Any]:
    """Fetch head-to-head record for a specific match."""
    data = _api_request(f"/matches/{match_id}/head2head", {"limit": 5})

    if "status" in data and data.get("status") == "error":
        return data

    agg = data.get("aggregates", {})
    matches = data.get("matches", [])

    h2h_matches = []
    for m in matches:
        h2h_matches.append({
            "date": m.get("utcDate"),
            "competition": m.get("competition", {}).get("name"),
            "home_team": m.get("homeTeam", {}).get("name"),
            "away_team": m.get("awayTeam", {}).get("name"),
            "score_home": m.get("score", {}).get("fullTime", {}).get("home"),
            "score_away": m.get("score", {}).get("fullTime", {}).get("away"),
            "winner": m.get("score", {}).get("winner"),
        })

    return {
        "mode": "h2h",
        "match_id": match_id,
        "total_matches": agg.get("numberOfMatches"),
        "home_team": agg.get("homeTeam", {}).get("name"),
        "away_team": agg.get("awayTeam", {}).get("name"),
        "home_wins": agg.get("homeTeam", {}).get("wins"),
        "away_wins": agg.get("awayTeam", {}).get("wins"),
        "draws": agg.get("homeTeam", {}).get("draws"),
        "recent_matches": h2h_matches,
    }


def _get_team_form(standings: Dict[str, Any], team_name: str) -> Optional[Dict[str, Any]]:
    """Look up a team's standing data across all competition standings."""
    for code, comp_data in standings.get("standings", {}).items():
        if isinstance(comp_data, dict) and "table" in comp_data:
            for entry in comp_data["table"]:
                if entry.get("team") == team_name:
                    return {
                        "competition": code,
                        "position": entry.get("position"),
                        "played": entry.get("played"),
                        "won": entry.get("won"),
                        "draw": entry.get("draw"),
                        "lost": entry.get("lost"),
                        "goals_for": entry.get("goals_for"),
                        "goals_against": entry.get("goals_against"),
                        "goal_difference": entry.get("goal_difference"),
                        "points": entry.get("points"),
                        "form": entry.get("form"),
                    }
    return None


def fetch_full(date_from: Optional[str] = None, date_to: Optional[str] = None) -> Dict[str, Any]:
    """Full analysis package: fixtures + standings + h2h for each scheduled match."""
    fixtures = fetch_fixtures(date_from, date_to)

    if not fixtures.get("scheduled_matches"):
        return {
            "mode": "full",
            "date_from": fixtures.get("date_from"),
            "date_to": fixtures.get("date_to"),
            "total_scheduled": 0,
            "message": "No scheduled matches found for the given date range.",
            "matches": [],
        }

    standings = fetch_standings()

    enriched_matches = []
    for match in fixtures.get("matches", []):
        home_form = _get_team_form(standings, match.get("home_team"))
        away_form = _get_team_form(standings, match.get("away_team"))

        match_data = {
            "id": match.get("id"),
            "competition": match.get("competition"),
            "competition_name": match.get("competition_name"),
            "date": match.get("date"),
            "home_team": match.get("home_team"),
            "away_team": match.get("away_team"),
            "home_form": home_form,
            "away_form": away_form,
            "h2h": None,
        }

        match_id = match.get("id")
        if match_id:
            time.sleep(RATE_LIMIT_DELAY)
            h2h = fetch_h2h(match_id)
            if h2h.get("mode") == "h2h":
                match_data["h2h"] = h2h

        enriched_matches.append(match_data)

    return {
        "mode": "full",
        "date_from": fixtures.get("date_from"),
        "date_to": fixtures.get("date_to"),
        "total_scheduled": len(enriched_matches),
        "matches": enriched_matches,
    }


def main():
    parser = argparse.ArgumentParser(description="Fetch football match data")
    parser.add_argument("--mode", required=True, choices=["fixtures", "standings", "h2h", "full"],
                        help="Data mode to fetch")
    parser.add_argument("--match-id", type=int, help="Match ID for h2h mode")
    parser.add_argument("--competition", type=str, help="Competition code for standings (e.g. PL, BL1)")
    parser.add_argument("--date-from", type=str, help="Start date (YYYY-MM-DD)")
    parser.add_argument("--date-to", type=str, help="End date (YYYY-MM-DD)")

    args = parser.parse_args()

    if args.mode == "fixtures":
        result = fetch_fixtures(args.date_from, args.date_to)
    elif args.mode == "standings":
        result = fetch_standings(args.competition)
    elif args.mode == "h2h":
        if not args.match_id:
            result = {"status": "error", "code": "missing_param", "message": "--match-id is required for h2h mode"}
        else:
            result = fetch_h2h(args.match_id)
    elif args.mode == "full":
        result = fetch_full(args.date_from, args.date_to)
    else:
        result = {"status": "error", "code": "unknown_mode", "message": f"Unknown mode: {args.mode}"}

    print(json.dumps(result, indent=2, ensure_ascii=False))


if __name__ == "__main__":
    main()


path: scripts/bet_tracker.py
content: #!/usr/bin/env python3
"""
Track football bet slips, results, and cumulative statistics.

Modes:
  save     -- Save a new bet slip to data/bets/YYYY-MM-DD.json
  result   -- Mark a pick's result (win, loss, void)
  stats    -- Show cumulative statistics (hitrate, ROI, monthly budget)
  history  -- Show bet history for the last N days
"""
from __future__ import annotations

import argparse
import json
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, List, Optional

DATA_DIR = Path(__file__).resolve().parents[1] / "data"
BETS_DIR = DATA_DIR / "bets"
STATS_FILE = DATA_DIR / "stats.json"
CONFIG_PATH = Path(__file__).resolve().parents[1] / "config" / "settings.json"


def _ensure_dirs():
    BETS_DIR.mkdir(parents=True, exist_ok=True)


def _load_config() -> Dict[str, Any]:
    if not CONFIG_PATH.exists():
        return {}
    with open(CONFIG_PATH, "r", encoding="utf-8") as fh:
        return json.load(fh)


def _load_bet_file(date_str: str) -> Dict[str, Any]:
    path = BETS_DIR / f"{date_str}.json"
    if not path.exists():
        return {"date": date_str, "slips": []}
    with open(path, "r", encoding="utf-8") as fh:
        return json.load(fh)


def _save_bet_file(date_str: str, data: Dict[str, Any]):
    _ensure_dirs()
    path = BETS_DIR / f"{date_str}.json"
    with open(path, "w", encoding="utf-8") as fh:
        json.dump(data, fh, indent=2, ensure_ascii=False)


def _load_stats() -> Dict[str, Any]:
    if not STATS_FILE.exists():
        return {
            "total_slips": 0,
            "total_picks": 0,
            "wins": 0,
            "losses": 0,
            "voids": 0,
            "pending": 0,
            "total_staked": 0.0,
            "total_returned": 0.0,
            "monthly": {},
        }
    with open(STATS_FILE, "r", encoding="utf-8") as fh:
        return json.load(fh)


def _save_stats(stats: Dict[str, Any]):
    _ensure_dirs()
    with open(STATS_FILE, "w", encoding="utf-8") as fh:
        json.dump(stats, fh, indent=2, ensure_ascii=False)


def save_bet(data_json: str) -> Dict[str, Any]:
    """Save a new bet slip.

    Expected JSON structure:
    {
      "type": "main" | "backup",
      "stake": 5.0,
      "picks": [
        {
          "match": "Ajax - PSV",
          "competition": "Eredivisie",
          "pick": "BTTS Yes",
          "odds": 1.85,
          "estimated_probability": 0.52,
          "motivation": "Both teams scored in 8 of last 10 matches",
          "risk": "medium"
        }
      ],
      "total_odds": 12.5,
      "notes": "optional notes"
    }
    """
    try:
        slip = json.loads(data_json)
    except json.JSONDecodeError as exc:
        return {"status": "error", "code": "invalid_json", "message": str(exc)}

    if "picks" not in slip or not slip["picks"]:
        return {"status": "error", "code": "no_picks", "message": "Bet slip must contain picks"}

    today = datetime.utcnow().date().isoformat()
    bet_data = _load_bet_file(today)

    total_odds = slip.get("total_odds", 1.0)
    if total_odds <= 0:
        combined = 1.0
        for pick in slip["picks"]:
            combined *= pick.get("odds", 1.0)
        total_odds = round(combined, 2)

    slip_record = {
        "id": len(bet_data["slips"]) + 1,
        "created_at": datetime.utcnow().isoformat() + "Z",
        "type": slip.get("type", "main"),
        "stake": slip.get("stake", 5.0),
        "total_odds": total_odds,
        "potential_return": round(slip.get("stake", 5.0) * total_odds, 2),
        "result": "pending",
        "picks": [],
        "notes": slip.get("notes", ""),
    }

    for pick in slip["picks"]:
        slip_record["picks"].append({
            "match": pick.get("match", ""),
            "competition": pick.get("competition", ""),
            "pick": pick.get("pick", ""),
            "odds": pick.get("odds", 1.0),
            "estimated_probability": pick.get("estimated_probability", 0.0),
            "motivation": pick.get("motivation", ""),
            "risk": pick.get("risk", "medium"),
            "result": "pending",
        })

    bet_data["slips"].append(slip_record)
    _save_bet_file(today, bet_data)

    stats = _load_stats()
    stats["total_slips"] += 1
    stats["total_picks"] += len(slip_record["picks"])
    stats["pending"] += len(slip_record["picks"])
    stats["total_staked"] += slip_record["stake"]

    month_key = today[:7]
    if month_key not in stats["monthly"]:
        stats["monthly"][month_key] = {
            "slips": 0, "picks": 0, "wins": 0, "losses": 0,
            "staked": 0.0, "returned": 0.0,
        }
    stats["monthly"][month_key]["slips"] += 1
    stats["monthly"][month_key]["picks"] += len(slip_record["picks"])
    stats["monthly"][month_key]["staked"] += slip_record["stake"]
    _save_stats(stats)

    return {
        "status": "ok",
        "message": f"Bet slip #{slip_record['id']} saved for {today}",
        "slip": slip_record,
    }


def mark_result(date_str: str, slip_idx: int, pick_idx: Optional[int],
                result: str) -> Dict[str, Any]:
    """Mark result for a pick or entire slip.

    Args:
        date_str: Date of the bet file (YYYY-MM-DD)
        slip_idx: 1-based slip index
        pick_idx: 1-based pick index within the slip (None = mark entire slip)
        result: "win", "loss", or "void"
    """
    if result not in ("win", "loss", "void"):
        return {"status": "error", "code": "invalid_result",
                "message": f"Result must be win, loss, or void. Got: {result}"}

    bet_data = _load_bet_file(date_str)
    if not bet_data["slips"]:
        return {"status": "error", "code": "no_slips",
                "message": f"No bet slips found for {date_str}"}

    idx = slip_idx - 1
    if idx < 0 or idx >= len(bet_data["slips"]):
        return {"status": "error", "code": "invalid_index",
                "message": f"Slip index {slip_idx} out of range (1-{len(bet_data['slips'])})"}

    slip = bet_data["slips"][idx]
    stats = _load_stats()
    month_key = date_str[:7]
    if month_key not in stats["monthly"]:
        stats["monthly"][month_key] = {
            "slips": 0, "picks": 0, "wins": 0, "losses": 0,
            "staked": 0.0, "returned": 0.0,
        }

    updated_picks = []

    if pick_idx is not None:
        pidx = pick_idx - 1
        if pidx < 0 or pidx >= len(slip["picks"]):
            return {"status": "error", "code": "invalid_pick_index",
                    "message": f"Pick index {pick_idx} out of range (1-{len(slip['picks'])})"}

        old_result = slip["picks"][pidx]["result"]
        slip["picks"][pidx]["result"] = result
        updated_picks.append(pick_idx)

        if old_result == "pending":
            stats["pending"] -= 1
        elif old_result == "win":
            stats["wins"] -= 1
            stats["monthly"][month_key]["wins"] -= 1
        elif old_result == "loss":
            stats["losses"] -= 1
            stats["monthly"][month_key]["losses"] -= 1

        if result == "win":
            stats["wins"] += 1
            stats["monthly"][month_key]["wins"] += 1
        elif result == "loss":
            stats["losses"] += 1
            stats["monthly"][month_key]["losses"] += 1
        elif result == "void":
            stats["voids"] += 1
    else:
        for i, pick in enumerate(slip["picks"]):
            old_result = pick["result"]
            pick["result"] = result
            updated_picks.append(i + 1)

            if old_result == "pending":
                stats["pending"] -= 1
            elif old_result == "win":
                stats["wins"] -= 1
                stats["monthly"][month_key]["wins"] -= 1
            elif old_result == "loss":
                stats["losses"] -= 1
                stats["monthly"][month_key]["losses"] -= 1

            if result == "win":
                stats["wins"] += 1
                stats["monthly"][month_key]["wins"] += 1
            elif result == "loss":
                stats["losses"] += 1
                stats["monthly"][month_key]["losses"] += 1
            elif result == "void":
                stats["voids"] += 1

    all_results = [p["result"] for p in slip["picks"]]
    if all(r == "win" for r in all_results):
        slip["result"] = "win"
        returned = slip["stake"] * slip["total_odds"]
        slip["actual_return"] = round(returned, 2)
        slip["profit"] = round(returned - slip["stake"], 2)
        stats["total_returned"] += returned
        stats["monthly"][month_key]["returned"] += returned
    elif any(r == "loss" for r in all_results):
        slip["result"] = "loss"
        slip["actual_return"] = 0.0
        slip["profit"] = -slip["stake"]
    elif all(r == "void" for r in all_results):
        slip["result"] = "void"
        slip["actual_return"] = slip["stake"]
        slip["profit"] = 0.0
        stats["total_returned"] += slip["stake"]
        stats["monthly"][month_key]["returned"] += slip["stake"]
    elif any(r == "pending" for r in all_results):
        slip["result"] = "pending"
    else:
        slip["result"] = "partial"

    _save_bet_file(date_str, bet_data)
    _save_stats(stats)

    return {
        "status": "ok",
        "message": f"Updated slip #{slip_idx} picks {updated_picks} to '{result}'",
        "slip_result": slip["result"],
        "slip": slip,
    }


def get_stats() -> Dict[str, Any]:
    """Return cumulative statistics."""
    stats = _load_stats()
    config = _load_config()
    budget = config.get("budget", {})
    monthly_limit = budget.get("monthly_limit", 75)

    current_month = datetime.utcnow().date().isoformat()[:7]
    month_data = stats.get("monthly", {}).get(current_month, {
        "slips": 0, "picks": 0, "wins": 0, "losses": 0,
        "staked": 0.0, "returned": 0.0,
    })

    total_decided = stats["wins"] + stats["losses"]
    hitrate = (stats["wins"] / total_decided * 100) if total_decided > 0 else 0.0
    roi = ((stats["total_returned"] - stats["total_staked"]) / stats["total_staked"] * 100) \
        if stats["total_staked"] > 0 else 0.0

    month_decided = month_data.get("wins", 0) + month_data.get("losses", 0)
    month_hitrate = (month_data.get("wins", 0) / month_decided * 100) if month_decided > 0 else 0.0
    month_roi = ((month_data.get("returned", 0) - month_data.get("staked", 0)) /
                 month_data.get("staked", 0) * 100) if month_data.get("staked", 0) > 0 else 0.0

    best_month = None
    worst_month = None
    for mk, md in stats.get("monthly", {}).items():
        profit = md.get("returned", 0) - md.get("staked", 0)
        if best_month is None or profit > best_month["profit"]:
            best_month = {"month": mk, "profit": round(profit, 2)}
        if worst_month is None or profit < worst_month["profit"]:
            worst_month = {"month": mk, "profit": round(profit, 2)}

    return {
        "mode": "stats",
        "all_time": {
            "total_slips": stats["total_slips"],
            "total_picks": stats["total_picks"],
            "wins": stats["wins"],
            "losses": stats["losses"],
            "voids": stats["voids"],
            "pending": stats["pending"],
            "hitrate_pct": round(hitrate, 1),
            "total_staked": round(stats["total_staked"], 2),
            "total_returned": round(stats["total_returned"], 2),
            "profit": round(stats["total_returned"] - stats["total_staked"], 2),
            "roi_pct": round(roi, 1),
        },
        "current_month": {
            "month": current_month,
            "slips": month_data.get("slips", 0),
            "picks": month_data.get("picks", 0),
            "wins": month_data.get("wins", 0),
            "losses": month_data.get("losses", 0),
            "hitrate_pct": round(month_hitrate, 1),
            "staked": round(month_data.get("staked", 0), 2),
            "returned": round(month_data.get("returned", 0), 2),
            "profit": round(month_data.get("returned", 0) - month_data.get("staked", 0), 2),
            "roi_pct": round(month_roi, 1),
            "budget_limit": monthly_limit,
            "budget_remaining": round(monthly_limit - month_data.get("staked", 0), 2),
            "budget_used_pct": round(month_data.get("staked", 0) / monthly_limit * 100, 1)
                if monthly_limit > 0 else 0.0,
        },
        "best_month": best_month,
        "worst_month": worst_month,
    }


def get_history(days: int = 30) -> Dict[str, Any]:
    """Show bet history for the last N days."""
    _ensure_dirs()
    today = datetime.utcnow().date()
    slips_all = []

    for i in range(days):
        date = (today - timedelta(days=i)).isoformat()
        bet_data = _load_bet_file(date)
        for slip in bet_data.get("slips", []):
            slip["date"] = date
            slips_all.append(slip)

    total_staked = sum(s.get("stake", 0) for s in slips_all)
    total_returned = sum(s.get("actual_return", 0) for s in slips_all if s.get("actual_return"))
    wins = sum(1 for s in slips_all if s.get("result") == "win")
    losses = sum(1 for s in slips_all if s.get("result") == "loss")

    return {
        "mode": "history",
        "days": days,
        "total_slips": len(slips_all),
        "wins": wins,
        "losses": losses,
        "pending": sum(1 for s in slips_all if s.get("result") == "pending"),
        "total_staked": round(total_staked, 2),
        "total_returned": round(total_returned, 2),
        "profit": round(total_returned - total_staked, 2),
        "slips": slips_all,
    }


def main():
    parser = argparse.ArgumentParser(description="Track football bets")
    parser.add_argument("--mode", required=True, choices=["save", "result", "stats", "history"],
                        help="Tracking mode")
    parser.add_argument("--data", type=str, help="JSON data for save mode")
    parser.add_argument("--date", type=str, help="Date (YYYY-MM-DD) for result mode")
    parser.add_argument("--slip-idx", type=int, help="1-based slip index for result mode")
    parser.add_argument("--pick-idx", type=int, help="1-based pick index for result mode (omit for entire slip)")
    parser.add_argument("--result", type=str, choices=["win", "loss", "void"],
                        help="Result for result mode")
    parser.add_argument("--days", type=int, default=30, help="Number of days for history mode")

    args = parser.parse_args()

    if args.mode == "save":
        if not args.data:
            output = {"status": "error", "code": "missing_data",
                      "message": "--data is required for save mode"}
        else:
            output = save_bet(args.data)
    elif args.mode == "result":
        if not args.date or not args.slip_idx or not args.result:
            output = {"status": "error", "code": "missing_params",
                      "message": "--date, --slip-idx, and --result are required for result mode"}
        else:
            output = mark_result(args.date, args.slip_idx, args.pick_idx, args.result)
    elif args.mode == "stats":
        output = get_stats()
    elif args.mode == "history":
        output = get_history(args.days)
    else:
        output = {"status": "error", "code": "unknown_mode", "message": f"Unknown mode: {args.mode}"}

    print(json.dumps(output, indent=2, ensure_ascii=False))


if __name__ == "__main__":
    main()


path: skill.json
content: {
  "name": "football-value-bets",
  "displayName": "Football Value Bets",
  "version": "1.0.0",
  "description": "Professional football bet analysis with data-driven picks and result tracking",
  "license": "MIT",
  "dependencies": {
    "requests": ">=2.31.0"
  },
  "config": {
    "FOOTBALL_DATA_API_KEY": {
      "description": "API key from football-data.org (free at football-data.org/client/register)",
      "required": true,
      "env": true
    }
  },
  "openclaw": {
    "compatible": true,
    "minVersion": "1.0.0"
  }
}


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)