Commit ec879339 authored by Your Name's avatar Your Name

Bump version from 10.0.29/10.0r29 to 1.0.30/1.0r30

Updated version in:
- mbetterclient/__init__.py (__version__)
- mbetterclient/config/settings.py (user_agent and version)
- mbetterclient/web_dashboard/app.py (app_version template global)
- main.py (--version argument)
- build.py (BUILD_CONFIG app_version)
parent 6d3020e3
......@@ -14,7 +14,7 @@ from typing import List, Dict, Any
# Build configuration
BUILD_CONFIG = {
'app_name': 'MbetterClient',
'app_version': '10.0.29',
'app_version': '1.0.30',
'description': 'Cross-platform multimedia client application',
'author': 'MBetter Team',
'entry_point': 'main.py',
......
......@@ -217,7 +217,7 @@ Examples:
parser.add_argument(
'--version',
action='version',
version='MbetterClient 10.0.29'
version='MbetterClient 1.0.30'
)
# Timer options
......
......@@ -4,7 +4,7 @@ MbetterClient - Cross-platform multimedia client application
A multi-threaded application with video playback, web dashboard, and REST API integration.
"""
__version__ = "10.0.29"
__version__ = "1.0.30"
__author__ = "MBetter Project"
__email__ = "dev@mbetter.net"
__description__ = "Cross-platform multimedia client with video overlay and web dashboard"
......
......@@ -262,7 +262,7 @@ class ApiConfig:
# Request settings
verify_ssl: bool = True
user_agent: str = "MbetterClient/10.0r29"
user_agent: str = "MbetterClient/1.0r30"
max_response_size_mb: int = 100
# Additional API client settings
......@@ -403,7 +403,7 @@ class AppSettings:
timer: TimerConfig = field(default_factory=TimerConfig)
# Application settings
version: str = "10.0.29"
version: str = "1.0.30"
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 source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
import random
def extract_balanced_safe(results, odds, current_balance, stake, house_edge=0.10, sensitivity=0.01, max_drift=0.5):
def extract_balanced_safe(results, odds, current_balance, stake, payouts, house_edge=0.10, sensitivity=0.01, max_drift=0.4):
"""
results: List of 7 items.
odds: List of 7 decimal odds.
......@@ -30,41 +30,72 @@ def extract_balanced_safe(results, odds, current_balance, stake, house_edge=0.10
sensitivity: Speed of balance recovery.
max_drift: Max % a probability can deviate from its 'edged' base.
"""
import logging
ext_logger = logging.getLogger(__name__)
ext_logger.info(f"🔍 [EXTRACT_BALANCED] INPUTS: results={results}, odds={odds}")
ext_logger.info(f"🔍 [EXTRACT_BALANCED] current_balance={current_balance:.2f}, stake={stake:.2f}, payouts={payouts}, house_edge={house_edge:.4f}, sensitivity={sensitivity:.4f}, max_drift={max_drift:.4f}")
# 1. Base Probabilities (1/odds)
raw_probs = [1.0 / o for o in odds]
ext_logger.info(f"🔍 [EXTRACT_BALANCED] Step 1 - Raw probabilities (1/odds): {raw_probs}")
# 2. Apply House Edge (Normalize to 1.0 + edge)
# This ensures that even with a 0 balance, the house wins on average.
total_raw = sum(raw_probs)
target_total = 1.0 + house_edge
edged_probs = [(p / total_raw) * target_total for p in raw_probs]
ext_logger.info(f"🔍 [EXTRACT_BALANCED] Step 2 - total_raw={total_raw:.4f}, target_total={target_total:.4f}")
ext_logger.info(f"🔍 [EXTRACT_BALANCED] Step 2 - Edged probabilities: {edged_probs}")
# 3. Adjust for USD Balance & Safety Caps
final_weights = []
expected_redistribution = stake*(1-house_edge)
for i, p in enumerate(edged_probs):
payout = stake * odds[i]
net_change = stake - payout
poutname = results[i]
payout = payouts.get(poutname, 0)
if type(payout).__name__ == 'list' and len(payout) > 1:
payout = payout[1]
#net_change = stake - payout
net_change = expected_redistribution - payout
ext_logger.info(f"🔍 [EXTRACT_BALANCED] edge index {i}: outcome {poutname} -> payout {payout} net_change={net_change}")
# Calculate correction factor
# Negative balance + Negative net_change = Weight reduction
correction = 1.0 + (current_balance * net_change * sensitivity / 100)
if current_balance < 0:
correction = 1.0 + (abs(current_balance) * net_change * sensitivity / 100)
else:
correction = 1.0 + (-abs(current_balance) * net_change * sensitivity / 100)
# Apply Safety Cap (max_drift)
# Prevents the 'edged' probability from swinging too wildly
lower_bound = p * (1 - max_drift)
upper_bound = p * (1 + max_drift)
final_w = max(min(p * correction, upper_bound), lower_bound)
corrected = p * correction
final_w = max(min(corrected, upper_bound), lower_bound)
final_weights.append(final_w)
ext_logger.info(f"🔍 [EXTRACT_BALANCED] Step 3 - Item {i} ({results[i]}): payout_if_wins={payout:.2f}, net_change={net_change:.2f}, original={p}, max_drift={max_drift:.6f}, correction={correction:.6f}, correctet={corrected:.6f}, bounds=[{lower_bound:.6f}, {upper_bound:.6f}], final_weight={final_w:.6f}")
ext_logger.info(f"🔍 [EXTRACT_BALANCED] Step 3 - Final weights: {final_weights}")
ext_logger.info(f"🔍 [EXTRACT_BALANCED] Step 3 - Sum of final weights: {sum(final_weights):.6f}")
# 4. Selection
selection = random.choices(results, weights=final_weights, k=1)
selected_odds = odds[results.index(selection[0])]
pout = payouts.get(selection[0], 0)
if type(pout).__name__ == 'list' and len(pout) > 1:
pout = pout[1]
ext_logger.info(f"🔍 [EXTRACT_BALANCED] Step 4 - SELECTED: {selection[0]} with odds={selected_odds}")
# 5. Result Impact
# Note: If the house wins the stake and pays 0, impact is +stake.
# If the user wins, impact is (stake - payout).
balance_impact = stake - (stake * selected_odds)
expected_redistribution = stake * (1-house_edge)
balance_impact = expected_redistribution - pout
ext_logger.info(f"🔍 [EXTRACT_BALANCED] Step 5 - balance_impact = {balance_impact:.2f}")
ext_logger.info(f"🔍 [EXTRACT_BALANCED] Payout: {pout:.2f}")
ext_logger.info(f"🔍 [EXTRACT_BALANCED] Expected redistribytion: {expected_redistribution:.2f}")
ext_logger.info(f"🔍 [EXTRACT_BALANCED] OUTPUT: selected={selection[0]}, impact={balance_impact:.2f}")
return selection[0], balance_impact
......@@ -116,39 +147,72 @@ def extract_match(balance, uopayin, mpayin, match, cap, payouts, odds):
import logging
logger = logging.getLogger(__name__)
logger.info("--------------------------------------------------")
logger.info(" EXTRACTION ")
logger.info("--------------------------------------------------")
logger.info(f"Match: {match}")
logger.info(f"Balance (accumulated shortfall): {balance}")
logger.info(f"UOPayin (UNDER+OVER bets): {uopayin}")
logger.info(f"MPayin (other bets): {mpayin}")
logger.info(f"Cap (redistribution %): {cap}")
logger.info(f"Payouts: {payouts}")
logger.info("--------------------------------------------------")
logger.info("=" * 80)
logger.info(" EXTRACTION - START ")
logger.info("=" * 80)
logger.info(f"📊 [EXTRACT_MATCH] Match: {match}")
logger.info(f"📊 [EXTRACT_MATCH] Balance (accumulated shortfall): {balance:.2f}")
logger.info(f"📊 [EXTRACT_MATCH] UOPayin (UNDER+OVER bets): {uopayin:.2f}")
logger.info(f"📊 [EXTRACT_MATCH] MPayin (other bets): {mpayin:.2f}")
logger.info(f"📊 [EXTRACT_MATCH] Total Payin: {uopayin + mpayin:.2f}")
logger.info(f"📊 [EXTRACT_MATCH] Cap (redistribution %): {cap:.2f}%")
logger.info(f"📊 [EXTRACT_MATCH] House edge for extraction: {(100-cap)/100:.4f}")
logger.info(f"📊 [EXTRACT_MATCH] Payouts: {payouts}")
logger.info(f"📊 [EXTRACT_MATCH] Odds: {odds}")
logger.info("=" * 80)
rodds = list(set(odds.keys()) - set(["UNDER", "OVER"]))
rodds_values = [odds[odd] for odd in rodds]
logger.info(f"📊 [EXTRACT_MATCH] Result outcomes: {rodds}")
logger.info(f"📊 [EXTRACT_MATCH] Result odds: {rodds_values}")
# First extraction: UNDER/OVER
logger.info("-" * 60)
logger.info("📊 [EXTRACT_MATCH] EXTRACTION 1: UNDER/OVER")
logger.info("-" * 60)
uores, uoimpact = extract_balanced_safe(['UNDER', 'OVER'], [odds['UNDER'],
odds['OVER']], balance, uopayin,
odds['OVER']], balance, uopayin, payouts,
float((100-cap)/100))
logger.info(f"📊 [EXTRACT_MATCH] UNDER/OVER result: {uores}, impact: {uoimpact:.2f}")
newbalance = balance+uoimpact
logger.info(f"📊 [EXTRACT_MATCH] New balance after UO extraction: {newbalance:.2f} (was {balance:.2f} + {uoimpact:.2f})")
# Only perform second extraction if there are other outcomes besides UNDER/OVER
if rodds and rodds_values:
mres, mimpact = extract_balanced_safe(rodds, rodds_values, newbalance, mpayin, float((100-cap)/100))
logger.info("-" * 60)
logger.info("📊 [EXTRACT_MATCH] EXTRACTION 2: RESULT OUTCOMES")
logger.info("-" * 60)
mres, mimpact = extract_balanced_safe(rodds, rodds_values, newbalance, mpayin, payouts['RESULTS'], float((100-cap)/100))
impact = uoimpact + mimpact
logger.info(f"📊 [EXTRACT_MATCH] Result outcome: {mres}, impact: {mimpact:.2f}")
else:
# No other outcomes, just use the UNDER/OVER result and impact
mres = None
mimpact = 0
impact = uoimpact
logger.info("📊 [EXTRACT_MATCH] No result outcomes, skipping second extraction")
mres_str = str(mres) if mres is not None else "N/A"
print(f" **** EXTRACTED: {uores}, {mres_str}, {impact} **** ")
logger.info("--------------------------------------------------")
# Calculate what the actual payout will be
under_payout = payouts.get("UNDER", 0)
over_payout = payouts.get("OVER", 0)
result_payout = payouts["RESULTS"][mres_str][1] if mres_str != "N/A" and mres_str in payouts.get("RESULTS", {}) else 0
total_actual_payout = (under_payout if uores == "UNDER" else over_payout) + result_payout
logger.info("=" * 80)
logger.info("📊 [EXTRACT_MATCH] EXTRACTION SUMMARY")
logger.info("=" * 80)
logger.info(f"📊 [EXTRACT_MATCH] UNDER/OVER winner: {uores}")
logger.info(f"📊 [EXTRACT_MATCH] Result winner: {mres_str}")
logger.info(f"📊 [EXTRACT_MATCH] UO impact: {uoimpact:.2f}, M impact: {mimpact:.2f}, Total impact: {impact:.2f}")
logger.info(f"📊 [EXTRACT_MATCH] Total payin: {uopayin + mpayin:.2f}")
logger.info(f"📊 [EXTRACT_MATCH] Total actual payout: {total_actual_payout:.2f}")
logger.info(f"📊 [EXTRACT_MATCH] House profit/loss: {(uopayin + mpayin) - total_actual_payout:.2f}")
logger.info("=" * 80)
print(f" **** EXTRACTED: {uores}, {mres_str}, impact={impact:.2f}, payout={total_actual_payout:.2f} **** ")
return uores, mres_str, impact, payouts["RESULTS"][mres_str][0]
......@@ -1564,8 +1628,12 @@ class GamesThread(ThreadedComponent):
match_obj = session.query(MatchModel).filter_by(id=match_id).first()
match_number = match_obj.match_number if match_obj else 0
# Log both IDs for debugging
logger.info(f"📊 [MATCH INFO] Match #{match_number} (DB ID: {match_id}), Fixture: {fixture_id}")
# Get accumulated balance (shortfall/surplus from previous extractions)
balance = self._get_global_redistribution_adjustment(session)
logger.info(f"💰 [BET CALC] Accumulated balance from DB: {balance:.2f}")
# Calculate total payin from ALL bets (UNDER + OVER + other outcomes)
# Get UNDER bets
......@@ -1576,6 +1644,10 @@ class GamesThread(ThreadedComponent):
BetDetailModel.result != 'cancelled'
).all()
under_amount = sum(bet.amount for bet in under_bets) if under_bets else 0.0
logger.info(f"💰 [BET CALC] UNDER bets: {len(under_bets)} bets, total amount: {under_amount:.2f}")
if under_bets:
for bet in under_bets:
logger.info(f"💰 [BET CALC] - Bet {bet.id}: amount={bet.amount:.2f}")
# Get OVER bets
over_bets = session.query(BetDetailModel).filter(
......@@ -1585,6 +1657,10 @@ class GamesThread(ThreadedComponent):
BetDetailModel.result != 'cancelled'
).all()
over_amount = sum(bet.amount for bet in over_bets) if over_bets else 0.0
logger.info(f"💰 [BET CALC] OVER bets: {len(over_bets)} bets, total amount: {over_amount:.2f}")
if over_bets:
for bet in over_bets:
logger.info(f"💰 [BET CALC] - Bet {bet.id}: amount={bet.amount:.2f}")
# Get all other bets (non UNDER/OVER)
other_bets = session.query(BetDetailModel).filter(
......@@ -1594,21 +1670,28 @@ class GamesThread(ThreadedComponent):
~BetDetailModel.outcome.in_(['UNDER', 'OVER'])
).all()
other_amount = sum(bet.amount for bet in other_bets) if other_bets else 0.0
logger.info(f"💰 [BET CALC] Other bets: {len(other_bets)} bets, total amount: {other_amount:.2f}")
# Total payin is sum of all bets
payin = under_amount + over_amount + other_amount
logger.info(f"💰 [BET CALC] Total payin: {payin:.2f} (UNDER {under_amount:.2f} + OVER {over_amount:.2f} + Other {other_amount:.2f})")
# Get redistribution cap percentage
cap = self._get_redistribution_cap()
logger.info(f"💰 [BET CALC] Redistribution cap: {cap:.2f}%")
# Get UNDER/OVER coefficients
under_coeff, over_coeff = self._get_fixture_coefficients(fixture_id, session)
# Get UNDER/OVER coefficients from the specific match (not the fixture)
under_coeff, over_coeff = self._get_match_coefficients(match_id, session)
logger.info(f"💰 [BET CALC] UNDER coefficient: {under_coeff}")
logger.info(f"💰 [BET CALC] OVER coefficient: {over_coeff}")
# Calculate UNDER payout
under_payout = under_amount * under_coeff if under_coeff else 0.0
logger.info(f"💰 [BET CALC] UNDER payout calculation: {under_amount:.2f} × {under_coeff} = {under_payout:.2f}")
# Calculate OVER payout
over_payout = over_amount * over_coeff if over_coeff else 0.0
logger.info(f"💰 [BET CALC] OVER payout calculation: {over_amount:.2f} × {over_coeff} = {over_payout:.2f}")
# Build RESULTS dictionary with payouts for each result
# Get all result options and their associated outcomes
......@@ -1631,6 +1714,8 @@ class GamesThread(ThreadedComponent):
total_payout = 0.0
outcome_list = []
logger.info(f"💰 [BET CALC] Result '{result_name}': {len(associations)} associated outcomes")
for association in associations:
outcome_name = association.outcome_name
outcome_list.append(outcome_name)
......@@ -1652,10 +1737,16 @@ class GamesThread(ThreadedComponent):
).all()
outcome_amount = sum(bet.amount for bet in outcome_bets) if outcome_bets else 0.0
total_payout += outcome_amount * coefficient
outcome_payout = outcome_amount * coefficient
total_payout += outcome_payout
logger.info(f"💰 [BET CALC] Outcome '{outcome_name}': coeff={coefficient}, bets={len(outcome_bets)}, amount={outcome_amount:.2f}, payout={outcome_payout:.2f}")
else:
logger.warning(f"💰 [BET CALC] Outcome '{outcome_name}': NO coefficient found!")
# Store in results dict with format: [list_of_outcomes, calculated_total_payout]
results_payouts[result_name] = [', '.join(outcome_list), total_payout]
logger.info(f"💰 [BET CALC] Result '{result_name}': outcomes=[{', '.join(outcome_list)}], total_payout={total_payout:.2f}")
# Build the payouts dictionary with the required schema
payouts = {
......@@ -1678,8 +1769,11 @@ class GamesThread(ThreadedComponent):
uopayin = under_amount + over_amount
mpayin = other_amount
logger.info(f"📊 [EXTRACT_MATCH] Calling extract_match with: balance={balance}, uopayin={uopayin}, mpayin={mpayin}, match={match_number}, cap={cap}")
logger.info(f"📊 [EXTRACT_MATCH] Payouts: {payouts}")
logger.info(f"📊 [EXTRACT_MATCH] Match #{match_number} (DB ID: {match_id}): balance={balance:.2f}, uopayin={uopayin:.2f}, mpayin={mpayin:.2f}, cap={cap:.2f}")
logger.info(f"📊 [EXTRACT_MATCH] Coefficients used: UNDER={under_coeff}, OVER={over_coeff}")
logger.info(f"📊 [EXTRACT_MATCH] Payouts: UNDER={under_payout:.2f}, OVER={over_payout:.2f}")
for rname, rdata in results_payouts.items():
logger.info(f"📊 [EXTRACT_MATCH] Result '{rname}': outcomes='{rdata[0]}', payout={rdata[1]:.2f}")
logger.info(f"📊 [EXTRACT_MATCH] Odds: {odds}")
# Call extract_match before the first extraction
......@@ -1709,9 +1803,31 @@ class GamesThread(ThreadedComponent):
self._collect_match_statistics(match_id, fixture_id, mres, session)
# Step 10: Update global redistribution adjustment tracking
# Calculate the ACTUAL adjustment from real values, not the theoretical impact
total_payin = uopayin + mpayin
expected_redistribution = total_payin * (cap / 100.0)
# Get the actual payout for the winning UNDER/OVER outcome
uo_payout = payouts[uores]
# Get the actual payout for the winning result outcome
result_payout = payouts["RESULTS"][mres][1] if mres in payouts.get("RESULTS", {}) else 0
actual_redistributed = uo_payout + result_payout
# Positive = surplus (house kept more than expected, can redistribute in future)
# Negative = shortfall (house paid out more than expected, needs to recover)
actual_adjustment = expected_redistribution - actual_redistributed
logger.info(f"💰 [EXTRACTION DEBUG] Step 10: Updating global redistribution adjustment tracking")
self._update_global_redistribution_adjustment(impact, uopayin+mpayin,
payouts[uores]+payouts["RESULTS"][mres][1], cap, session)
logger.info(f"💰 [ADJUSTMENT CALC] Total payin: {total_payin:.2f}")
logger.info(f"💰 [ADJUSTMENT CALC] Cap: {cap:.2f}%")
logger.info(f"💰 [ADJUSTMENT CALC] Expected redistribution: {total_payin:.2f} × {cap/100:.4f} = {expected_redistribution:.2f}")
logger.info(f"💰 [ADJUSTMENT CALC] UO winner: {uores}, UO payout: {uo_payout:.2f}")
logger.info(f"💰 [ADJUSTMENT CALC] Result winner: {mres}, Result payout: {result_payout:.2f}")
logger.info(f"💰 [ADJUSTMENT CALC] Actual redistributed: {uo_payout:.2f} + {result_payout:.2f} = {actual_redistributed:.2f}")
logger.info(f"💰 [ADJUSTMENT CALC] Adjustment: {expected_redistribution:.2f} - {actual_redistributed:.2f} = {actual_adjustment:.2f}")
logger.info(f"💰 [ADJUSTMENT CALC] Interpretation: {'SURPLUS (house kept more)' if actual_adjustment > 0 else 'SHORTFALL (house paid more)'}")
self._update_global_redistribution_adjustment(actual_adjustment, total_payin,
actual_redistributed, cap, session)
finally:
session.close()
......@@ -3188,7 +3304,11 @@ class GamesThread(ThreadedComponent):
return 0.0
def _get_fixture_coefficients(self, fixture_id: str, session) -> tuple:
"""Get UNDER/OVER coefficients from fixture/match outcomes"""
"""Get UNDER/OVER coefficients from fixture/match outcomes
DEPRECATED: This method gets coefficients from the first match in the fixture,
which may not be the match being processed. Use _get_match_coefficients() instead.
"""
try:
# Get one active match from the fixture to get UNDER/OVER coefficients
match = session.query(MatchModel).filter(
......@@ -3216,6 +3336,39 @@ class GamesThread(ThreadedComponent):
logger.error(f"Failed to get fixture coefficients: {e}")
return None, None
def _get_match_coefficients(self, match_id: int, session) -> tuple:
"""Get UNDER/OVER coefficients from a specific match's outcomes.
Args:
match_id: The specific match ID to get coefficients from
session: Database session
Returns:
Tuple of (under_coefficient, over_coefficient)
"""
try:
# Get UNDER/OVER coefficients directly from match outcomes table
under_coeff = None
over_coeff = None
match_outcomes = session.query(MatchOutcomeModel).filter(
MatchOutcomeModel.match_id == match_id,
MatchOutcomeModel.column_name.in_(['UNDER', 'OVER'])
).all()
for outcome in match_outcomes:
if outcome.column_name == 'UNDER':
under_coeff = outcome.float_value
elif outcome.column_name == 'OVER':
over_coeff = outcome.float_value
logger.debug(f"Match {match_id} coefficients: UNDER={under_coeff}, OVER={over_coeff}")
return under_coeff, over_coeff
except Exception as e:
logger.error(f"Failed to get match coefficients for match {match_id}: {e}")
return None, None
def _get_redistribution_cap(self) -> float:
"""Get redistribution CAP percentage from game configuration"""
try:
......
......@@ -230,7 +230,7 @@ class WebDashboard(ThreadedComponent):
def inject_globals():
return {
'app_name': 'MbetterClient',
'app_version': '10.0.29',
'app_version': '1.0.30',
'current_time': time.time(),
}
......
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