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)}")
......
This diff is collapsed.
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