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
......
......@@ -29,13 +29,82 @@ class GamesThread(ThreadedComponent):
self.message_queue = None
self.waiting_for_validation_fixture: Optional[str] = None
def _get_today_utc_date(self) -> datetime.date:
"""Get today's date in UTC (consistent with database storage)"""
return datetime.utcnow().date()
def _check_and_handle_day_change(self) -> bool:
"""Check if day has changed and handle cleanup/reset accordingly.
Returns True if day change was detected and handled, False otherwise."""
try:
session = self.db_manager.get_session()
try:
today = self._get_today_utc_date()
# Get the current fixture if any
if not self.current_fixture_id:
return False
# Get matches for current fixture
current_matches = session.query(MatchModel).filter(
MatchModel.fixture_id == self.current_fixture_id,
MatchModel.active_status == True
).all()
if not current_matches:
return False
# Check if all matches in current fixture are from a previous day
all_matches_old_day = True
for match in current_matches:
if match.start_time:
match_date = match.start_time.date()
if match_date == today:
all_matches_old_day = False
break
if all_matches_old_day:
logger.info(f"Day change detected! Current fixture {self.current_fixture_id} is from previous day")
# Cancel all pending/bet/scheduled matches in the old fixture
old_matches = session.query(MatchModel).filter(
MatchModel.fixture_id == self.current_fixture_id,
MatchModel.active_status == True,
MatchModel.status.in_(['pending', 'scheduled', 'bet', 'ingame'])
).all()
for match in old_matches:
logger.info(f"Cancelling old match {match.match_number} due to day change: {match.fighter1_township} vs {match.fighter2_township}")
match.status = 'cancelled'
# Cancel/refund associated bets
self._cancel_match_bets(match.id, session)
session.commit()
logger.info(f"Cancelled {len(old_matches)} old matches due to day change")
# Reset game state
self.game_active = False
self.current_fixture_id = None
logger.info("Game state reset due to day change - will initialize new fixture")
return True
return False
finally:
session.close()
except Exception as e:
logger.error(f"Failed to check/handle day change: {e}")
return False
def _cleanup_stale_ingame_matches(self):
"""Clean up any stale 'ingame' matches from previous crashed sessions and old 'bet' fixtures"""
try:
session = self.db_manager.get_session()
try:
# Get today's date
today = datetime.now().date()
# Get today's date in UTC (consistent with database)
today = self._get_today_utc_date()
# PART 1: Clean up stale 'ingame' matches from today (existing logic)
stale_matches = session.query(MatchModel).filter(
......@@ -57,7 +126,7 @@ class GamesThread(ThreadedComponent):
else:
logger.info("No stale ingame matches found")
# PART 2: Clean up ALL old 'bet' fixtures (new logic)
# PART 2: Clean up ALL old 'bet' fixtures from previous days
old_bet_matches = session.query(MatchModel).filter(
MatchModel.status == 'bet',
MatchModel.active_status == True,
......@@ -69,7 +138,7 @@ class GamesThread(ThreadedComponent):
).all()
if old_bet_matches:
logger.info(f"Found {len(old_bet_matches)} old 'bet' matches - cancelling them")
logger.info(f"Found {len(old_bet_matches)} old 'bet' matches from previous days - cancelling them")
for match in old_bet_matches:
logger.info(f"Cancelling old bet match {match.match_number}: {match.fighter1_township} vs {match.fighter2_township}")
......@@ -79,9 +148,9 @@ class GamesThread(ThreadedComponent):
self._cancel_match_bets(match.id, session)
session.commit()
logger.info(f"Cancelled {len(old_bet_matches)} old bet matches")
logger.info(f"Cancelled {len(old_bet_matches)} old bet matches from previous days")
else:
logger.info("No old bet matches found to cancel")
logger.info("No old bet matches from previous days found to cancel")
finally:
session.close()
......@@ -194,6 +263,11 @@ class GamesThread(ThreadedComponent):
try:
logger.info(f"Processing START_GAME message from {message.sender}")
# Check for day change and handle cleanup if needed
day_change_handled = self._check_and_handle_day_change()
if day_change_handled:
logger.info("Day change was detected and handled - proceeding with new fixture")
# If any fixture is currently being downloaded, wait for all ZIP validation to complete
if self.waiting_for_validation_fixture is not None:
logger.info(f"Fixture {self.waiting_for_validation_fixture} is currently downloading - waiting for all ZIP files to be validated")
......@@ -786,8 +860,8 @@ class GamesThread(ThreadedComponent):
try:
session = self.db_manager.get_session()
try:
# Get today's date
today = datetime.now().date()
# Get today's date in UTC (consistent with database)
today = self._get_today_utc_date()
# Find all fixtures that have matches with today's start_time
fixtures_with_today_matches = session.query(MatchModel.fixture_id).filter(
......@@ -819,8 +893,8 @@ class GamesThread(ThreadedComponent):
try:
session = self.db_manager.get_session()
try:
# Get today's date
today = datetime.now().date()
# Get today's date in UTC (consistent with database)
today = self._get_today_utc_date()
# Find fixtures with ingame matches today
ingame_matches = session.query(MatchModel).filter(
......@@ -919,8 +993,8 @@ class GamesThread(ThreadedComponent):
try:
session = self.db_manager.get_session()
try:
# Get today's date
today = datetime.now().date()
# Get today's date in UTC (consistent with database)
today = self._get_today_utc_date()
# Find all fixtures with today's matches
all_fixtures = session.query(MatchModel.fixture_id).filter(
......@@ -1188,8 +1262,8 @@ class GamesThread(ThreadedComponent):
try:
session = self.db_manager.get_session()
try:
# Get today's date
today = datetime.now().date()
# Get today's date in UTC (consistent with database)
today = self._get_today_utc_date()
# Find fixtures with today's matches that are not in terminal states
terminal_states = ['done', 'cancelled', 'failed', 'paused']
......@@ -2268,6 +2342,10 @@ class GamesThread(ThreadedComponent):
try:
logger.info(f"🔍 [EXTRACTION DEBUG] Starting result extraction for fixture {fixture_id}, match {match_id}")
# Initialize variables for debug output
selected_result = None
extraction_winning_outcome_names = []
session = self.db_manager.get_session()
try:
# DEBUG: Check if match exists and its current state
......@@ -2368,6 +2446,25 @@ class GamesThread(ThreadedComponent):
total_bet_amount = sum(bet.amount for bet in all_bets) if all_bets else 0.0
logger.info(f"💵 [EXTRACTION DEBUG] Total bet amount calculated: {total_bet_amount:.2f} from {len(all_bets)} bets (excluding cancelled bets)")
# Get UNDER/OVER specific statistics for debug output
under_bets = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'UNDER',
BetDetailModel.result == 'pending',
BetDetailModel.result != 'cancelled'
).all()
under_count = len(under_bets) if under_bets else 0
under_amount = sum(bet.amount for bet in under_bets) if under_bets else 0.0
over_bets = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'OVER',
BetDetailModel.result == 'pending',
BetDetailModel.result != 'cancelled'
).all()
over_count = len(over_bets) if over_bets else 0
over_amount = sum(bet.amount for bet in over_bets) if over_bets else 0.0
# Step 5: Get UNDER/OVER result and calculate payouts for CAP adjustment
logger.info(f"🎯 [EXTRACTION DEBUG] Step 5: Getting UNDER/OVER result and calculating payouts for CAP adjustment")
......@@ -2386,16 +2483,21 @@ class GamesThread(ThreadedComponent):
over_payout = self._calculate_payout(match_id, 'OVER', over_coeff, session) if over_coeff else 0.0
logger.info(f"🎯 [EXTRACTION DEBUG] UNDER payout: {under_payout:.2f}, OVER payout: {over_payout:.2f}")
# Calculate total payin from UNDER/OVER bets for debug logging
total_payin = self._calculate_total_payin(match_id, session)
logger.info(f"💰 [EXTRACTION DEBUG] Total payin from UNDER/OVER bets: {total_payin:.2f}")
# Get redistribution CAP
cap_percentage = self._get_redistribution_cap()
# Get accumulated shortfall from previous extractions for today
today = datetime.now().date()
today = self._get_today_utc_date()
accumulated_shortfall = self._get_daily_shortfall(today, session)
logger.info(f"🎯 [EXTRACTION DEBUG] Accumulated shortfall: {accumulated_shortfall:.2f}")
# Calculate base CAP threshold
base_cap_threshold = total_bet_amount * (cap_percentage / 100.0)
# Calculate base CAP threshold using ALL bets (UNDER/OVER + other bets)
total_payin_all_bets = total_payin + total_bet_amount
base_cap_threshold = total_payin_all_bets * (cap_percentage / 100.0)
# Adjust CAP threshold based on UNDER/OVER result
cap_threshold = base_cap_threshold + accumulated_shortfall
......@@ -2420,7 +2522,15 @@ class GamesThread(ThreadedComponent):
logger.info(f"📊 [EXTRACTION DEBUG] Result '{result_name}': payout = {payout:.2f}, bet_count = {bet_count}")
logger.info(f"📊 [EXTRACTION DEBUG] Full payouts dict: {payouts}")
logger.info(f"📊 [EXTRACTION DEBUG] Full bet counts dict: {bet_counts}")
# Add detailed extraction debug output including payouts and winning bets sums
logger.info(f"💰 [EXTRACTION DEBUG] UNDER/OVER payouts: UNDER={under_payout:.2f}, OVER={over_payout:.2f}")
logger.info(f"💰 [EXTRACTION DEBUG] Total UNDER/OVER bets: UNDER={under_count} bets ({under_amount:.2f}), OVER={over_count} bets ({over_amount:.2f})")
logger.info(f"💰 [EXTRACTION DEBUG] Total other bets: {len(all_bets)} bets ({total_bet_amount:.2f})")
# Calculate expected redistribution based on CAP (total intake × CAP%)
expected_redistribution = total_payin_all_bets * (cap_percentage / 100.0)
# Step 6: Filter payouts below adjusted CAP threshold
logger.info(f"🎯 [EXTRACTION DEBUG] Step 6: Filtering payouts below adjusted CAP threshold")
eligible_payouts = {k: v for k, v in payouts.items() if v <= cap_threshold}
......@@ -2444,6 +2554,11 @@ class GamesThread(ThreadedComponent):
extraction_winning_outcome_names = [outcome.outcome_name for outcome in winning_outcomes]
logger.info(f"🏆 [EXTRACTION DEBUG] Winning outcomes for result '{selected_result}': {extraction_winning_outcome_names}")
# Log expected redistribution and actual selected payout
logger.info(f"💰 [EXTRACTION DEBUG] Expected redistribution: {expected_redistribution:.2f}, Actual selected payout: {payouts.get(selected_result, 0):.2f}")
logger.info(f"💰 [EXTRACTION DEBUG] CAP percentage: {cap_percentage}%, base threshold: {base_cap_threshold:.2f}, adjusted threshold: {cap_threshold:.2f}")
logger.info(f"💰 [EXTRACTION DEBUG] Selected result: {selected_result}, winning outcomes: {extraction_winning_outcome_names}")
# Step 8: Update bet results
logger.info(f"💾 [EXTRACTION DEBUG] Step 8: Updating bet results for match {match_id}")
self._update_bet_results(match_id, selected_result, under_over_result, extraction_winning_outcome_names, session)
......@@ -2454,8 +2569,8 @@ class GamesThread(ThreadedComponent):
# Step 10: Update daily shortfall tracking
logger.info(f"💰 [EXTRACTION DEBUG] Step 10: Updating daily shortfall tracking")
today = datetime.now().date()
self._update_daily_shortfall(today, total_bet_amount, payouts[selected_result], cap_percentage, session)
today = self._get_today_utc_date()
self._update_daily_shortfall(today, total_payin_all_bets, payouts[selected_result], cap_percentage, session)
logger.info(f"✅ [EXTRACTION DEBUG] Result extraction completed successfully: selected {selected_result}")
......@@ -2539,10 +2654,9 @@ class GamesThread(ThreadedComponent):
def _update_bet_results(self, match_id: int, selected_result: str, under_over_result: Optional[str], extraction_winning_outcome_names: List[str], session):
"""Update bet results for UNDER/OVER and selected result with win amount calculation"""
try:
logger.info(f"DEBUG _update_bet_results: Starting for match {match_id}, selected_result='{selected_result}'")
logger.info(f"DEBUG _update_bet_results: Starting for match {match_id}, selected_result='{selected_result}', extraction_winning_outcome_names={extraction_winning_outcome_names}")
# Initialize variables to avoid UnboundLocalError
extraction_winning_outcome_names = []
# Initialize match variable to avoid UnboundLocalError
match = None
# Get coefficient for the selected result
......@@ -3221,8 +3335,8 @@ class GamesThread(ThreadedComponent):
# Check if there are any active fixtures (matches in non-terminal states)
session = self.db_manager.get_session()
try:
# Get today's date
today = datetime.now().date()
# Get today's date in UTC (consistent with database)
today = self._get_today_utc_date()
# Check for active matches today
active_matches = session.query(MatchModel).filter(
......
......@@ -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