Resitribution daily correction

parent 3b50b307
...@@ -12,7 +12,7 @@ from typing import Optional, Dict, Any, List ...@@ -12,7 +12,7 @@ from typing import Optional, Dict, Any, List
from .thread_manager import ThreadedComponent from .thread_manager import ThreadedComponent
from .message_bus import MessageBus, Message, MessageType, MessageBuilder from .message_bus import MessageBus, Message, MessageType, MessageBuilder
from ..database.manager import DatabaseManager from ..database.manager import DatabaseManager
from ..database.models import MatchModel, MatchStatus, BetDetailModel, MatchOutcomeModel, GameConfigModel, ExtractionAssociationModel from ..database.models import MatchModel, MatchStatus, BetDetailModel, MatchOutcomeModel, GameConfigModel, ExtractionAssociationModel, DailyRedistributionShortfallModel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -1880,6 +1880,62 @@ class GamesThread(ThreadedComponent): ...@@ -1880,6 +1880,62 @@ class GamesThread(ThreadedComponent):
logger.error(f"Failed to get redistribution CAP: {e}") logger.error(f"Failed to get redistribution CAP: {e}")
return 70.0 return 70.0
def _get_daily_shortfall(self, date, session) -> float:
"""Get accumulated shortfall for a specific date"""
try:
shortfall_record = session.query(DailyRedistributionShortfallModel).filter_by(
date=date
).first()
if shortfall_record:
logger.debug(f"Found shortfall record for {date}: {shortfall_record.accumulated_shortfall}")
return shortfall_record.accumulated_shortfall
else:
logger.debug(f"No shortfall record found for {date}, returning 0.0")
return 0.0
except Exception as e:
logger.error(f"Failed to get daily shortfall for {date}: {e}")
return 0.0
def _update_daily_shortfall(self, date, payin_amount, redistributed_amount, cap_percentage, session):
"""Update daily shortfall tracking after extraction"""
try:
# Calculate the shortfall for this extraction
expected_redistribution = payin_amount * (cap_percentage / 100.0)
shortfall = max(0, expected_redistribution - redistributed_amount)
logger.info(f"💰 [SHORTFALL DEBUG] Payin: {payin_amount:.2f}, Expected: {expected_redistribution:.2f}, Redistributed: {redistributed_amount:.2f}, Shortfall: {shortfall:.2f}")
# Get or create the daily record
shortfall_record = session.query(DailyRedistributionShortfallModel).filter_by(
date=date
).first()
if not shortfall_record:
shortfall_record = DailyRedistributionShortfallModel(
date=date,
accumulated_shortfall=shortfall,
total_payin=payin_amount,
total_redistributed=redistributed_amount,
cap_percentage=cap_percentage
)
session.add(shortfall_record)
logger.info(f"Created new shortfall record for {date} with shortfall {shortfall:.2f}")
else:
# Update existing record
shortfall_record.accumulated_shortfall += shortfall
shortfall_record.total_payin += payin_amount
shortfall_record.total_redistributed += redistributed_amount
shortfall_record.cap_percentage = cap_percentage # Update to latest
logger.info(f"Updated shortfall record for {date}, new accumulated shortfall: {shortfall_record.accumulated_shortfall:.2f}")
session.commit()
except Exception as e:
logger.error(f"Failed to update daily shortfall for {date}: {e}")
session.rollback()
def _weighted_random_selection(self, under_coeff: float, over_coeff: float) -> str: def _weighted_random_selection(self, under_coeff: float, over_coeff: float) -> str:
"""Weighted random selection based on inverse coefficients""" """Weighted random selection based on inverse coefficients"""
try: try:
...@@ -2308,8 +2364,17 @@ class GamesThread(ThreadedComponent): ...@@ -2308,8 +2364,17 @@ class GamesThread(ThreadedComponent):
# Step 5: Get redistribution CAP # Step 5: Get redistribution CAP
logger.info(f"🎯 [EXTRACTION DEBUG] Step 5: Retrieving redistribution CAP") logger.info(f"🎯 [EXTRACTION DEBUG] Step 5: Retrieving redistribution CAP")
cap_percentage = self._get_redistribution_cap() cap_percentage = self._get_redistribution_cap()
cap_threshold = total_bet_amount * (cap_percentage / 100.0)
logger.info(f"🎯 [EXTRACTION DEBUG] CAP percentage: {cap_percentage}%, threshold: {cap_threshold:.2f}") # Step 5.1: Get accumulated shortfall from previous extractions for today
logger.info(f"🎯 [EXTRACTION DEBUG] Step 5.1: Retrieving accumulated shortfall for today")
today = datetime.now().date()
accumulated_shortfall = self._get_daily_shortfall(today, session)
logger.info(f"🎯 [EXTRACTION DEBUG] Accumulated shortfall: {accumulated_shortfall:.2f}")
# Step 5.2: Calculate CAP threshold including shortfall
base_cap_threshold = total_bet_amount * (cap_percentage / 100.0)
cap_threshold = base_cap_threshold + accumulated_shortfall
logger.info(f"🎯 [EXTRACTION DEBUG] CAP percentage: {cap_percentage}%, base threshold: {base_cap_threshold:.2f}, shortfall: {accumulated_shortfall:.2f}, final threshold: {cap_threshold:.2f}")
logger.info(f"📊 [EXTRACTION DEBUG] Extraction summary - {len(payouts)} results, total_bet_amount={total_bet_amount:.2f}, CAP={cap_percentage}%, threshold={cap_threshold:.2f}") logger.info(f"📊 [EXTRACTION DEBUG] Extraction summary - {len(payouts)} results, total_bet_amount={total_bet_amount:.2f}, CAP={cap_percentage}%, threshold={cap_threshold:.2f}")
logger.info(f"📊 [EXTRACTION DEBUG] Payouts: {payouts}") logger.info(f"📊 [EXTRACTION DEBUG] Payouts: {payouts}")
...@@ -2345,6 +2410,11 @@ class GamesThread(ThreadedComponent): ...@@ -2345,6 +2410,11 @@ class GamesThread(ThreadedComponent):
logger.info(f"📈 [EXTRACTION DEBUG] Step 9: Collecting match statistics") logger.info(f"📈 [EXTRACTION DEBUG] Step 9: Collecting match statistics")
self._collect_match_statistics(match_id, fixture_id, selected_result, session) self._collect_match_statistics(match_id, fixture_id, selected_result, session)
# 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)
logger.info(f"✅ [EXTRACTION DEBUG] Result extraction completed successfully: selected {selected_result}") logger.info(f"✅ [EXTRACTION DEBUG] Result extraction completed successfully: selected {selected_result}")
# DEBUG: Final check - ensure result is not None # DEBUG: Final check - ensure result is not None
......
...@@ -2677,6 +2677,60 @@ class Migration_034_AddDefaultLicenseText(DatabaseMigration): ...@@ -2677,6 +2677,60 @@ class Migration_034_AddDefaultLicenseText(DatabaseMigration):
return False return False
class Migration_035_AddDailyRedistributionShortfallTable(DatabaseMigration):
"""Add daily_redistribution_shortfall table for tracking CAP shortfalls across matches"""
def __init__(self):
super().__init__("035", "Add daily_redistribution_shortfall table for tracking CAP shortfalls")
def up(self, db_manager) -> bool:
"""Create daily_redistribution_shortfall table"""
try:
with db_manager.engine.connect() as conn:
# Create daily_redistribution_shortfall table
conn.execute(text("""
CREATE TABLE IF NOT EXISTS daily_redistribution_shortfall (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date DATE NOT NULL UNIQUE,
accumulated_shortfall REAL DEFAULT 0.0 NOT NULL,
total_payin REAL DEFAULT 0.0 NOT NULL,
total_redistributed REAL DEFAULT 0.0 NOT NULL,
cap_percentage REAL DEFAULT 70.0 NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
)
"""))
# Create indexes for daily_redistribution_shortfall table
indexes = [
"CREATE INDEX IF NOT EXISTS ix_daily_redistribution_shortfall_date ON daily_redistribution_shortfall(date)",
]
for index_sql in indexes:
conn.execute(text(index_sql))
conn.commit()
logger.info("Daily redistribution shortfall table created successfully")
return True
except Exception as e:
logger.error(f"Failed to create daily_redistribution_shortfall table: {e}")
return False
def down(self, db_manager) -> bool:
"""Drop daily_redistribution_shortfall table"""
try:
with db_manager.engine.connect() as conn:
conn.execute(text("DROP TABLE IF EXISTS daily_redistribution_shortfall"))
conn.commit()
logger.info("Daily redistribution shortfall table dropped")
return True
except Exception as e:
logger.error(f"Failed to drop daily_redistribution_shortfall table: {e}")
return False
# Registry of all migrations in order # Registry of all migrations in order
# Registry of all migrations in order # Registry of all migrations in order
...@@ -2715,6 +2769,7 @@ MIGRATIONS: List[DatabaseMigration] = [ ...@@ -2715,6 +2769,7 @@ MIGRATIONS: List[DatabaseMigration] = [
Migration_032_FixExtractionAssociationDefaults(), Migration_032_FixExtractionAssociationDefaults(),
Migration_033_AddBarcodeFieldsToBets(), Migration_033_AddBarcodeFieldsToBets(),
Migration_034_AddDefaultLicenseText(), Migration_034_AddDefaultLicenseText(),
Migration_035_AddDailyRedistributionShortfallTable(),
] ]
......
...@@ -7,7 +7,7 @@ import hashlib ...@@ -7,7 +7,7 @@ import hashlib
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Dict, Any, Optional, List from typing import Dict, Any, Optional, List
from sqlalchemy import ( from sqlalchemy import (
Column, Integer, String, Text, DateTime, Boolean, Float, Column, Integer, String, Text, DateTime, Date, Boolean, Float,
JSON, ForeignKey, UniqueConstraint, Index, create_engine, Enum JSON, ForeignKey, UniqueConstraint, Index, create_engine, Enum
) )
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
...@@ -946,3 +946,21 @@ class ExtractionStatsModel(BaseModel): ...@@ -946,3 +946,21 @@ class ExtractionStatsModel(BaseModel):
def __repr__(self): def __repr__(self):
return f'<ExtractionStats Match {self.match_id}: {self.total_bets} bets, {self.total_amount_collected:.2f} collected, {self.actual_result}>' return f'<ExtractionStats Match {self.match_id}: {self.total_bets} bets, {self.total_amount_collected:.2f} collected, {self.actual_result}>'
class DailyRedistributionShortfallModel(BaseModel):
"""Daily redistribution shortfall tracking for CAP adjustments"""
__tablename__ = 'daily_redistribution_shortfall'
__table_args__ = (
Index('ix_daily_redistribution_shortfall_date', 'date'),
UniqueConstraint('date', name='uq_daily_redistribution_shortfall_date'),
)
date = Column(Date, nullable=False, unique=True, comment='Date for shortfall tracking')
accumulated_shortfall = Column(Float(precision=2), default=0.0, nullable=False, comment='Accumulated shortfall from previous extractions')
total_payin = Column(Float(precision=2), default=0.0, nullable=False, comment='Total payin for the day')
total_redistributed = Column(Float(precision=2), default=0.0, nullable=False, comment='Total redistributed for the day')
cap_percentage = Column(Float(precision=2), default=70.0, nullable=False, comment='CAP percentage used for calculations')
def __repr__(self):
return f'<DailyRedistributionShortfall {self.date}: shortfall={self.accumulated_shortfall:.2f}, payin={self.total_payin:.2f}, redistributed={self.total_redistributed:.2f}>'
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