Commit 2ec209bc authored by Your Name's avatar Your Name

Add Sure Bet Analysis tool for fixture odds

- Created sure_bet_analyzer.py with SureBetAnalyzer and SureBetRepair classes
- Detects sure bet conditions for under/over and second extraction outcomes
- Added analyze, repair, and save-as-new routes
- Modified fixture upload to automatically redirect to analysis
- Added UI buttons and results display in fixture_detail.html
parent cee2ed46
......@@ -429,6 +429,165 @@ def fixture_detail(fixture_id):
flash('Error loading fixture details', 'error')
return redirect(url_for('main.matches'))
@csrf.exempt
@bp.route('/fixture/<fixture_id>/analyze-sure-bets')
@login_required
@require_active_user
def analyze_fixture_sure_bets(fixture_id):
"""Analyze a fixture for sure bet conditions"""
try:
from app.models import Match, FileUpload
from app.utils.sure_bet_analyzer import analyze_fixture_for_sure_bets
# Verify fixture exists and user has access
if not current_user.is_admin:
matches = Match.query.filter_by(fixture_id=fixture_id, created_by=current_user.id).all()
else:
matches = Match.query.filter_by(fixture_id=fixture_id).all()
if not matches:
flash('Fixture not found', 'error')
return redirect(url_for('main.fixture_detail', fixture_id=fixture_id))
# Perform analysis
result = analyze_fixture_for_sure_bets(fixture_id)
# Get associated uploads
match_ids = [m.id for m in matches]
uploads = FileUpload.query.filter(FileUpload.match_id.in_(match_ids)).all() if match_ids else []
return render_template('main/fixture_detail.html',
fixture_info={
'fixture_id': fixture_id,
'filename': matches[0].filename,
'upload_date': matches[0].created_at,
'total_matches': len(matches),
'active_matches': sum(1 for m in matches if m.active_status),
'created_by': matches[0].created_by
},
matches=matches,
uploads=uploads,
analysis_result=result)
except Exception as e:
logger.error(f"Sure bet analysis error: {str(e)}")
flash(f'Error analyzing fixture: {str(e)}', 'error')
return redirect(url_for('main.fixture_detail', fixture_id=fixture_id))
@csrf.exempt
@bp.route('/fixture/<fixture_id>/repair-sure-bets')
@login_required
@require_active_user
def repair_fixture_sure_bets(fixture_id):
"""Repair sure bet conditions in a fixture by adjusting odds"""
try:
from app.models import Match, FileUpload
from app.utils.sure_bet_analyzer import repair_fixture_sure_bets as do_repair
# Verify fixture exists and user has access
if not current_user.is_admin:
matches = Match.query.filter_by(fixture_id=fixture_id, created_by=current_user.id).all()
else:
matches = Match.query.filter_by(fixture_id=fixture_id).all()
if not matches:
flash('Fixture not found', 'error')
return redirect(url_for('main.fixture_detail', fixture_id=fixture_id))
# Perform repair
result = do_repair(fixture_id)
if result.get('success'):
flash(f"Repaired {result.get('total_repairs', 0)} odds. Remaining issues: {result.get('remaining_issues', 0)}", 'success')
else:
flash(f"Repair failed: {result.get('error', 'Unknown error')}", 'error')
# Re-fetch matches after repair
if not current_user.is_admin:
matches = Match.query.filter_by(fixture_id=fixture_id, created_by=current_user.id).all()
else:
matches = Match.query.filter_by(fixture_id=fixture_id).all()
match_ids = [m.id for m in matches]
uploads = FileUpload.query.filter(FileUpload.match_id.in_(match_ids)).all() if match_ids else []
# Include repair result in analysis result for display
analysis_result = result
return render_template('main/fixture_detail.html',
fixture_info={
'fixture_id': fixture_id,
'filename': matches[0].filename,
'upload_date': matches[0].created_at,
'total_matches': len(matches),
'active_matches': sum(1 for m in matches if m.active_status),
'created_by': matches[0].created_by
},
matches=matches,
uploads=uploads,
analysis_result=analysis_result)
except Exception as e:
logger.error(f"Sure bet repair error: {str(e)}")
flash(f'Error repairing fixture: {str(e)}', 'error')
return redirect(url_for('main.fixture_detail', fixture_id=fixture_id))
@csrf.exempt
@bp.route('/fixture/<fixture_id>/save-as-new')
@login_required
@require_active_user
def save_fixture_as_new(fixture_id):
"""Save a fixture as a new fixture for clients to update"""
try:
from app.models import Match, FileUpload
from app.utils.sure_bet_analyzer import save_fixture_as_new as do_save_new
# Verify fixture exists and user has access
if not current_user.is_admin:
matches = Match.query.filter_by(fixture_id=fixture_id, created_by=current_user.id).all()
else:
matches = Match.query.filter_by(fixture_id=fixture_id).all()
if not matches:
flash('Fixture not found', 'error')
return redirect(url_for('main.fixture_detail', fixture_id=fixture_id))
# Save as new
result = do_save_new(fixture_id, current_user.id)
if result.get('success'):
flash(f"Created new fixture: {result.get('message', '')}", 'success')
# Redirect to the new fixture
return redirect(url_for('main.fixture_detail', fixture_id=result.get('new_fixture_id')))
else:
flash(f"Failed to create new fixture: {result.get('error', 'Unknown error')}", 'error')
# Re-fetch current fixture data
match_ids = [m.id for m in matches]
uploads = FileUpload.query.filter(FileUpload.match_id.in_(match_ids)).all() if match_ids else []
return render_template('main/fixture_detail.html',
fixture_info={
'fixture_id': fixture_id,
'filename': matches[0].filename,
'upload_date': matches[0].created_at,
'total_matches': len(matches),
'active_matches': sum(1 for m in matches if m.active_status),
'created_by': matches[0].created_by
},
matches=matches,
uploads=uploads)
except Exception as e:
logger.error(f"Save as new fixture error: {str(e)}")
flash(f'Error saving fixture: {str(e)}', 'error')
return redirect(url_for('main.fixture_detail', fixture_id=fixture_id))
@csrf.exempt
@bp.route('/fixture/<fixture_id>/delete', methods=['POST'])
@login_required
......
......@@ -82,6 +82,13 @@
.btn-success:hover {
background-color: #218838;
}
.btn-info {
background-color: #17a2b8;
color: white;
}
.btn-info:hover {
background-color: #138496;
}
.btn-sm {
padding: 4px 8px;
font-size: 0.8rem;
......@@ -170,6 +177,49 @@
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-warning {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeeba;
}
.alert-info {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.sure-bet-alert {
background-color: #ffcccc;
color: #cc0000;
border: 2px solid #cc0000;
padding: 15px;
margin-bottom: 20px;
border-radius: 8px;
}
.sure-bet-alert h3 {
margin-top: 0;
color: #cc0000;
}
.analysis-result {
margin-top: 20px;
}
.issue-item {
background-color: #fff3cd;
border-left: 4px solid #ffc107;
padding: 12px;
margin-bottom: 10px;
border-radius: 4px;
}
.issue-item.critical {
background-color: #ffcccc;
border-left-color: #cc0000;
}
.repair-success {
background-color: #d4edda;
border-left: 4px solid #28a745;
padding: 12px;
margin-bottom: 10px;
border-radius: 4px;
}
.alert-success {
background-color: #d4edda;
color: #155724;
......@@ -282,7 +332,29 @@
<h1>Fixture Details</h1>
<a href="{{ url_for('main.fixtures') }}" class="btn">← Back to Fixtures</a>
</div>
<div>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<!-- Sure Bet Analysis Buttons -->
<a href="{{ url_for('main.analyze_fixture_sure_bets', fixture_id=fixture_info.fixture_id) }}"
class="btn btn-warning" title="Analyze fixture for sure bet conditions">
🔍 Analyze Odds
</a>
{% if analysis_result and analysis_result.has_sure_bets %}
<a href="{{ url_for('main.repair_fixture_sure_bets', fixture_id=fixture_info.fixture_id) }}"
class="btn btn-success"
onclick="return confirm('This will adjust odds to fix sure bet conditions. Continue?');"
title="Automatically repair sure bet conditions">
🔧 Repair Odds
</a>
<a href="{{ url_for('main.save_fixture_as_new', fixture_id=fixture_info.fixture_id) }}"
class="btn btn-info"
onclick="return confirm('Create a copy of this fixture as a new fixture? Clients will update to the new fixture.');"
title="Save as new fixture for clients to update">
💾 Save as New
</a>
{% endif %}
<form method="POST" action="{{ url_for('main.delete_fixture', fixture_id=fixture_info.fixture_id) }}"
style="display: inline;"
onsubmit="return confirm('Are you sure you want to delete this entire fixture and all {{ fixture_info.total_matches }} matches? This action cannot be undone.');">
......@@ -291,6 +363,54 @@
</div>
</div>
<!-- Sure Bet Analysis Results -->
{% if analysis_result %}
<div class="section">
{% if analysis_result.has_sure_bets %}
<div class="sure-bet-alert">
<h3>⚠️ Sure Bet Conditions Detected!</h3>
<p>Found <strong>{{ analysis_result.total_issues }}</strong> sure bet issue(s) in <strong>{{ analysis_result.matches_with_issues }}</strong> match(es).</p>
<p><strong>These odds allow players to guarantee a win!</strong></p>
</div>
<h2>Problematic Matches</h2>
{% for issue in analysis_result.issues %}
<div class="issue-item {% if 'win_draw' in issue.issue_type %}critical{% endif %}">
<strong>Match #{{ issue.match_number }}</strong> - {{ issue.extraction|title }} Extraction
<br>
<span style="color: #cc0000;">{{ issue.description }}</span>
<br>
<small>Outcomes involved: {{ issue.outcomes_involved|join(', ') }}</small>
<br>
<small>Odds:
{% for outcome, odds in issue.odds_values.items() %}
<strong>{{ outcome }}:</strong> {{ odds }}{% if not loop.last %}, {% endif %}
{% endfor %}
</small>
{% if issue.guaranteed_profit > 0 %}
<br>
<small style="color: #cc0000;">Guaranteed profit per 100 bet: {{ "%.2f"|format(issue.guaranteed_profit) }}</small>
{% endif %}
</div>
{% endfor %}
{% else %}
<div class="alert alert-success">
<h3>✅ No Sure Bet Conditions Detected</h3>
<p>All matches in this fixture have safe odds distribution.</p>
</div>
{% endif %}
{% if analysis_result.success is defined and analysis_result.success and analysis_result.matches_repaired is defined %}
<div class="repair-success">
<h3>🔧 Repair Complete</h3>
<p>Repaired {{ analysis_result.matches_repaired }} matches ({{ analysis_result.total_repairs }} odds adjusted).</p>
<p>Remaining issues: {{ analysis_result.remaining_issues }}</p>
</div>
{% endif %}
</div>
{% endif %}
<!-- Fixture Information -->
<div class="fixture-info">
<div class="info-item">
......
......@@ -412,7 +412,7 @@ class FixtureParser:
status='ERROR', error_message=error_msg)
return False, error_msg, []
def save_matches_to_database(self, parsed_matches: List[Dict], file_sha1sum: str) -> Tuple[bool, str, List[int]]:
def save_matches_to_database(self, parsed_matches: List[Dict], file_sha1sum: str) -> Tuple[bool, str, List[int], str]:
"""
Save parsed matches to database
......@@ -421,7 +421,7 @@ class FixtureParser:
file_sha1sum: SHA1 checksum of the fixture file
Returns:
tuple: (success, error_message, list_of_match_ids)
tuple: (success, error_message, list_of_match_ids, fixture_id)
"""
try:
saved_match_ids = []
......@@ -472,13 +472,13 @@ class FixtureParser:
db.session.commit()
logger.info(f"Successfully saved {len(saved_match_ids)} matches to database")
return True, None, saved_match_ids
return True, None, saved_match_ids, fixture_id
except Exception as e:
db.session.rollback()
error_msg = f"Database save failed: {str(e)}"
logger.error(error_msg)
return False, error_msg, []
return False, error_msg, [], None
def get_parsing_statistics(self) -> Dict:
"""Get fixture parsing statistics"""
......
......@@ -52,7 +52,7 @@ def upload_fixture():
return render_template('upload/fixture.html', form=form)
# Save matches to database
success, save_error, match_ids = fixture_parser.save_matches_to_database(
success, save_error, match_ids, fixture_id = fixture_parser.save_matches_to_database(
parsed_matches, upload_record.sha1sum
)
......@@ -61,7 +61,8 @@ def upload_fixture():
return render_template('upload/fixture.html', form=form)
flash(f'Successfully uploaded and parsed {len(match_ids)} matches!', 'success')
return redirect(url_for('main.matches'))
# Redirect to the analysis page to check for sure bet conditions
return redirect(url_for('main.analyze_fixture_sure_bets', fixture_id=fixture_id))
except Exception as e:
logger.error(f"Fixture upload error: {str(e)}")
......
"""
Sure Bet Analysis Module
This module provides functionality to detect and repair "sure bet" (arbitrage) conditions
in betting fixtures. A sure bet occurs when the odds distribution allows a player to
guarantee a win by betting on all possible outcomes.
The extraction logic:
- First extraction: under/over (one winner)
- Second extraction: win1, win2, ko1, ko2, ret1, ret2, draw (one winner)
- ko1 wins → ko1 and win2 win
- ko2 wins → ko2 and win1 win
- ret1 wins → ret1 and win2 win
- ret2 wins → ret2 and win1 win
- win1 wins → only win1 wins
- win2 wins → only win2 wins
- draw wins → only draw wins
"""
import logging
import uuid
from typing import Dict, List, Tuple, Optional, Any
from datetime import datetime
logger = logging.getLogger(__name__)
class SureBetIssue:
"""Represents a detected sure bet issue in a match"""
def __init__(self, match_id: int, match_number: int, extraction: str,
issue_type: str, description: str, outcomes_involved: List[str],
odds_values: Dict[str, float], guaranteed_profit: float = 0.0):
self.match_id = match_id
self.match_number = match_number
self.extraction = extraction # 'first' or 'second'
self.issue_type = issue_type # 'over_under_sure_bet', 'multi_outcome_sure_bet', etc.
self.description = description
self.outcomes_involved = outcomes_involved
self.odds_values = odds_values
self.guaranteed_profit = guaranteed_profit
def to_dict(self) -> Dict[str, Any]:
return {
'match_id': self.match_id,
'match_number': self.match_number,
'extraction': self.extraction,
'issue_type': self.issue_type,
'description': self.description,
'outcomes_involved': self.outcomes_involved,
'odds_values': self.odds_values,
'guaranteed_profit': self.guaranteed_profit
}
class SureBetAnalyzer:
"""
Analyzes fixture matches for sure bet conditions.
A sure bet (arbitrage) occurs when:
1. First extraction: under > 2 AND over > 2 (betting equally on both guarantees profit)
2. Second extraction:
- win1 > 3 AND win2 > 3 AND draw > 3 (betting on all three guarantees profit)
- For 2-outcome scenarios (ko1, ko2, ret1, ret2): product of odds > 4
"""
# Minimum odds threshold for sure bet detection
MIN_SURE_BET_ODDS_FIRST = 2.0 # For under/over
MIN_SURE_BET_ODDS_SECOND = 3.0 # For win1, win2, draw
MIN_SURE_BET_PRODUCT = 4.0 # For 2-outcome scenarios (ko1, ko2, ret1, ret2)
# Second extraction outcome groups
SECOND_EXTRACTION_OUTCOMES = ['win1', 'win2', 'ko1', 'ko2', 'ret1', 'ret2', 'draw']
# Outcomes that pay out together for second extraction
# (extracted outcome: [winning outcomes])
SECOND_EXTRACTION_PAYOUTS = {
'ko1': ['ko1', 'win2'],
'ko2': ['ko2', 'win1'],
'ret1': ['ret1', 'win2'],
'ret2': ['ret2', 'win1'],
'win1': ['win1'],
'win2': ['win2'],
'draw': ['draw']
}
def analyze_match(self, match) -> List[SureBetIssue]:
"""
Analyze a single match for sure bet conditions.
Args:
match: Match model object with outcomes relationship
Returns:
List of SureBetIssue objects
"""
issues = []
# Get outcomes as dictionary
outcomes = match.outcomes.all()
outcomes_dict = {o.column_name: float(o.float_value) for o in outcomes}
# Check first extraction (under/over)
first_extraction_issues = self._check_first_extraction(
match.id, match.match_number, outcomes_dict
)
issues.extend(first_extraction_issues)
# Check second extraction
second_extraction_issues = self._check_second_extraction(
match.id, match.match_number, outcomes_dict
)
issues.extend(second_extraction_issues)
return issues
def _check_first_extraction(self, match_id: int, match_number: int,
outcomes: Dict[str, float]) -> List[SureBetIssue]:
"""
Check first extraction (under/over) for sure bet conditions.
A sure bet exists if both under and over have odds > 2.
In this case, betting equally on both guarantees:
- If under wins: you win (under_odds * bet) - 2*bet
- If over wins: you win (over_odds * bet) - 2*bet
"""
issues = []
under_odds = outcomes.get('under')
over_odds = outcomes.get('over')
if under_odds and over_odds:
if under_odds > self.MIN_SURE_BET_ODDS_FIRST and over_odds > self.MIN_SURE_BET_ODDS_FIRST:
# Calculate guaranteed profit (betting 100 on each)
bet_amount = 100
under_profit = (under_odds * bet_amount) - (2 * bet_amount)
over_profit = (over_odds * bet_amount) - (2 * bet_amount)
guaranteed_profit = min(under_profit, over_profit)
issue = SureBetIssue(
match_id=match_id,
match_number=match_number,
extraction='first',
issue_type='over_under_sure_bet',
description=f"Under ({under_odds}) and Over ({over_odds}) both have odds > {self.MIN_SURE_BET_ODDS_FIRST}. "
f"Betting equally on both guarantees profit.",
outcomes_involved=['under', 'over'],
odds_values={'under': under_odds, 'over': over_odds},
guaranteed_profit=guaranteed_profit
)
issues.append(issue)
logger.warning(f"Match #{match_number}: Sure bet detected - under={under_odds}, over={over_odds}")
return issues
def _check_second_extraction(self, match_id: int, match_number: int,
outcomes: Dict[str, float]) -> List[SureBetIssue]:
"""
Check second extraction for sure bet conditions.
Second extraction outcomes: win1, win2, ko1, ko2, ret1, ret2, draw
Winning scenarios:
- ko1 extracted: ko1 and win2 win
- ko2 extracted: ko2 and win1 win
- ret1 extracted: ret1 and win2 win
- ret2 extracted: ret2 and win1 win
- win1 extracted: only win1 wins
- win2 extracted: only win2 wins
- draw extracted: only draw wins
Sure bet conditions:
1. win1 > 3 AND win2 > 3 AND draw > 3: betting on all 3 guarantees profit
2. For 2-outcome scenarios (ko1, ko2, ret1, ret2): product of odds > 4
"""
issues = []
# Get available second extraction outcomes
available_outcomes = {k: v for k, v in outcomes.items()
if k in self.SECOND_EXTRACTION_OUTCOMES}
if not available_outcomes:
return issues
# Check 1: win1, win2, draw combination (all must be > 3)
# IMPORTANT: This is a special case because in second extraction,
# no matter which outcome is extracted (win1, win2, draw, ko1, ko2, ret1, ret2),
# ONE of win1, win2, or draw will ALWAYS win!
# This is because:
# - If win1 extracted: win1 wins
# - If win2 extracted: win2 wins
# - If draw extracted: draw wins
# - If ko1 extracted: win2 wins (also ko1 wins but we bet on win2)
# - If ko2 extracted: win1 wins (also ko2 wins but we bet on win1)
# - If ret1 extracted: win2 wins (also ret1 wins but we bet on win2)
# - If ret2 extracted: win1 wins (also ret2 wins but we bet on win1)
win1_odds = outcomes.get('win1')
win2_odds = outcomes.get('win2')
draw_odds = outcomes.get('draw')
if win1_odds and win2_odds and draw_odds:
if (win1_odds > self.MIN_SURE_BET_ODDS_SECOND and
win2_odds > self.MIN_SURE_BET_ODDS_SECOND and
draw_odds > self.MIN_SURE_BET_ODDS_SECOND):
# Calculate guaranteed profit (betting 100 on each)
bet_amount = 100
# One of these will win regardless of extraction result
min_payout = min(win1_odds, win2_odds, draw_odds) * bet_amount
guaranteed_profit = min_payout - (3 * bet_amount)
issue = SureBetIssue(
match_id=match_id,
match_number=match_number,
extraction='second',
issue_type='win_draw_sure_bet',
description=f"Win1 ({win1_odds}), Win2 ({win2_odds}), and Draw ({draw_odds}) "
f"all have odds > {self.MIN_SURE_BET_ODDS_SECOND}. "
f"BETTING ON ALL THREE GUARANTEES A WIN - one of them always wins "
f"regardless of extraction result (win1, win2, draw, ko1, ko2, ret1, or ret2).",
outcomes_involved=['win1', 'win2', 'draw'],
odds_values={'win1': win1_odds, 'win2': win2_odds, 'draw': draw_odds},
guaranteed_profit=guaranteed_profit
)
issues.append(issue)
logger.warning(f"Match #{match_number}: CRITICAL Sure bet detected - win1={win1_odds}, win2={win2_odds}, draw={draw_odds}")
# Check 2: Two-outcome scenarios (ko1, ko2, ret1, ret2)
# These pay out on 2 outcomes, so product of odds must be > 4 for sure bet
# ko1 scenario: ko1 + win2 both win
ko1_odds = outcomes.get('ko1')
if ko1_odds and win2_odds:
product = ko1_odds * win2_odds
if product > self.MIN_SURE_BET_PRODUCT:
bet_amount = 100
# If ko1 wins: ko1 pays (ko1*100), win2 loses
# If win2 wins: win2 pays (win2*100), ko1 loses
# Both pay out, so player gets the sum
min_payout = min(ko1_odds, win2_odds) * bet_amount
guaranteed_profit = min_payout - (2 * bet_amount)
issue = SureBetIssue(
match_id=match_id,
match_number=match_number,
extraction='second',
issue_type='ko1_win2_sure_bet',
description=f"KO1 ({ko1_odds}) and Win2 ({win2_odds}) product ({product:.2f}) > {self.MIN_SURE_BET_PRODUCT}. "
f"If KO1 wins, both KO1 and Win2 pay out. Betting equally guarantees profit.",
outcomes_involved=['ko1', 'win2'],
odds_values={'ko1': ko1_odds, 'win2': win2_odds},
guaranteed_profit=guaranteed_profit
)
issues.append(issue)
logger.warning(f"Match #{match_number}: Sure bet detected - ko1={ko1_odds}, win2={win2_odds}, product={product:.2f}")
# ko2 scenario: ko2 + win1 both win
ko2_odds = outcomes.get('ko2')
if ko2_odds and win1_odds:
product = ko2_odds * win1_odds
if product > self.MIN_SURE_BET_PRODUCT:
bet_amount = 100
min_payout = min(ko2_odds, win1_odds) * bet_amount
guaranteed_profit = min_payout - (2 * bet_amount)
issue = SureBetIssue(
match_id=match_id,
match_number=match_number,
extraction='second',
issue_type='ko2_win1_sure_bet',
description=f"KO2 ({ko2_odds}) and Win1 ({win1_odds}) product ({product:.2f}) > {self.MIN_SURE_BET_PRODUCT}. "
f"If KO2 wins, both KO2 and Win1 pay out. Betting equally guarantees profit.",
outcomes_involved=['ko2', 'win1'],
odds_values={'ko2': ko2_odds, 'win1': win1_odds},
guaranteed_profit=guaranteed_profit
)
issues.append(issue)
logger.warning(f"Match #{match_number}: Sure bet detected - ko2={ko2_odds}, win1={win1_odds}, product={product:.2f}")
# ret1 scenario: ret1 + win2 both win
ret1_odds = outcomes.get('ret1')
if ret1_odds and win2_odds:
product = ret1_odds * win2_odds
if product > self.MIN_SURE_BET_PRODUCT:
bet_amount = 100
min_payout = min(ret1_odds, win2_odds) * bet_amount
guaranteed_profit = min_payout - (2 * bet_amount)
issue = SureBetIssue(
match_id=match_id,
match_number=match_number,
extraction='second',
issue_type='ret1_win2_sure_bet',
description=f"Ret1 ({ret1_odds}) and Win2 ({win2_odds}) product ({product:.2f}) > {self.MIN_SURE_BET_PRODUCT}. "
f"If Ret1 wins, both Ret1 and Win2 pay out. Betting equally guarantees profit.",
outcomes_involved=['ret1', 'win2'],
odds_values={'ret1': ret1_odds, 'win2': win2_odds},
guaranteed_profit=guaranteed_profit
)
issues.append(issue)
logger.warning(f"Match #{match_number}: Sure bet detected - ret1={ret1_odds}, win2={win2_odds}, product={product:.2f}")
# ret2 scenario: ret2 + win1 both win
ret2_odds = outcomes.get('ret2')
if ret2_odds and win1_odds:
product = ret2_odds * win1_odds
if product > self.MIN_SURE_BET_PRODUCT:
bet_amount = 100
min_payout = min(ret2_odds, win1_odds) * bet_amount
guaranteed_profit = min_payout - (2 * bet_amount)
issue = SureBetIssue(
match_id=match_id,
match_number=match_number,
extraction='second',
issue_type='ret2_win1_sure_bet',
description=f"Ret2 ({ret2_odds}) and Win1 ({win1_odds}) product ({product:.2f}) > {self.MIN_SURE_BET_PRODUCT}. "
f"If Ret2 wins, both Ret2 and Win1 pay out. Betting equally guarantees profit.",
outcomes_involved=['ret2', 'win1'],
odds_values={'ret2': ret2_odds, 'win1': win1_odds},
guaranteed_profit=guaranteed_profit
)
issues.append(issue)
logger.warning(f"Match #{match_number}: Sure bet detected - ret2={ret2_odds}, win1={win1_odds}, product={product:.2f}")
return issues
def analyze_fixture(self, matches: List) -> Dict[str, Any]:
"""
Analyze all matches in a fixture for sure bet conditions.
Args:
matches: List of Match objects
Returns:
Dictionary with analysis results
"""
all_issues = []
match_results = []
for match in matches:
issues = self.analyze_match(match)
if issues:
all_issues.extend(issues)
for issue in issues:
match_results.append({
'match_id': match.id,
'match_number': match.match_number,
'issues': [i.to_dict() for i in issues]
})
return {
'total_matches': len(matches),
'matches_with_issues': len(set(i.match_id for i in all_issues)),
'total_issues': len(all_issues),
'has_sure_bets': len(all_issues) > 0,
'issues': [i.to_dict() for i in all_issues],
'match_results': match_results
}
class SureBetRepair:
"""
Repairs sure bet conditions by adjusting odds.
Strategies:
1. For first extraction (under/over): reduce one or both odds
2. For second extraction (combinations): reduce the offending odds
"""
# Target odds to reduce to (slightly below sure bet threshold)
TARGET_ODDS_FIRST = 1.95 # Just below 2.0 threshold
TARGET_ODDS_SECOND = 2.95 # Just below 3.0 threshold
TARGET_PRODUCT = 3.90 # Just below 4.0 threshold
def __init__(self, analyzer: SureBetAnalyzer = None):
self.analyzer = analyzer or SureBetAnalyzer()
def repair_match(self, match, issues: List[SureBetIssue]) -> Dict[str, Any]:
"""
Repair a match by adjusting odds to eliminate sure bet conditions.
Args:
match: Match model object
issues: List of SureBetIssue objects to fix
Returns:
Dictionary with repair results
"""
from app import db
from app.models import MatchOutcome
repairs_made = []
outcomes = match.outcomes.all()
outcomes_dict = {o.column_name: float(o.float_value) for o in outcomes}
for issue in issues:
repair_action = None
if issue.issue_type == 'over_under_sure_bet':
# Reduce both under and over odds
under_odds = outcomes_dict.get('under')
over_odds = outcomes_dict.get('over')
if under_odds and over_odds:
new_under = self.TARGET_ODDS_FIRST
new_over = self.TARGET_ODDS_FIRST
# Update in database
outcome = MatchOutcome.query.filter_by(
match_id=match.id, column_name='under'
).first()
if outcome:
outcome.float_value = new_under
repairs_made.append({
'outcome': 'under',
'old_value': under_odds,
'new_value': new_under
})
outcome = MatchOutcome.query.filter_by(
match_id=match.id, column_name='over'
).first()
if outcome:
outcome.float_value = new_over
repairs_made.append({
'outcome': 'over',
'old_value': over_odds,
'new_value': new_over
})
elif issue.issue_type == 'win_draw_sure_bet':
# Reduce all three odds
win1_odds = outcomes_dict.get('win1')
win2_odds = outcomes_dict.get('win2')
draw_odds = outcomes_dict.get('draw')
for outcome_name, old_odds in [('win1', win1_odds), ('win2', win2_odds), ('draw', draw_odds)]:
if old_odds:
new_odds = self.TARGET_ODDS_SECOND
outcome = MatchOutcome.query.filter_by(
match_id=match.id, column_name=outcome_name
).first()
if outcome:
outcome.float_value = new_odds
repairs_made.append({
'outcome': outcome_name,
'old_value': old_odds,
'new_value': new_odds
})
elif issue.issue_type == 'ko1_win2_sure_bet':
# Reduce either ko1 or win2
ko1_odds = outcomes_dict.get('ko1')
win2_odds = outcomes_dict.get('win2')
# Reduce the higher one more
if ko1_odds and win2_odds:
if ko1_odds >= win2_odds:
new_ko1 = win2_odds * 0.95 # Slightly below threshold
outcome = MatchOutcome.query.filter_by(
match_id=match.id, column_name='ko1'
).first()
if outcome:
outcome.float_value = new_ko1
repairs_made.append({
'outcome': 'ko1',
'old_value': ko1_odds,
'new_value': new_ko1
})
else:
new_win2 = ko1_odds * 0.95
outcome = MatchOutcome.query.filter_by(
match_id=match.id, column_name='win2'
).first()
if outcome:
outcome.float_value = new_win2
repairs_made.append({
'outcome': 'win2',
'old_value': win2_odds,
'new_value': new_win2
})
elif issue.issue_type == 'ko2_win1_sure_bet':
ko2_odds = outcomes_dict.get('ko2')
win1_odds = outcomes_dict.get('win1')
if ko2_odds and win1_odds:
if ko2_odds >= win1_odds:
new_ko2 = win1_odds * 0.95
outcome = MatchOutcome.query.filter_by(
match_id=match.id, column_name='ko2'
).first()
if outcome:
outcome.float_value = new_ko2
repairs_made.append({
'outcome': 'ko2',
'old_value': ko2_odds,
'new_value': new_ko2
})
else:
new_win1 = ko2_odds * 0.95
outcome = MatchOutcome.query.filter_by(
match_id=match.id, column_name='win1'
).first()
if outcome:
outcome.float_value = new_win1
repairs_made.append({
'outcome': 'win1',
'old_value': win1_odds,
'new_value': new_win1
})
elif issue.issue_type == 'ret1_win2_sure_bet':
ret1_odds = outcomes_dict.get('ret1')
win2_odds = outcomes_dict.get('win2')
if ret1_odds and win2_odds:
if ret1_odds >= win2_odds:
new_ret1 = win2_odds * 0.95
outcome = MatchOutcome.query.filter_by(
match_id=match.id, column_name='ret1'
).first()
if outcome:
outcome.float_value = new_ret1
repairs_made.append({
'outcome': 'ret1',
'old_value': ret1_odds,
'new_value': new_ret1
})
else:
new_win2 = ret1_odds * 0.95
outcome = MatchOutcome.query.filter_by(
match_id=match.id, column_name='win2'
).first()
if outcome:
outcome.float_value = new_win2
repairs_made.append({
'outcome': 'win2',
'old_value': win2_odds,
'new_value': new_win2
})
elif issue.issue_type == 'ret2_win1_sure_bet':
ret2_odds = outcomes_dict.get('ret2')
win1_odds = outcomes_dict.get('win1')
if ret2_odds and win1_odds:
if ret2_odds >= win1_odds:
new_ret2 = win1_odds * 0.95
outcome = MatchOutcome.query.filter_by(
match_id=match.id, column_name='ret2'
).first()
if outcome:
outcome.float_value = new_ret2
repairs_made.append({
'outcome': 'ret2',
'old_value': ret2_odds,
'new_value': new_ret2
})
else:
new_win1 = ret2_odds * 0.95
outcome = MatchOutcome.query.filter_by(
match_id=match.id, column_name='win1'
).first()
if outcome:
outcome.float_value = new_win1
repairs_made.append({
'outcome': 'win1',
'old_value': win1_odds,
'new_value': new_win1
})
# Commit changes
if repairs_made:
match.updated_at = datetime.utcnow()
db.session.commit()
logger.info(f"Repaired {len(repairs_made)} odds for match #{match.match_number}")
return {
'match_id': match.id,
'match_number': match.match_number,
'repairs_made': repairs_made,
'success': len(repairs_made) > 0
}
def save_as_new_fixture(self, original_matches: List, user_id: int) -> Tuple[bool, str, Optional[str]]:
"""
Create a copy of the fixture with a new fixture_id.
Args:
original_matches: List of Match objects to copy
user_id: ID of user performing the action
Returns:
Tuple of (success, message, new_fixture_id)
"""
from app import db
from app.models import Match, MatchOutcome
try:
new_fixture_id = str(uuid.uuid4())
new_match_ids = []
for match in original_matches:
# Create new match with new fixture_id
new_match = Match(
match_number=match.match_number,
fighter1_township=match.fighter1_township,
fighter2_township=match.fighter2_township,
venue_kampala_township=match.venue_kampala_township,
filename=match.filename,
file_sha1sum=match.file_sha1sum,
fixture_id=new_fixture_id,
created_by=user_id,
start_time=match.start_time,
end_time=match.end_time,
status='pending',
active_status=False
)
db.session.add(new_match)
db.session.flush()
# Copy all outcomes
for outcome in match.outcomes.all():
new_outcome = MatchOutcome(
match_id=new_match.id,
column_name=outcome.column_name,
float_value=outcome.float_value
)
db.session.add(new_outcome)
new_match_ids.append(new_match.id)
db.session.commit()
logger.info(f"Created new fixture {new_fixture_id} with {len(new_match_ids)} matches")
return True, f"Successfully created new fixture with {len(new_match_ids)} matches", new_fixture_id
except Exception as e:
db.session.rollback()
logger.error(f"Failed to save as new fixture: {str(e)}")
return False, f"Failed to create new fixture: {str(e)}", None
# Global instances
_sure_bet_analyzer = None
_sure_bet_repair = None
def get_sure_bet_analyzer() -> SureBetAnalyzer:
"""Get singleton SureBetAnalyzer instance"""
global _sure_bet_analyzer
if _sure_bet_analyzer is None:
_sure_bet_analyzer = SureBetAnalyzer()
return _sure_bet_analyzer
def get_sure_bet_repair() -> SureBetRepair:
"""Get singleton SureBetRepair instance"""
global _sure_bet_repair
if _sure_bet_repair is None:
_sure_bet_repair = SureBetRepair()
return _sure_bet_repair
def analyze_fixture_for_sure_bets(fixture_id: str) -> Dict[str, Any]:
"""
Analyze a fixture for sure bet conditions.
Args:
fixture_id: The fixture ID to analyze
Returns:
Dictionary with analysis results
"""
from app.models import Match
matches = Match.query.filter_by(fixture_id=fixture_id).all()
if not matches:
return {
'success': False,
'error': 'Fixture not found',
'total_matches': 0,
'has_sure_bets': False,
'issues': []
}
analyzer = get_sure_bet_analyzer()
result = analyzer.analyze_fixture(matches)
result['success'] = True
result['fixture_id'] = fixture_id
return result
def repair_fixture_sure_bets(fixture_id: str) -> Dict[str, Any]:
"""
Repair sure bet conditions in a fixture.
Args:
fixture_id: The fixture ID to repair
Returns:
Dictionary with repair results
"""
from app.models import Match
matches = Match.query.filter_by(fixture_id=fixture_id).all()
if not matches:
return {
'success': False,
'error': 'Fixture not found',
'repairs_made': 0
}
analyzer = get_sure_bet_analyzer()
repair = get_sure_bet_repair()
all_repairs = []
for match in matches:
issues = analyzer.analyze_match(match)
if issues:
result = repair.repair_match(match, issues)
all_repairs.append(result)
# Re-analyze to confirm repairs worked
new_result = analyzer.analyze_fixture(matches)
return {
'success': True,
'fixture_id': fixture_id,
'matches_repaired': len(all_repairs),
'total_repairs': sum(len(r.get('repairs_made', [])) for r in all_repairs),
'remaining_issues': new_result.get('total_issues', 0),
'repair_details': all_repairs
}
def save_fixture_as_new(fixture_id: str, user_id: int) -> Dict[str, Any]:
"""
Save a fixture as a new fixture (for clients to update).
Args:
fixture_id: The fixture ID to copy
user_id: ID of user performing the action
Returns:
Dictionary with results
"""
from app.models import Match
matches = Match.query.filter_by(fixture_id=fixture_id).all()
if not matches:
return {
'success': False,
'error': 'Fixture not found'
}
repair = get_sure_bet_repair()
success, message, new_fixture_id = repair.save_as_new_fixture(matches, user_id)
return {
'success': success,
'message': message,
'new_fixture_id': new_fixture_id,
'original_fixture_id': fixture_id,
'match_count': len(matches)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment