Add comprehensive user workflow documentation and statistics page

- Create USER_WORKFLOW_DOCUMENTATION.md with extensive coverage of:
  * Complete cashier workflow from login to end-of-day procedures
  * Game flow and match progression with state machine details
  * Extraction algorithm and redistribution cap system
  * Major functionality and configuration options
- Add statistics.html template for admin statistics dashboard
- Update core components for improved functionality
parent 71d9820e
# MbetterClient User Workflow Documentation
## Table of Contents
1. [Cashier Workflow](#cashier-workflow)
2. [Game Flow and Match Progression](#game-flow-and-match-progression)
3. [Extraction Algorithm and Redistribution](#extraction-algorithm-and-redistribution)
4. [Major Functionality and Configurations](#major-functionality-and-configurations)
---
## Cashier Workflow
### Overview
The cashier role in MbetterClient is designed for streamlined betting operations with a focused interface optimized for speed and efficiency. Cashiers handle bet creation, verification, and management while having restricted access to administrative functions.
### Login Process
1. **Access the Application**: Navigate to `http://localhost:5000` (default port)
2. **Enter Credentials**: Use cashier-specific username and password
3. **Automatic Redirect**: System automatically redirects cashiers to the cashier dashboard
4. **Interface Focus**: Clean, distraction-free interface with essential betting controls
### Daily Operations Workflow
#### 1. Morning Setup (Pre-Match Preparation)
```
Login → Verify System Status → Check Available Matches → Prepare Betting Interface
```
- **System Status Check**: Verify all components are running (video player, API client, database)
- **Match Availability**: Confirm matches are loaded and in 'bet' status
- **Interface Preparation**: Ensure betting forms are ready for customer interactions
#### 2. Customer Bet Creation Process
```
Customer Request → Select Match → Choose Outcomes → Enter Amounts → Confirm Bet → Print Receipt
```
**Detailed Steps:**
1. **Customer Interaction**
- Greet customer and understand betting requirements
- Verify customer identity if required
2. **Match Selection**
- Access "New Bet" page from cashier dashboard
- System automatically loads available matches in 'bet' status
- Display matches with fighter information and venue details
3. **Outcome Selection**
- View dynamic betting outcomes from database (not hardcoded WIN1/WIN2/X)
- Outcomes are generated from `match_outcomes` table data
- Examples: "KO Round 1", "Points Win", "Draw", etc.
- Select multiple outcomes per bet with individual amounts
4. **Amount Entry**
- Enter betting amounts for each selected outcome
- Real-time total calculation
- Validate amounts (positive numbers, reasonable limits)
5. **Bet Confirmation**
- Review complete bet summary
- Generate unique UUID for bet tracking
- Submit bet to database
- Display success confirmation with bet ID
6. **Receipt Generation**
- Print bet receipt (infrastructure ready, implementation pending)
- Include bet ID, selected outcomes, amounts, and total
- QR code for mobile verification
#### 3. Bet Management During Matches
```
Monitor Active Bets → Verify Bet Status → Handle Payouts → Update Records
```
- **Bet Monitoring**: View all bets for current day with real-time status
- **Status Tracking**: Monitor bet results (pending → won/lost/cancelled)
- **Payout Processing**: Mark winning bets as paid
- **Customer Service**: Handle bet verification requests
#### 4. End-of-Day Procedures
```
Close Betting → Finalize Results → Generate Reports → System Shutdown
```
- **Betting Closure**: Ensure no new bets accepted after match start
- **Result Finalization**: Confirm all bet outcomes are processed
- **Daily Reports**: Review betting statistics and totals
- **System Maintenance**: Clean logout and system preparation for next day
### Key Interface Elements
#### Cashier Dashboard Navigation
- **Left Side**: MbetterClient branding only (minimalist design)
- **Right Side**: Live server time clock + user dropdown menu
- **Hidden Elements**: All admin functions (configuration, templates, etc.)
- **Focused Menu**: Bets, Bet Verification, Start Games controls
#### Betting Interface Features
- **Dynamic Outcomes**: Database-driven betting options
- **Real-time Totals**: Automatic calculation of bet amounts
- **UUID Tracking**: Secure, unique bet identification
- **Print Integration**: Receipt printing infrastructure
- **Mobile Verification**: QR code access for bet checking
### Error Handling and Recovery
#### Common Issues
- **Match Not Available**: Verify match status is 'bet' in database
- **Database Connection**: Check system status indicators
- **Invalid Amounts**: Client-side validation prevents submission
- **Network Issues**: Automatic retry mechanisms for API calls
#### Recovery Procedures
- **Bet Creation Failure**: Retry submission, check database connectivity
- **Print Failure**: Manual receipt creation, QR code verification
- **System Freeze**: Force logout and re-login, contact administrator
---
## Game Flow and Match Progression
### Overview
MbetterClient manages the complete lifecycle of boxing matches from fixture loading to result extraction. The system uses a sophisticated state machine with automatic progression and manual controls.
### Match States and Transitions
#### 1. Initial State: Scheduled
```
Status: 'scheduled'
Description: Match is loaded from fixture but not yet available for betting
Actions: None (waiting for manual or automatic activation)
```
#### 2. Betting State: Bet
```
Status: 'bet'
Description: Match is active for betting, outcomes available to customers
Entry: Manual activation or START_GAME message
Exit: Match start time reached or manual progression
```
#### 3. Active State: Ingame
```
Status: 'ingame'
Description: Match is currently in progress
Entry: MATCH_START message from timer or manual trigger
Exit: Match completion and result extraction
```
#### 4. Completed States
```
Status: 'completed' - Match finished with results
Status: 'cancelled' - Match cancelled, no results
```
### Automatic Game Flow
#### Timer-Based Progression
```
Fixture Load → START_GAME Message → Match Activation → Timer Countdown → MATCH_START → Result Extraction
```
1. **Fixture Synchronization**
- API client fetches fixture data from server
- Matches loaded into database with timestamps
- Automatic status assignment based on current time
2. **START_GAME Trigger**
- Command-line timer: `--start-timer X` (minutes)
- Manual trigger: "Start Games" button in cashier interface
- Message bus broadcast to all components
3. **Match Activation Logic**
```python
# Priority-based match selection
def find_next_match():
# 1. First priority: Matches with 'bet' status
bet_matches = session.query(MatchModel).filter_by(status='bet').all()
if bet_matches:
return bet_matches[0] # First available bet match
# 2. Second priority: Matches with 'scheduled' status
scheduled_matches = session.query(MatchModel).filter_by(status='scheduled').all()
if scheduled_matches:
return scheduled_matches[0]
# 3. Third priority: Matches with 'pending' status
pending_matches = session.query(MatchModel).filter_by(status='pending').all()
if pending_matches:
return pending_matches[0]
return None # No suitable matches found
```
4. **Timer Countdown**
- Configurable interval (default: 20 minutes)
- Visual countdown in status bar and navbar
- Color-coded urgency (yellow orange red)
- Automatic MATCH_START message when timer reaches zero
5. **Match Start Sequence**
- Status change: 'bet'/'scheduled' 'ingame'
- Video player overlay updates
- Betting interface locks for the match
- Timer resets for next match
### Manual Game Control
#### Cashier Controls
- **Start Games Button**: Immediate START_GAME trigger
- **Match Timer Override**: Manual MATCH_START for specific matches
- **Emergency Stop**: Halt current match progression
#### Admin Controls
- **Fixture Management**: Load, reset, or modify fixture data
- **Match Status Override**: Force status changes for any match
- **Timer Configuration**: Adjust countdown intervals
- **Global Betting Mode**: Switch between betting strategies
### Global Betting Mode Configuration
#### Mode 1: All Bets on Start (Default)
```
START_GAME → ALL matches in fixture become 'bet' status simultaneously
Benefits: Maximum betting opportunity, all matches available at once
Use Case: Large fixtures with multiple concurrent betting periods
```
#### Mode 2: One Bet at a Time
```
START_GAME → ONLY first match becomes 'bet', others remain 'scheduled'
Benefits: Controlled betting flow, focused customer attention
Use Case: Sequential match progression, limited betting windows
```
#### Configuration
```json
{
"config_key": "betting_mode",
"config_value": "all_bets_on_start",
"value_type": "string",
"description": "Global betting mode: all_bets_on_start or one_bet_at_a_time"
}
```
### Result Extraction Process
#### Automatic Extraction
1. **Match Completion Detection**
- Timer expiration or manual completion signal
- Status change: 'ingame' → 'completed'
2. **Outcome Processing**
- Query match_outcomes table for final results
- Map outcomes to extraction associations
- Calculate redistribution amounts
3. **Bet Resolution**
- Update all related bets with win/loss/cancelled status
- Calculate payout amounts for winning bets
- Update betting statistics
#### Manual Result Entry
- Admin interface for result override
- Direct outcome value modification
- Emergency result correction
---
## Extraction Algorithm and Redistribution
### Overview
The extraction system implements a sophisticated algorithm for determining match winners and redistributing betting pools. It uses configurable outcome associations and percentage-based caps to ensure fair and profitable operations.
### Core Algorithm Components
#### 1. Outcome Association System
```
Database-driven mapping between match outcomes and betting results
```
**Association Table Structure:**
```sql
CREATE TABLE extraction_associations (
outcome_name VARCHAR(255) NOT NULL, -- Match outcome (e.g., "WIN1", "KO1")
extraction_result VARCHAR(50) NOT NULL, -- Betting result (WIN1, X, WIN2)
is_default BOOLEAN DEFAULT FALSE, -- Default association flag
UNIQUE(outcome_name, extraction_result) -- Prevent duplicates
);
```
**Default Associations:**
- **WIN1**: WIN1, K01, RET1, PTS1 (Fighter 1 victory outcomes)
- **X (Draw)**: DRAW, 12, X1, X2, DKO (Draw/tie outcomes)
- **WIN2**: WIN2, K02, RET2, PTS2 (Fighter 2 victory outcomes)
#### 2. Redistribution Cap System
```
Maximum percentage of collected bets that can be redistributed as winnings
```
**Configuration:**
```json
{
"config_key": "extraction_redistribution_cap",
"config_value": "70",
"value_type": "float",
"description": "Maximum redistribution percentage (10-100%)"
}
```
**Cap Logic:**
- **Default**: 70% of total collected amount
- **Range**: 10% to 100% (configurable)
- **Purpose**: Ensure house profit margin
- **Calculation**: `redistribution_amount = MIN(requested_amount, cap_percentage × total_collected)`
### Extraction Algorithm Flow
#### Step 1: Match Completion Detection
```python
def detect_match_completion(match_id):
"""Check if match has completed and results are available"""
match = session.query(MatchModel).filter_by(id=match_id).first()
if match.status == 'completed':
return True
return False
```
#### Step 2: Outcome Retrieval
```python
def get_match_outcomes(match_id):
"""Retrieve final outcomes from match_outcomes table"""
outcomes = session.query(MatchOutcomeModel).filter_by(match_id=match_id).all()
return {outcome.column_name: outcome.float_value for outcome in outcomes}
```
#### Step 3: Association Mapping
```python
def map_outcomes_to_results(outcomes, associations):
"""Map match outcomes to betting results using associations"""
results = {'WIN1': 0, 'X': 0, 'WIN2': 0}
for outcome_name, outcome_value in outcomes.items():
# Find associations for this outcome
associated_results = associations.get(outcome_name, [])
# Distribute outcome value among associated results
if associated_results:
value_per_result = outcome_value / len(associated_results)
for result in associated_results:
results[result] += value_per_result
return results
```
#### Step 4: Bet Resolution
```python
def resolve_bets(match_id, extraction_result):
"""Update all bets for the match based on extraction result"""
bet_details = session.query(BetDetailModel).filter_by(match_id=match_id).all()
for bet_detail in bet_details:
if bet_detail.outcome == extraction_result:
bet_detail.result = 'won'
# Calculate winnings based on redistribution cap
elif bet_detail.result == 'pending':
bet_detail.result = 'lost'
session.commit()
```
#### Step 5: Redistribution Calculation
```python
def calculate_redistribution(bets_won, total_collected, cap_percentage=70):
"""Calculate actual redistribution respecting the cap"""
requested_total = sum(bet.amount * 2 for bet in bets_won) # 2x payout
cap_amount = (total_collected * cap_percentage) / 100
if requested_total <= cap_amount:
# All winnings can be paid
return requested_total
else:
# Apply cap - distribute proportionally
scale_factor = cap_amount / requested_total
return cap_amount, scale_factor
```
### Advanced Redistribution Scenarios
#### Scenario 1: Normal Redistribution (Under Cap)
```
Total Collected: $1000
Winning Bets: 2 bets × $100 each = $200 requested
Cap (70%): $700 maximum
Result: Pay full $200 (under cap)
```
#### Scenario 2: Capped Redistribution
```
Total Collected: $1000
Winning Bets: 10 bets × $100 each = $1000 requested
Cap (70%): $700 maximum
Result: Pay $700 total, scaled proportionally
```
#### Scenario 3: Multiple Outcome Winners
```
Match Result: DRAW (X)
Total Collected: $2000
WIN1 Bets: 5 × $50 = $250
X Bets: 3 × $100 = $300 (winners)
WIN2 Bets: 7 × $75 = $525
Cap (70%): $1400 maximum
Result: Pay $300 to X bet winners (under cap)
```
### Extraction Statistics Tracking
#### Statistics Table Structure
```sql
CREATE TABLE extraction_stats (
match_id INTEGER PRIMARY KEY,
fixture_id VARCHAR(255),
match_datetime DATETIME,
total_bets INTEGER,
total_amount_collected DECIMAL(10,2),
total_redistributed DECIMAL(10,2),
actual_result VARCHAR(100),
extraction_result VARCHAR(50),
cap_applied BOOLEAN,
cap_percentage DECIMAL(5,2),
under_bets INTEGER,
under_amount DECIMAL(10,2),
over_bets INTEGER,
over_amount DECIMAL(10,2)
);
```
#### Statistical Analysis
- **Profit Margin**: `total_collected - total_redistributed`
- **Cap Application Rate**: Percentage of matches hitting redistribution cap
- **Outcome Distribution**: Frequency analysis of different extraction results
- **Betting Patterns**: UNDER vs OVER bet distribution and success rates
### Configuration and Tuning
#### Association Management
- **Drag-and-Drop Interface**: Visual outcome association management
- **Multi-Association Support**: Outcomes can map to multiple results
- **Real-time Updates**: Immediate database persistence
- **Default Templates**: Pre-configured associations for common scenarios
#### Cap Management
- **Percentage Range**: 10% to 100% configurable
- **Per-Match Override**: Individual match cap adjustments
- **Dynamic Adjustment**: Cap changes based on fixture performance
- **Reporting**: Cap impact analysis and recommendations
---
## Major Functionality and Configurations
### Core System Components
#### 1. Web Dashboard
**Primary Interface**: Flask-based web application with Bootstrap UI
**Authentication**: JWT token-based security with role-based access
**Real-time Updates**: WebSocket-like polling for live status updates
**Responsive Design**: Mobile-friendly interface with offline capabilities
#### 2. Video Player (Qt-based)
**Rendering Engine**: PyQt5/QML with hardware acceleration
**Overlay System**: Dual rendering (WebEngine/Native) with template support
**Format Support**: MP4, AVI, MOV, MKV, WebM playback
**Controls**: Full keyboard shortcuts and remote control support
#### 3. API Client
**External Integration**: RESTful API communication with remote servers
**Authentication**: Bearer token and API key management
**Retry Logic**: Exponential backoff with configurable limits
**Response Processing**: Custom handlers for different data formats
#### 4. Database System
**Engine**: SQLite with SQLAlchemy ORM
**Migrations**: Versioned schema updates with rollback support
**Models**: Comprehensive data models for matches, bets, users, and configuration
**Performance**: Optimized queries with proper indexing
#### 5. Message Bus
**Inter-component Communication**: Publisher-subscriber pattern
**Thread Safety**: Asynchronous message passing between components
**Extensibility**: Plugin architecture support
**Monitoring**: Message logging and debugging capabilities
### Configuration Categories
#### System Configuration
```json
{
"web": {
"host": "localhost",
"port": 5000,
"secret_key": "auto-generated",
"jwt_expiration_hours": 24
},
"qt": {
"fullscreen": true,
"default_template": "news_template",
"overlay_type": "webengine"
},
"logging": {
"level": "INFO",
"max_file_size": "10MB",
"backup_count": 5
}
}
```
#### Betting Configuration
```json
{
"betting_mode": "all_bets_on_start",
"match_interval": 20,
"extraction_redistribution_cap": 70,
"currency_symbol": "USh",
"currency_position": "before"
}
```
#### Video and Display Configuration
```json
{
"video": {
"width": 1920,
"height": 1080,
"fullscreen": false,
"supported_formats": ["mp4", "avi", "mov", "mkv"]
},
"screen_cast": {
"enabled": true,
"resolution": "1280x720",
"framerate": 15,
"auto_start_capture": false
}
}
```
#### API and Integration Configuration
```json
{
"api": {
"fastapi_url": "https://mbetter.nexlab.net/",
"api_token": "configured-token",
"api_interval": 600,
"api_timeout": 30,
"api_enabled": true
},
"barcode": {
"enabled": false,
"standard": "code128",
"width": 200,
"height": 100
},
"qrcode": {
"enabled": false,
"size": 200,
"error_correction": "M"
}
}
```
### User Roles and Permissions
#### 1. Administrator Role
**Full System Access**:
- User management (create, edit, delete users)
- System configuration and settings
- Fixture management and data import
- Extraction configuration and associations
- System shutdown and maintenance
- Log viewing and debugging
- Statistics and reporting access
#### 2. Cashier Role
**Limited Betting Operations**:
- Bet creation and management
- Customer receipt printing
- Bet verification and payout marking
- Match starting controls
- Personal password changes
- Restricted to betting-related functions
#### 3. API Access Levels
**Token-based Permissions**:
- Read-only access for monitoring
- Betting operations for integrated systems
- Administrative access for system management
- Configurable expiration and scope limitations
### Advanced Features
#### Screen Casting System
**Chromecast Integration**:
- Automatic device discovery on local network
- Real-time screen capture with FFmpeg
- Configurable quality settings (resolution, bitrate, framerate)
- Cross-platform audio/video capture
- HTTP streaming server for device compatibility
#### Offline Capabilities
**CDN Fallback System**:
- Local Bootstrap and FontAwesome libraries
- Automatic CDN-to-local switching
- Complete offline functionality
- PyInstaller-compatible resource bundling
#### Real-time Status Monitoring
**System Health Dashboard**:
- Component status indicators
- Match timer with visual countdown
- Video player status and controls
- API connectivity monitoring
- Database connection health
#### Automated Match Timer
**Intelligent Progression**:
- Configurable countdown intervals
- Priority-based match selection
- Automatic status transitions
- Visual feedback with color coding
- Manual override capabilities
### Performance and Scalability
#### Database Optimization
- **Indexing Strategy**: Optimized indexes on frequently queried columns
- **Connection Pooling**: Efficient database connection management
- **Query Optimization**: Reduced N+1 queries with proper JOIN operations
- **Migration System**: Zero-downtime schema updates
#### Memory Management
- **Component Isolation**: Separate processes for critical functions
- **Resource Monitoring**: Built-in memory and CPU usage tracking
- **Cleanup Procedures**: Automatic resource cleanup on shutdown
- **Caching Strategy**: Intelligent caching for frequently accessed data
#### Network Efficiency
- **API Optimization**: Batch operations and reduced request frequency
- **Compression**: Response compression for large data transfers
- **Connection Reuse**: Persistent connections where possible
- **Timeout Management**: Configurable timeouts with retry logic
### Security Features
#### Authentication and Authorization
- **JWT Tokens**: Secure token-based authentication
- **Role-based Access**: Granular permission system
- **Session Management**: Automatic session timeout and cleanup
- **Password Security**: Proper hashing and validation
#### Data Protection
- **Input Validation**: Comprehensive input sanitization
- **SQL Injection Prevention**: Parameterized queries
- **XSS Protection**: Template escaping and validation
- **CSRF Protection**: Token-based request validation
#### System Security
- **File Access Control**: Restricted file system access
- **Network Security**: Configurable firewall rules
- **Audit Logging**: Comprehensive security event logging
- **Update Mechanism**: Secure update distribution
### Monitoring and Maintenance
#### System Monitoring
- **Health Checks**: Automated component health verification
- **Performance Metrics**: CPU, memory, and network usage tracking
- **Error Reporting**: Comprehensive error logging and alerting
- **Status Dashboard**: Real-time system status visualization
#### Maintenance Procedures
- **Automated Backups**: Database and configuration backups
- **Log Rotation**: Automatic log file management
- **Update Process**: Safe application updates with rollback
- **Cleanup Tasks**: Automated temporary file and cache cleanup
This comprehensive documentation covers the complete workflow, technical architecture, and operational procedures for MbetterClient. The system is designed for reliability, performance, and ease of use in professional betting environments.
\ No newline at end of file
...@@ -1395,6 +1395,9 @@ class GamesThread(ThreadedComponent): ...@@ -1395,6 +1395,9 @@ class GamesThread(ThreadedComponent):
# Update bet results for UNDER/OVER and the selected result # Update bet results for UNDER/OVER and the selected result
self._update_bet_results(match_id, selected_result, session) self._update_bet_results(match_id, selected_result, session)
# Collect statistics for this match completion
self._collect_match_statistics(match_id, fixture_id, selected_result, session)
logger.info(f"Result extraction completed: selected {selected_result}") logger.info(f"Result extraction completed: selected {selected_result}")
return selected_result return selected_result
...@@ -1530,6 +1533,119 @@ class GamesThread(ThreadedComponent): ...@@ -1530,6 +1533,119 @@ class GamesThread(ThreadedComponent):
logger.error(f"Failed to update bet results: {e}") logger.error(f"Failed to update bet results: {e}")
session.rollback() session.rollback()
def _collect_match_statistics(self, match_id: int, fixture_id: str, selected_result: str, session):
"""Collect and store statistics for match completion"""
try:
from ..database.models import ExtractionStatsModel, BetDetailModel, MatchModel
import json
# Get match information
match = session.query(MatchModel).filter_by(id=match_id).first()
if not match:
logger.warning(f"Match {match_id} not found for statistics collection")
return
# Calculate statistics
total_bets = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.active_status == True
).count()
total_amount_collected = session.query(
BetDetailModel.amount
).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.active_status == True
).all()
total_amount_collected = sum(bet.amount for bet in total_amount_collected) if total_amount_collected else 0.0
# Calculate redistribution amount (sum of all win_amounts)
total_redistributed = session.query(
BetDetailModel.win_amount
).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.result == 'win',
BetDetailModel.active_status == True
).all()
total_redistributed = sum(bet.win_amount for bet in total_redistributed) if total_redistributed else 0.0
# Get UNDER/OVER specific statistics
under_bets = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'UNDER',
BetDetailModel.active_status == True
).count()
under_amount = session.query(
BetDetailModel.amount
).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'UNDER',
BetDetailModel.active_status == True
).all()
under_amount = sum(bet.amount for bet in under_amount) if under_amount else 0.0
over_bets = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'OVER',
BetDetailModel.active_status == True
).count()
over_amount = session.query(
BetDetailModel.amount
).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'OVER',
BetDetailModel.active_status == True
).all()
over_amount = sum(bet.amount for bet in over_amount) if over_amount else 0.0
# Check if CAP was applied
cap_percentage = self._get_redistribution_cap()
cap_applied = False
cap_threshold = total_amount_collected * (cap_percentage / 100.0)
# Get extraction result (the actual result selected)
extraction_result = selected_result if selected_result not in ['UNDER', 'OVER'] else None
# Create result breakdown (simplified for now)
result_breakdown = {
'selected_result': selected_result,
'extraction_result': extraction_result,
'under_over_result': selected_result if selected_result in ['UNDER', 'OVER'] else None,
'total_payin': total_amount_collected,
'total_payout': total_redistributed,
'profit': total_amount_collected - total_redistributed
}
# Create or update extraction stats record
stats_record = ExtractionStatsModel(
match_id=match_id,
fixture_id=fixture_id,
match_datetime=match.start_time or datetime.utcnow(),
total_bets=total_bets,
total_amount_collected=total_amount_collected,
total_redistributed=total_redistributed,
actual_result=selected_result,
result_breakdown=json.dumps(result_breakdown),
under_bets=under_bets,
under_amount=under_amount,
over_bets=over_bets,
over_amount=over_amount,
extraction_result=extraction_result,
cap_applied=cap_applied,
cap_percentage=cap_percentage if cap_applied else None
)
session.add(stats_record)
session.commit()
logger.info(f"Collected statistics for match {match_id}: {total_bets} bets, collected={total_amount_collected:.2f}, redistributed={total_redistributed:.2f}")
except Exception as e:
logger.error(f"Failed to collect match statistics: {e}")
session.rollback()
def _fallback_result_selection(self) -> str: def _fallback_result_selection(self) -> str:
"""Fallback result selection when extraction fails""" """Fallback result selection when extraction fails"""
try: try:
......
...@@ -1928,6 +1928,79 @@ class Migration_025_AddResultOptionModel(DatabaseMigration): ...@@ -1928,6 +1928,79 @@ class Migration_025_AddResultOptionModel(DatabaseMigration):
logger.info("ResultOptionModel migration rollback - no database changes") logger.info("ResultOptionModel migration rollback - no database changes")
return True return True
class Migration_026_AddExtractionStatsTable(DatabaseMigration):
"""Add extraction_stats table for collecting match betting statistics"""
def __init__(self):
super().__init__("026", "Add extraction_stats table for collecting match betting statistics")
def up(self, db_manager) -> bool:
"""Create extraction_stats table"""
try:
with db_manager.engine.connect() as conn:
# Create extraction_stats table
conn.execute(text("""
CREATE TABLE IF NOT EXISTS extraction_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
match_id INTEGER NOT NULL REFERENCES matches(id),
fixture_id VARCHAR(255) NOT NULL,
match_datetime DATETIME NOT NULL,
total_bets INTEGER DEFAULT 0 NOT NULL,
total_amount_collected REAL DEFAULT 0.0 NOT NULL,
total_redistributed REAL DEFAULT 0.0 NOT NULL,
actual_result VARCHAR(50) NOT NULL,
result_breakdown TEXT NOT NULL,
under_bets INTEGER DEFAULT 0 NOT NULL,
under_amount REAL DEFAULT 0.0 NOT NULL,
over_bets INTEGER DEFAULT 0 NOT NULL,
over_amount REAL DEFAULT 0.0 NOT NULL,
extraction_result VARCHAR(50),
cap_applied BOOLEAN DEFAULT FALSE NOT NULL,
cap_percentage REAL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
)
"""))
# Create indexes for extraction_stats table
indexes = [
"CREATE INDEX IF NOT EXISTS ix_extraction_stats_match_id ON extraction_stats(match_id)",
"CREATE INDEX IF NOT EXISTS ix_extraction_stats_fixture_id ON extraction_stats(fixture_id)",
"CREATE INDEX IF NOT EXISTS ix_extraction_stats_match_datetime ON extraction_stats(match_datetime)",
"CREATE INDEX IF NOT EXISTS ix_extraction_stats_actual_result ON extraction_stats(actual_result)",
"CREATE INDEX IF NOT EXISTS ix_extraction_stats_composite ON extraction_stats(fixture_id, match_id)",
]
for index_sql in indexes:
conn.execute(text(index_sql))
conn.commit()
logger.info("Extraction stats table created successfully")
return True
except Exception as e:
logger.error(f"Failed to create extraction_stats table: {e}")
return False
def down(self, db_manager) -> bool:
"""Drop extraction_stats table"""
try:
with db_manager.engine.connect() as conn:
conn.execute(text("DROP TABLE IF EXISTS extraction_stats"))
conn.commit()
logger.info("Extraction stats table dropped")
return True
except Exception as e:
logger.error(f"Failed to drop extraction_stats table: {e}")
return False
# Registry of all migrations in order # Registry of all migrations in order
MIGRATIONS: List[DatabaseMigration] = [ MIGRATIONS: List[DatabaseMigration] = [
Migration_001_InitialSchema(), Migration_001_InitialSchema(),
...@@ -1955,6 +2028,7 @@ MIGRATIONS: List[DatabaseMigration] = [ ...@@ -1955,6 +2028,7 @@ MIGRATIONS: List[DatabaseMigration] = [
Migration_023_AddAvailableBetsTable(), Migration_023_AddAvailableBetsTable(),
Migration_024_AddResultOptionsTable(), Migration_024_AddResultOptionsTable(),
Migration_025_AddResultOptionModel(), Migration_025_AddResultOptionModel(),
Migration_026_AddExtractionStatsTable(),
] ]
......
...@@ -837,3 +837,101 @@ class ResultOptionModel(BaseModel): ...@@ -837,3 +837,101 @@ class ResultOptionModel(BaseModel):
def __repr__(self): def __repr__(self):
return f'<ResultOption {self.result_name}: {self.description}>' return f'<ResultOption {self.result_name}: {self.description}>'
class ExtractionStatsModel(BaseModel):
"""Statistics for extraction results and betting patterns"""
__tablename__ = 'extraction_stats'
__table_args__ = (
Index('ix_extraction_stats_match_id', 'match_id'),
Index('ix_extraction_stats_fixture_id', 'fixture_id'),
Index('ix_extraction_stats_match_datetime', 'match_datetime'),
Index('ix_extraction_stats_actual_result', 'actual_result'),
Index('ix_extraction_stats_composite', 'fixture_id', 'match_id'),
)
# Match identification
match_id = Column(Integer, ForeignKey('matches.id'), nullable=False, comment='Foreign key to matches table')
fixture_id = Column(String(255), nullable=False, comment='Fixture identifier')
match_datetime = Column(DateTime, nullable=False, comment='When the match was completed')
# Overall betting statistics
total_bets = Column(Integer, default=0, nullable=False, comment='Total number of bets placed on this match')
total_amount_collected = Column(Float(precision=2), default=0.0, nullable=False, comment='Total amount collected from all bets')
total_redistributed = Column(Float(precision=2), default=0.0, nullable=False, comment='Total amount redistributed to winners')
# Result statistics (JSON structure for flexible result tracking)
actual_result = Column(String(50), nullable=False, comment='The actual result of the match (WIN1, DRAW, WIN2, etc.)')
result_breakdown = Column(JSON, nullable=False, comment='Detailed breakdown of bets and amounts by result option')
# UNDER/OVER specific statistics
under_bets = Column(Integer, default=0, nullable=False, comment='Number of UNDER bets')
under_amount = Column(Float(precision=2), default=0.0, nullable=False, comment='Total amount bet on UNDER')
over_bets = Column(Integer, default=0, nullable=False, comment='Number of OVER bets')
over_amount = Column(Float(precision=2), default=0.0, nullable=False, comment='Total amount bet on OVER')
# Extraction system statistics
extraction_result = Column(String(50), comment='Result from extraction system (if different from actual)')
cap_applied = Column(Boolean, default=False, nullable=False, comment='Whether redistribution CAP was applied')
cap_percentage = Column(Float(precision=2), comment='CAP percentage used (if applied)')
# Relationships
match = relationship('MatchModel')
def set_result_breakdown(self, breakdown: Dict[str, Dict[str, Any]]):
"""Set result breakdown as JSON
Expected format:
{
"WIN1": {"bets": 10, "amount": 500.00, "coefficient": 2.0},
"DRAW": {"bets": 5, "amount": 250.00, "coefficient": 3.0},
...
}
"""
self.result_breakdown = breakdown
def get_result_breakdown(self) -> Dict[str, Dict[str, Any]]:
"""Get result breakdown from JSON"""
if isinstance(self.result_breakdown, dict):
return self.result_breakdown
elif isinstance(self.result_breakdown, str):
try:
return json.loads(self.result_breakdown)
except json.JSONDecodeError:
return {}
else:
return {}
def add_result_stat(self, result_name: str, bet_count: int, amount: float, coefficient: float = None):
"""Add or update statistics for a specific result"""
breakdown = self.get_result_breakdown()
if result_name not in breakdown:
breakdown[result_name] = {"bets": 0, "amount": 0.0}
breakdown[result_name]["bets"] += bet_count
breakdown[result_name]["amount"] += amount
if coefficient is not None:
breakdown[result_name]["coefficient"] = coefficient
self.set_result_breakdown(breakdown)
def get_profit_loss(self) -> float:
"""Calculate profit/loss for this match (collected - redistributed)"""
return self.total_amount_collected - self.total_redistributed
def get_payout_ratio(self) -> float:
"""Calculate payout ratio (redistributed / collected)"""
if self.total_amount_collected > 0:
return self.total_redistributed / self.total_amount_collected
return 0.0
def to_dict(self, exclude_fields: Optional[List[str]] = None) -> Dict[str, Any]:
"""Convert to dictionary with calculated fields"""
result = super().to_dict(exclude_fields)
result['result_breakdown'] = self.get_result_breakdown()
result['profit_loss'] = self.get_profit_loss()
result['payout_ratio'] = self.get_payout_ratio()
return result
def __repr__(self):
return f'<ExtractionStats Match {self.match_id}: {self.total_bets} bets, {self.total_amount_collected:.2f} collected, {self.actual_result}>'
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="fas fa-chart-bar me-2"></i>Extraction Statistics
<small class="text-muted">Match betting analytics and performance metrics</small>
</h1>
</div>
</div>
<!-- Statistics Overview Cards -->
<div class="row mb-4">
<div class="col-md-3 mb-3">
<div class="card">
<div class="card-body text-center">
<i class="fas fa-trophy text-warning fa-2x mb-2"></i>
<h4 class="card-title" id="total-matches">0</h4>
<p class="card-text text-muted">Total Matches</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card">
<div class="card-body text-center">
<i class="fas fa-coins text-success fa-2x mb-2"></i>
<h4 class="card-title" id="total-collected">USh 0</h4>
<p class="card-text text-muted">Total Collected</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card">
<div class="card-body text-center">
<i class="fas fa-hand-holding-usd text-primary fa-2x mb-2"></i>
<h4 class="card-title" id="total-redistributed">USh 0</h4>
<p class="card-text text-muted">Total Redistributed</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card">
<div class="card-body text-center">
<i class="fas fa-chart-line text-info fa-2x mb-2"></i>
<h4 class="card-title" id="net-profit">USh 0</h4>
<p class="card-text text-muted">Net Profit</p>
</div>
</div>
</div>
</div>
<!-- Filters and Controls -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-filter me-2"></i>Filters
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<label class="form-label">Date Range</label>
<select class="form-select" id="date-range">
<option value="today">Today</option>
<option value="week">This Week</option>
<option value="month" selected>This Month</option>
<option value="all">All Time</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Fixture ID</label>
<input type="text" class="form-control" id="fixture-filter" placeholder="Filter by fixture ID">
</div>
<div class="col-md-3">
<label class="form-label">Result Type</label>
<select class="form-select" id="result-filter">
<option value="">All Results</option>
<option value="UNDER">UNDER</option>
<option value="OVER">OVER</option>
<option value="WIN1">WIN1</option>
<option value="WIN2">WIN2</option>
<option value="KO1">KO1</option>
<option value="KO2">KO2</option>
<option value="DRAW">DRAW</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">&nbsp;</label>
<div class="d-grid">
<button class="btn btn-primary" id="apply-filters">
<i class="fas fa-search me-1"></i>Apply Filters
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Statistics Table -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-table me-2"></i>Match Statistics
<span class="badge bg-primary ms-2" id="stats-count">0</span>
</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="statistics-table">
<thead class="table-dark">
<tr>
<th><i class="fas fa-calendar me-1"></i>Date/Time</th>
<th><i class="fas fa-hashtag me-1"></i>Match #</th>
<th><i class="fas fa-layer-group me-1"></i>Fixture</th>
<th><i class="fas fa-trophy me-1"></i>Result</th>
<th><i class="fas fa-coins me-1"></i>Collected</th>
<th><i class="fas fa-hand-holding-usd me-1"></i>Redistributed</th>
<th><i class="fas fa-chart-line me-1"></i>Profit</th>
<th><i class="fas fa-percentage me-1"></i>Profit %</th>
<th><i class="fas fa-info-circle me-1"></i>Details</th>
</tr>
</thead>
<tbody id="statistics-tbody">
<tr>
<td colspan="9" class="text-center text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>Loading statistics...
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Statistics Details Modal -->
<div class="modal fade" id="statsDetailsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Match Statistics Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="stats-details-content">
<!-- Details will be loaded here -->
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
let currentFilters = {
date_range: 'month',
fixture_id: '',
result: ''
};
// Load initial statistics
loadStatistics();
// Apply filters button
document.getElementById('apply-filters').addEventListener('click', function() {
currentFilters.date_range = document.getElementById('date-range').value;
currentFilters.fixture_id = document.getElementById('fixture-filter').value.trim();
currentFilters.result = document.getElementById('result-filter').value;
loadStatistics();
});
function loadStatistics() {
const tbody = document.getElementById('statistics-tbody');
tbody.innerHTML = `
<tr>
<td colspan="9" class="text-center text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>Loading statistics...
</td>
</tr>
`;
fetch('/api/statistics?' + new URLSearchParams(currentFilters))
.then(response => response.json())
.then(data => {
if (data.success) {
updateOverviewCards(data.summary);
updateStatisticsTable(data.statistics);
} else {
tbody.innerHTML = `
<tr>
<td colspan="9" class="text-center text-danger">
<i class="fas fa-exclamation-triangle me-2"></i>${data.error || 'Failed to load statistics'}
</td>
</tr>
`;
}
})
.catch(error => {
tbody.innerHTML = `
<tr>
<td colspan="9" class="text-center text-danger">
<i class="fas fa-exclamation-triangle me-2"></i>Error loading statistics: ${error.message}
</td>
</tr>
`;
});
}
function updateOverviewCards(summary) {
document.getElementById('total-matches').textContent = summary.total_matches;
document.getElementById('total-collected').textContent = formatCurrency(summary.total_collected);
document.getElementById('total-redistributed').textContent = formatCurrency(summary.total_redistributed);
document.getElementById('net-profit').textContent = formatCurrency(summary.net_profit);
// Color code profit
const profitElement = document.getElementById('net-profit');
if (summary.net_profit > 0) {
profitElement.className = 'card-title text-success';
} else if (summary.net_profit < 0) {
profitElement.className = 'card-title text-danger';
} else {
profitElement.className = 'card-title text-muted';
}
}
function updateStatisticsTable(statistics) {
const tbody = document.getElementById('statistics-tbody');
const countBadge = document.getElementById('stats-count');
countBadge.textContent = statistics.length;
if (statistics.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="9" class="text-center text-muted">
<i class="fas fa-info-circle me-2"></i>No statistics found for the selected filters
</td>
</tr>
`;
return;
}
tbody.innerHTML = statistics.map(stat => {
const profit = stat.total_amount_collected - stat.total_redistributed;
const profitPercent = stat.total_amount_collected > 0 ?
((profit / stat.total_amount_collected) * 100).toFixed(1) : '0.0';
const profitClass = profit > 0 ? 'text-success' : profit < 0 ? 'text-danger' : 'text-muted';
return `
<tr>
<td>${new Date(stat.match_datetime).toLocaleString()}</td>
<td><strong>${stat.match_id}</strong></td>
<td><code>${stat.fixture_id}</code></td>
<td>
<span class="badge bg-${getResultBadgeClass(stat.actual_result)}">
${stat.actual_result}
</span>
</td>
<td class="text-success">${formatCurrency(stat.total_amount_collected)}</td>
<td class="text-primary">${formatCurrency(stat.total_redistributed)}</td>
<td class="${profitClass}"><strong>${formatCurrency(profit)}</strong></td>
<td class="${profitClass}"><strong>${profitPercent}%</strong></td>
<td>
<button class="btn btn-sm btn-outline-info" onclick="showStatsDetails(${stat.id})">
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
`;
}).join('');
}
function getResultBadgeClass(result) {
const classes = {
'UNDER': 'warning',
'OVER': 'warning',
'WIN1': 'success',
'WIN2': 'success',
'KO1': 'danger',
'KO2': 'danger',
'DRAW': 'secondary'
};
return classes[result] || 'primary';
}
function formatCurrency(amount) {
return new Intl.NumberFormat('en-UG', {
style: 'currency',
currency: 'UGX',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(amount).replace('UGX', 'USh');
}
});
// Global function for showing statistics details
function showStatsDetails(statsId) {
const modal = new bootstrap.Modal(document.getElementById('statsDetailsModal'));
const content = document.getElementById('stats-details-content');
content.innerHTML = '<div class="text-center"><i class="fas fa-spinner fa-spin"></i> Loading details...</div>';
modal.show();
fetch(`/api/statistics/${statsId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
const stat = data.statistics;
const breakdown = JSON.parse(stat.result_breakdown || '{}');
content.innerHTML = `
<div class="row">
<div class="col-md-6">
<h6>Match Information</h6>
<table class="table table-sm">
<tr><td><strong>Match ID:</strong></td><td>${stat.match_id}</td></tr>
<tr><td><strong>Fixture ID:</strong></td><td><code>${stat.fixture_id}</code></td></tr>
<tr><td><strong>Date/Time:</strong></td><td>${new Date(stat.match_datetime).toLocaleString()}</td></tr>
<tr><td><strong>Actual Result:</strong></td><td><span class="badge bg-primary">${stat.actual_result}</span></td></tr>
<tr><td><strong>Extraction Result:</strong></td><td>${stat.extraction_result || 'N/A'}</td></tr>
</table>
</div>
<div class="col-md-6">
<h6>Financial Summary</h6>
<table class="table table-sm">
<tr><td><strong>Total Bets:</strong></td><td>${stat.total_bets}</td></tr>
<tr><td><strong>Amount Collected:</strong></td><td class="text-success">${formatCurrency(stat.total_amount_collected)}</td></tr>
<tr><td><strong>Amount Redistributed:</strong></td><td class="text-primary">${formatCurrency(stat.total_redistributed)}</td></tr>
<tr><td><strong>Net Profit:</strong></td><td class="${stat.total_amount_collected - stat.total_redistributed > 0 ? 'text-success' : 'text-danger'}"><strong>${formatCurrency(stat.total_amount_collected - stat.total_redistributed)}</strong></td></tr>
</table>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<h6>UNDER/OVER Breakdown</h6>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-body text-center">
<h5 class="text-warning">${stat.under_bets}</h5>
<p class="mb-1">UNDER Bets</p>
<small class="text-muted">${formatCurrency(stat.under_amount)}</small>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-body text-center">
<h5 class="text-warning">${stat.over_bets}</h5>
<p class="mb-1">OVER Bets</p>
<small class="text-muted">${formatCurrency(stat.over_amount)}</small>
</div>
</div>
</div>
</div>
</div>
</div>
${stat.cap_applied ? `
<div class="row mt-3">
<div class="col-12">
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>CAP Applied:</strong> Redistribution was capped at ${stat.cap_percentage}%
</div>
</div>
</div>
` : ''}
`;
} else {
content.innerHTML = `<div class="alert alert-danger">${data.error || 'Failed to load details'}</div>`;
}
})
.catch(error => {
content.innerHTML = `<div class="alert alert-danger">Error loading details: ${error.message}</div>`;
});
}
function formatCurrency(amount) {
return new Intl.NumberFormat('en-UG', {
style: 'currency',
currency: 'UGX',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(amount).replace('UGX', 'USh');
}
</script>
{% endblock %}
\ No newline at end of file
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