Fix betting interface

parent c265b445
# MbetterClient Result Extraction Algorithm
## Overview
The Result Extraction Algorithm is the core component responsible for determining match outcomes in the MbetterClient betting system. It ensures fair play while maintaining system profitability through sophisticated redistribution controls and CAP (Controlled Redistribution) logic.
## Algorithm Flow
### Phase 1: Initialization and Data Collection
#### Step 1.1: Input Validation
- **Inputs**: `fixture_id`, `match_id`
- **Validation**: Ensure match exists and has pending bets
- **Initialization**: Set `selected_result = None`, `extraction_winning_outcome_names = []`
#### Step 1.2: Match Outcomes Retrieval
```sql
SELECT column_name, float_value FROM match_outcomes WHERE match_id = ?
```
- Retrieves all possible betting outcomes for the match (WIN1, WIN2, DRAW, KO1, KO2, UNDER, OVER, etc.)
- Each outcome has an associated coefficient (payout multiplier)
#### Step 1.3: Result Options Filtering
```sql
SELECT result_name FROM result_options
WHERE is_active = true
AND result_name IN (match_outcomes_list)
AND result_name NOT IN ('UNDER', 'OVER')
```
- Identifies active result options that correspond to match outcomes
- Excludes UNDER/OVER as they are handled separately
### Phase 2: Financial Analysis
#### Step 2.1: Total Payin Calculation
**Total Intake = UNDER/OVER Bets + Other Bets**
**UNDER/OVER Payin:**
```sql
SELECT SUM(amount) FROM bet_detail
WHERE match_id = ? AND outcome IN ('UNDER', 'OVER')
AND result = 'pending' AND result != 'cancelled'
```
**Other Bets Payin:**
```sql
SELECT SUM(amount) FROM bet_detail
WHERE match_id = ? AND outcome NOT IN ('UNDER', 'OVER')
AND result = 'pending' AND result != 'cancelled'
```
**Total Payin = under_payin + other_payin**
#### Step 2.2: UNDER/OVER Payout Calculation
For each UNDER/OVER outcome:
```
payout = bet_amount × coefficient
```
- UNDER payout = total UNDER bets × UNDER coefficient
- OVER payout = total OVER bets × OVER coefficient
#### Step 2.3: CAP Threshold Calculation
**Base CAP Threshold = Total Payin × CAP Percentage**
Where CAP Percentage defaults to 70% but is configurable.
**Adjusted CAP Threshold = Base CAP + Accumulated Shortfall**
**Final CAP Threshold = Adjusted CAP - UNDER/OVER Winner Payout**
If UNDER wins: `Final CAP = Adjusted CAP - under_payout`
If OVER wins: `Final CAP = Adjusted CAP - over_payout`
### Phase 3: Result Selection
#### Step 3.1: Payout Calculation for Each Result
For each possible result (WIN1, WIN2, DRAW, etc.):
**Find Associated Outcomes:**
```sql
SELECT outcome_name FROM extraction_associations
WHERE extraction_result = 'result_name'
```
**Calculate Total Result Payout:**
```
result_payout = 0
FOR EACH associated_outcome:
bet_amount = SUM(bets on associated_outcome)
coefficient = outcome_coefficient
result_payout += bet_amount × coefficient
```
**Example:**
- Result: "WIN1"
- Associated outcomes: "KO1" (coeff: 3.0), "SUB1" (coeff: 2.5)
- Bets: $10 on KO1, $15 on SUB1
- WIN1 payout = ($10 × 3.0) + ($15 × 2.5) = $30 + $37.50 = $67.50
#### Step 3.2: Eligibility Filtering
```
eligible_results = {result: payout for result, payout in payouts.items()
if payout <= final_cap_threshold}
```
#### Step 3.3: Fallback Logic
If no results are eligible (all payouts exceed CAP):
```
lowest_payout_result = min(payouts, key=payouts.get)
eligible_results = {lowest_payout_result: payouts[lowest_payout_result]}
```
#### Step 3.4: Weighted Random Selection
Among eligible results, select the one with maximum payout:
```
max_payout = max(eligible_results.values())
candidates = [r for r, p in eligible_results.items() if p == max_payout]
selected_result = random.choice(candidates)
```
### Phase 4: Winning Outcomes Determination
#### Step 4.1: Association Lookup
```sql
SELECT DISTINCT outcome_name FROM extraction_associations
WHERE extraction_result = selected_result
```
#### Step 4.2: Validation Against Match Outcomes
```
extraction_winning_outcome_names = [outcome for outcome in associated_outcomes
if outcome in match_possible_outcomes]
```
### Phase 5: Bet Result Updates
#### Step 5.1: UNDER/OVER Bet Processing
Based on stored `under_over_result` from video selection:
**If UNDER wins:**
- Mark UNDER bets as 'win' with `win_amount = bet_amount × under_coefficient`
- Mark OVER bets as 'lost'
**If OVER wins:**
- Mark OVER bets as 'win' with `win_amount = bet_amount × over_coefficient`
- Mark UNDER bets as 'lost'
#### Step 5.2: Selected Result Processing
Mark bets on `selected_result` as 'win':
```
win_amount = bet_amount × result_coefficient
```
#### Step 5.3: Associated Outcomes Processing
For each outcome in `extraction_winning_outcome_names`:
```
outcome_coefficient = get_coefficient(match_id, outcome)
win_amount = bet_amount × outcome_coefficient
```
#### Step 5.4: Loss Processing
Mark all other bets as 'lost':
```
losing_outcomes = [selected_result] + extraction_winning_outcome_names + ['UNDER', 'OVER']
UPDATE bet_detail SET result = 'lost'
WHERE match_id = ? AND outcome NOT IN losing_outcomes AND result = 'pending'
```
### Phase 6: Database Updates
#### Step 6.1: Match Result Storage
```sql
UPDATE matches SET
result = selected_result,
winning_outcomes = json.dumps(extraction_winning_outcome_names),
under_over_result = under_over_result,
result_breakdown = json.dumps({
'selected_result': selected_result,
'winning_outcomes': extraction_winning_outcome_names,
'under_over_result': under_over_result
})
WHERE id = match_id
```
#### Step 6.2: Statistics Collection
Store comprehensive extraction metrics in `extraction_stats` table:
- Total bets, amounts collected, redistributed
- UNDER/OVER statistics
- CAP applied status
- Result breakdown
#### Step 6.3: Shortfall Tracking
**Expected Redistribution = Total Payin × CAP Percentage**
**Actual Redistribution = Sum of all win_amounts**
**Shortfall = max(0, Expected - Actual)**
Update daily shortfall tracking for future CAP adjustments.
### Phase 7: System Notifications
#### Step 7.1: Result Broadcasting
Send `PLAY_VIDEO_RESULTS` message with:
- `fixture_id`, `match_id`
- `result = selected_result`
- `under_over_result`
- `winning_outcomes = extraction_winning_outcome_names`
#### Step 7.2: Match Completion
Send `MATCH_DONE` message to advance to next match.
## Key Design Principles
### CAP (Controlled Redistribution) Logic
- **Purpose**: Prevent excessive payouts that could harm profitability
- **Mechanism**: Only select results where total payout ≤ CAP threshold
- **Adjustment Factors**:
- Accumulated shortfalls from previous extractions
- Committed UNDER/OVER payouts
- Total intake from all betting activity
### Multi-Level Winning System
- **Primary Result**: Main outcome selected by algorithm
- **Associated Outcomes**: Additional winning outcomes
- **UNDER/OVER Independence**: Parallel result system
### Profit Maximization
- **Weighted Selection**: Prefers results maximizing redistribution
- **Fallback Protection**: Always selects result, even if CAP exceeded
- **Shortfall Carryover**: Tracks and compensates for under-redistribution
### Data Integrity
- **Transaction Safety**: All updates in database transactions
- **Comprehensive Logging**: All decisions and calculations logged
- **Error Recovery**: Multiple fallback mechanisms
## Configuration Parameters
- **CAP Percentage**: Default 70%, configurable via `extraction_redistribution_cap`
- **Shortfall Tracking**: Daily accumulated shortfalls affect future CAP calculations
- **Result Associations**: Configurable via `extraction_associations` table
- **Betting Mode**: Affects match status progression but not extraction logic
## Error Handling
- **Fallback Selection**: Random selection if extraction fails
- **CAP Override**: Selects lowest payout result if all exceed CAP
- **Transaction Rollback**: Database consistency maintained on errors
- **Logging**: Comprehensive debug information for troubleshooting
## Performance Considerations
- **Database Efficiency**: Single transactions for all updates
- **Memory Management**: Streaming result processing for large datasets
- **Concurrent Safety**: Match-level locking prevents race conditions
- **Audit Trail**: Complete history of all extraction decisions
\ No newline at end of file
......@@ -205,7 +205,7 @@ Examples:
parser.add_argument(
'--version',
action='version',
version='MbetterClient 1.0.0'
version='MbetterClient 1.0.9'
)
# Timer options
......
......@@ -512,7 +512,7 @@ def main():
"""Main entry point"""
app = QApplication(sys.argv)
app.setApplicationName("MBetter Discovery")
app.setApplicationVersion("1.0.0")
app.setApplicationVersion("1.0.9")
app.setQuitOnLastWindowClosed(False) # Keep running in tray
# Create and show main window
......
......@@ -4,7 +4,7 @@ MbetterClient - Cross-platform multimedia client application
A multi-threaded application with video playback, web dashboard, and REST API integration.
"""
__version__ = "1.0.0"
__version__ = "1.0.9"
__author__ = "MBetter Project"
__email__ = "dev@mbetter.net"
__description__ = "Cross-platform multimedia client with video overlay and web dashboard"
......
......@@ -657,6 +657,31 @@ class UpdatesResponseHandler(ResponseHandler):
fixture_id = fixture_row.fixture_id
logger.debug(f"Validating fixture: {fixture_id}")
# Skip validation for recycled fixtures (copies of already verified fixtures)
if fixture_id.startswith('recycle_'):
logger.debug(f"Skipping ZIP validation for recycled fixture: {fixture_id}")
continue
# Skip validation for fixtures not from today (only today's fixtures need verification)
# Get fixture_active_time from any match in this fixture
fixture_active_time = None
sample_match = session.query(MatchModel).filter(
MatchModel.fixture_id == fixture_id
).first()
if sample_match and sample_match.fixture_active_time:
fixture_active_time = sample_match.fixture_active_time
if fixture_active_time:
# Convert Unix timestamp to date
from datetime import datetime, date
fixture_date = datetime.fromtimestamp(fixture_active_time).date()
today = date.today()
if fixture_date != today:
logger.debug(f"Skipping ZIP validation for fixture {fixture_id} from {fixture_date} (not today)")
continue
# Get all matches for this fixture that have ZIP files
matches_with_zips = session.query(MatchModel).filter(
MatchModel.fixture_id == fixture_id,
......
......@@ -262,7 +262,7 @@ class ApiConfig:
# Request settings
verify_ssl: bool = True
user_agent: str = "MbetterClient/1.0"
user_agent: str = "MbetterClient/1.0r9"
max_response_size_mb: int = 100
# Additional API client settings
......@@ -366,7 +366,7 @@ class AppSettings:
timer: TimerConfig = field(default_factory=TimerConfig)
# Application settings
version: str = "1.0.0"
version: str = "1.0.9"
debug_mode: bool = False
dev_message: bool = False # Enable debug mode showing only message bus messages
debug_messages: bool = False # Show all messages passing through the message bus on screen
......
This diff is collapsed.
......@@ -372,8 +372,8 @@ class MatchTimerComponent(ThreadedComponent):
target_match = self._find_next_match_in_list(matches)
if not target_match:
# Priority 2: Find matches in today's fixtures
today = datetime.now().date()
# Priority 2: Find matches in today's fixtures (using UTC for consistency)
today = datetime.utcnow().date()
today_matches = session.query(MatchModel).filter(
MatchModel.start_time >= datetime.combine(today, datetime.min.time()),
MatchModel.start_time < datetime.combine(today, datetime.max.time())
......
......@@ -435,8 +435,8 @@ class OverlayWebChannel(QObject):
session = self.db_manager.get_session()
try:
# Get today's date
today = datetime.now().date()
# Get today's date in UTC (consistent with database storage)
today = datetime.utcnow().date()
# Get active matches for today (non-terminal states)
active_matches = session.query(MatchModel).filter(
......
......@@ -1242,7 +1242,7 @@
// Show no matches message
function showNoMatches(message) {
document.getElementById('loadingMessage').style.display = 'none';
document.getElementById('fixturesContent').style.display = 'none';
document.getElementById('matchContent').style.display = 'none';
const noMatches = document.getElementById('noMatches');
noMatches.textContent = message;
noMatches.style.display = 'block';
......
......@@ -209,7 +209,7 @@ class WebDashboard(ThreadedComponent):
def inject_globals():
return {
'app_name': 'MbetterClient',
'app_version': '1.0.0',
'app_version': '1.0.9',
'current_time': time.time(),
}
......
......@@ -785,7 +785,7 @@ def cashier_bet_details(bet_id):
has_pending = True
elif is_bet_detail_winning(detail, match, session):
results['won'] += 1
results['winnings'] += float(detail.amount) * float(odds) # Use actual odds
results['winnings'] += float(detail.win_amount or 0) # Use actual win amount
elif detail.result == 'lost':
results['lost'] += 1
elif detail.result == 'cancelled':
......@@ -4847,9 +4847,11 @@ def get_available_matches_for_betting():
# Get actual match outcomes from the database
match_outcomes = session.query(MatchOutcomeModel).filter_by(match_id=match.id).all()
# Convert outcomes to betting options format
# Convert outcomes to betting options format - show ALL fixture outcomes
betting_outcomes = []
existing_outcome_names = set()
for outcome in match_outcomes:
betting_outcomes.append({
'outcome_id': outcome.id,
......@@ -4857,6 +4859,24 @@ def get_available_matches_for_betting():
'outcome_value': outcome.float_value,
'display_name': outcome.column_name # Use actual outcome name from database
})
existing_outcome_names.add(outcome.column_name)
# Always add UNDER and OVER outcomes if not already present
if 'UNDER' not in existing_outcome_names:
betting_outcomes.append({
'outcome_id': None, # No database ID for UNDER/OVER
'outcome_name': 'UNDER',
'outcome_value': None,
'display_name': 'UNDER'
})
if 'OVER' not in existing_outcome_names:
betting_outcomes.append({
'outcome_id': None, # No database ID for UNDER/OVER
'outcome_name': 'OVER',
'outcome_value': None,
'display_name': 'OVER'
})
# If no outcomes found, fallback to standard betting options as safety measure
if not betting_outcomes:
......@@ -5610,10 +5630,9 @@ def get_match_reports():
match_stats[match_id]['bets_count'] += 1
match_stats[match_id]['payin'] += float(detail.amount)
# Calculate potential payout if bet was won
if detail.result in ['won', 'win']:
# Get match to find odds
match = session.query(MatchModel).filter_by(id=match_id).first()
# Calculate potential payout if bet was won (use same logic as bet list)
match = session.query(MatchModel).filter_by(id=match_id).first()
if is_bet_detail_winning(detail, match, session):
if match:
outcomes_dict = match.get_outcomes_dict()
odds = outcomes_dict.get(detail.outcome, 0.0)
......@@ -5887,13 +5906,10 @@ def download_excel_report():
match_stats[match_id]['bets_count'] += 1
match_stats[match_id]['payin'] += float(detail.amount)
# Calculate potential payout if bet was won
if detail.result in ['won', 'win']:
match = session.query(MatchModel).filter_by(id=match_id).first()
if match:
outcomes_dict = match.get_outcomes_dict()
odds = outcomes_dict.get(detail.outcome, 0.0)
match_stats[match_id]['payout'] += float(detail.amount) * float(odds)
# Add actual payout for winning bets (use same logic as bet list)
match = session.query(MatchModel).filter_by(id=match_id).first()
if is_bet_detail_winning(detail, match, session):
match_stats[match_id]['payout'] += float(detail.win_amount or 0)
# Write match data
row = 4
......
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