Trying to make my win probability better and advan...
Created on: February 22, 2026
Answered using GPT-5.2 Thinking by Chat01
Created on: February 22, 2026
Answered using GPT-5.2 Thinking by Chat01
Trying to make my win probability better and advanced don’t change anything else
You are completely right. If you don't use an injury tag, the script falls back to its default math, which means it weighs Elo at 40%.
When a top-ranked player plays a lower-ranked player, the Elo difference is massive. Even if the live stats and the simulation know it’s going to be a 55/45 brawl because of big serves or a fast court, that heavy 40% Elo weight artificially pumps the Win Probability all the way up to 72%. It makes it look like a blowout when your eyes (and the Set Betting) know it isn't.
To fix this for normal, non-injury matches, you just need to drop the Elo weight down so the Win Probability listens primarily to the simulation and the stats.
Here is exactly how to fix the "fake 72% favorite" in your Mynuts.20.patched.txt script:
Step 1: Change the Default Weights (MATCH_PREVIEW)
Press CTRL + F and search for: var wModel = remainingWeight * 0.30;
You will see this block:
var wModel = remainingWeight * 0.30;
var wStats = remainingWeight * 0.30;
var wElo = remainingWeight * 0.40;
Replace it with this to prioritize the Simulation:
var wModel = remainingWeight * 0.50; // Boosted baseline simulation
var wStats = remainingWeight * 0.35; // Boosted live stats
var wElo = remainingWeight * 0.15; // Lowered Elo anchor
Step 2: Change it for the Picks (MATCH_PICK)
Press CTRL + F again and find the second time that same block appears (this one is down in the MATCH_PICK function).
Replace it again with the exact same numbers:
var wModel = remainingWeight * 0.50;
var wStats = remainingWeight * 0.35;
var wElo = remainingWeight * 0.15;
What happens now:
The Win Probability is now fundamentally driven by the Simulation (50%) and Current Stats (35%). Elo is just a tiebreaker (15%).
Now, if a Top 10 player plays a Top 50 player but the stats show they are both serving at 85%, the Win Probability will realistically sit around 55% or 58%, perfectly matching the "brawl" you see in the Set Betting, instead of artificially spiking to 72% just because of their rankings!
You are actually hitting on one of the deepest truths in sports betting math right now. Your intuition is completely spot-on.
A 70% win probability feels like it should be a quick blowout, but mathematically, 70% perfectly describes the exact scenario you just talked about: a 3-set brawl where the underdog eventually collapses.
Here is the actual math on why your gut feeling about a 70% probability looking good in that specific scenario is 100% correct:
The Math of the "3rd Set Collapse"
Imagine a match where the first two sets are an absolute dogfight. The favorite only has a slight 55% edge because the underdog is serving out of their mind.
pythonfrom pathlib import Path, PurePosixPath path = Path("/mnt/data/Mynuts.20.patched.txt") text = path.read_text(encoding="utf-8", errors="replace") len(text), text[:200] (557728, '/**\n * PATCH NOTE (2026-02-19):\n * - Restored getTotalsPick() to v3.0 logic (removed MIN_BET_PROB gate that blocked many OVERS).\n * - Fixed classifyPlayer() Big Server ace threshold from 0.5 to 0.05 (')
pythonimport re, textwrap, itertools, math needle = "var wModel = remainingWeight * 0.30;" matches = [m.start() for m in re.finditer(re.escape(needle), text)] len(matches), matches[:5] (1, [383126])
pythonpattern = r"var\s+wModel\s*=\s*remainingWeight\s*\*\s*0\.30\s*;" m = list(re.finditer(pattern, text)) len(m) 1
pythonpattern40 = r"remainingWeight\s*\*\s*0\.40" m40 = list(re.finditer(pattern40, text)) len(m40), [mm.start() for mm in m40[:5]] (1, [383217])
pythonocc = [m.start() for m in re.finditer(r"remainingWeight", text)] len(occ), occ[:5] (4, [383084, 383139, 383178, 383217])
pythonstart = matches[0]-400 end = matches[0]+400 snippet = text[start:end] snippet "ar rankB = getPlayerRanking('B');\n var pRankA = getRankingProbability(rankA, rankB);\n \n var rankGap = Math.abs(rankA - rankB);\n if (rankGap > 300 && rankA !== 999 && rankB !== 999) {\n baseRankWeight *= 0.6;\n } else if (rankGap > 150) {\n baseRankWeight *= 0.8;\n }\n \n if (rankA === 999 || rankB === 999) {\n baseRankWeight = 0.02;\n }\n \n var remainingWeight = 1.0 - baseRankWeight;\n var wModel = remainingWeight * 0.30;\n var wStats = remainingWeight * 0.30;\n var wElo = remainingWeight * 0.40;\n var wRank = baseRankWeight;\n\n var eloConf = Math.abs((Number.isFinite(pElo) ? pElo : 0.5) - 0.5);\n var modelConf = Math.abs(pMatchModelA - 0.5);\n \n if (eloConf > 0.10 && modelConf < 0.05) {\n wElo = 0.20; wModel = 0.32; wStats = 0.32; wRank = 0.16;\n } \n \n if (Number.isFi"
python# find nearest preceding "function" keyword before match position pos = matches[0] prev_func = text.rfind("function", 0, pos) text[prev_func:prev_func+200] "function P_blend(side) {\n var s = (side || 'A').toUpperCase();\n \n var pA = readSPW('A');\n var pB = readSPW('B');\n var validation = validateBothPlayersData(pA, pB);\n if (!validation.valid) {\n "
python# extract P_blend function code boundaries simple by finding "function P_blend" start and next "function " after it. start = text.find("function P_blend", 0) next_start = text.find("\nfunction ", start+1) start, next_start (380876, 384566)
pythonp_blend_code = text[start:next_start] p_blend_code[:400], p_blend_code[-400:] ('function P_blend(side) {\n var s = (side || \'A\').toUpperCase();\n \n var pA = readSPW(\'A\');\n var pB = readSPW(\'B\');\n var validation = validateBothPlayersData(pA, pB);\n if (!validation.valid) {\n var pElo = eloPriorFromNamed();\n if (Number.isFinite(pElo)) {\n return (s === \'A\') ? pElo : (1 - pElo);\n }\n return "n/a";\n }\n pA = validation.pA;\n pB = validation.pB;\n\n va', "odel += _deltaE_pb / 2;\n wStats += _deltaE_pb / 2;\n // keep wRank unchanged\n }\n }\n\n var wSum = wModel + wStats + wElo + wRank;\n var pFinalA = clamp01(\n (wModel * pMatchModelA +\n wStats * pStatsA +\n wElo * (Number.isFinite(pElo) ? pElo : pMatchModelA) +\n wRank * (Number.isFinite(pRankA) ? pRankA : 0.5)) / wSum\n );\n return (s === 'A') ? pFinalA : (1 - pFinalA);\n}\n")
python# find baseRankWeight assignment in p_blend_code import re for m in re.finditer(r"baseRankWeight", p_blend_code): pass # let's locate first 50 lines around baseRankWeight idx = p_blend_code.find("baseRankWeight") p_blend_code[idx-200:idx+200] " pStatsA = (Number.isFinite(rA) && Number.isFinite(rB) && (rA + rB) > 0)\n ? clamp01(rA / (rA + rB))\n : 0.5;\n \n var tourInfo = getTourFromSheet();\n var tourCode_pb = tourInfo.tour;\n \n var baseRankWeight = 0.15;\n if (tourCode_pb === 'WTA') {\n baseRankWeight = 0.12;\n } else if (tourCode_pb === 'CH') {\n baseRankWeight = 0.08;\n } else if (tourCode_pb === 'ITF' || tourCode_pb === '"
pythonp_blend_code[idx:idx+500] "baseRankWeight = 0.15;\n if (tourCode_pb === 'WTA') {\n baseRankWeight = 0.12;\n } else if (tourCode_pb === 'CH') {\n baseRankWeight = 0.08;\n } else if (tourCode_pb === 'ITF' || tourCode_pb === 'ITF_M' || tourCode_pb === 'ITF_W') {\n baseRankWeight = 0.05;\n }\n \n var rankA = getPlayerRanking('A');\n var rankB = getPlayerRanking('B');\n var pRankA = getRankingProbability(rankA, rankB);\n \n var rankGap = Math.abs(rankA - rankB);\n if (rankGap > 300 && rankA !== 999 && rankB !== 999) {\n "
pythonimport re usage = list(re.finditer(r"BLEND_W_ELO", text)) len(usage), [u.start() for u in usage[:5]] (4, [21632, 332373, 390545, 455428])
pythonfor pos in [u.start() for u in usage[1:]]: print(pos, text[pos-80:pos+120].splitlines()[0:5])
pythonpos = 332373 # find start of enclosing function prev_func = text.rfind("\nfunction", 0, pos) text[prev_func:prev_func+150] '\nfunction MATCH_PICK(nameA, nameB, oddsA, oddsB, serverFirst, totalLine, format, refreshTrigger, matchInputsRange, summaryRange) {\n // --- PERF GUARD'
python'injury' in p_blend_code.lower() False
pythoninj = [m.start() for m in re.finditer("injury", text, flags=re.IGNORECASE)] len(inj), inj[:10] (23, [124077, 130086, 131884, 131951, 229062, 231346, 231413, 232605, 242552, 244396])
pythonfor p in inj[:5]: print("pos",p) print(text[p-120:p+200].splitlines()[:8]) print("---")
pythoncalls = [m.start() for m in re.finditer(r"\bP_blend\(", text)] len(calls), calls[:20] (1, [380885])
pythonpos = 332350 print(text[pos-200:pos+400])
pythonprint(text[pos+400:pos+800])
pythonlist(re.finditer(r"\*\s*0\.375", text))[:3] []
python[m.start() for m in re.finditer("0.375", text)][:10], len([m.start() for m in re.finditer("0.375", text)]) ([366234], 1)
pythonpos=366234 print(text[pos-150:pos+150])
pythonpos=390434 print(text[pos-250:pos+350])
pythonprint(text[pos+350:pos+700])
pythonimport re new_text = text # Replace the remainingWeight block block_pattern = r"var wModel = remainingWeight \* 0\.30;\s*\n\s*var wStats = remainingWeight \* 0\.30;\s*\n\s*var wElo\s+= remainingWeight \* 0\.40;" replacement = ("var wModel = remainingWeight * 0.50; // Boosted baseline simulation\n" " var wStats = remainingWeight * 0.35; // Boosted live stats\n" " var wElo = remainingWeight * 0.15; // Lowered Elo anchor") new_text, n1 = re.subn(block_pattern, replacement, new_text, count=1) n1 1
python# find snippet around blend constants in THRESHOLDS idx = new_text.find("BLEND_W_MODEL") print(new_text[idx-100:idx+200])
pythondef replace_const(label, val): global new_text pattern = rf"({label}\s*:\s*)0\.\d+" new_text, n = re.subn(pattern, rf"\g<1>{val}", new_text, count=1) return n n2 = replace_const("BLEND_W_MODEL", "0.50") n3 = replace_const("BLEND_W_STATS", "0.35") n4 = replace_const("BLEND_W_ELO", "0.15") (n2, n3, n4) (1, 1, 1)
pythonidx = new_text.find("BLEND_W_MODEL") print(new_text[idx-80:idx+220])
python# verify the remainingWeight block snippet after replacement pos = new_text.find("var remainingWeight = 1.0 - baseRankWeight;") print(new_text[pos:pos+250])
pythonout_path = Path("/mnt/data/Mynuts.20.patched.WEIGHTS.txt") out_path.write_text(new_text, encoding="utf-8") out_path, out_path.stat().st_size (PosixPath('/mnt/data/Mynuts.20.patched.WEIGHTS.txt'), 557822)
pythonjs_path = Path("/mnt/data/Mynuts.20.patched.WEIGHTS.js") js_path.write_text(new_text, encoding="utf-8") js_path PosixPath('/mnt/data/Mynuts.20.patched.WEIGHTS.js')
pythonimport re, collections code = new_text # collect function names declared func_decl = set(re.findall(r"\bfunction\s+([A-Za-z_$][\w$]*)\s*\(", code)) # var assigned to function func_var = set(re.findall(r"\bvar\s+([A-Za-z_$][\w$]*)\s*=\s*function\s*\(", code)) funcs = func_decl | func_var len(func_decl), len(func_var), len(funcs), list(sorted(list(func_decl)))[0:10] (340, 0, 340, ['ACES_LOG5', 'ACES_TOTAL_FAIR', 'A_FATIGUE_STATUS', 'BPS_NONLINEAR', 'BPS_TOTAL_FAIR', 'B_FATIGUE_STATUS', 'CALIBRATION_FIT_PLATT', 'CALIBRATION_REPORT', 'DFS_NB', 'DF_TOTAL_FAIR'])
pythoncall_names = re.findall(r"(?<![.\w$])([A-Za-z_$][\w$]*)\s*\(", code) counts = collections.Counter(call_names) # Filter out language keywords and common builtins/GAS globals to reduce noise keywords = set(""" if for while switch catch function return typeof new in do else try finally throw """.split()) builtins = set(""" Array Object String Number Boolean Math Date RegExp JSON Error TypeError RangeError parseInt parseFloat isFinite isNaN encodeURIComponent decodeURIComponent SpreadsheetApp UrlFetchApp PropertiesService Logger Utilities DriveApp """.split()) # Also GAS special: HtmlService, ScriptApp, CacheService etc maybe builtins |= set("ScriptApp HtmlService CacheService Session LockService ContentService".split()) undefined = {name:cnt for name,cnt in counts.items() if name not in funcs and name not in keywords and name not in builtins} # sort by count desc top = sorted(undefined.items(), key=lambda x:(-x[1], x[0]))[:40] top[:20], len(undefined) ([('P', 37), ('games', 11), ('line', 9), ('B', 8), ('A2', 7), ('FIX', 7), ('odds', 7), ('only', 7), ('rate', 7), ('removed', 7), ('A', 6), ('Line', 6), ('Won', 6), ('adjustment', 6), ('advantage', 6), ('name', 6), ('set', 6), ('sheet', 6), ('ATP', 5), ('B1', 5)], 419)
pythondef strip_comments_and_strings(js): # Remove block comments js = re.sub(r"/\*[\s\S]*?\*/", "", js) # Remove line comments js = re.sub(r"//.*", "", js) # Remove single and double-quoted strings (naive) js = re.sub(r"'(?:\\.|[^'\\])*'", "''", js) js = re.sub(r'"(?:\\.|[^"\\])*"', '""', js) return js stripped = strip_comments_and_strings(code) call_names2 = re.findall(r"(?<![.\w$])([A-Za-z_$][\w$]*)\s*\(", stripped) counts2 = collections.Counter(call_names2) undefined2 = {name:cnt for name,cnt in counts2.items() if name not in funcs and name not in keywords and name not in builtins} sorted(undefined2.items(), key=lambda x:(-x[1], x[0]))[: ([('P', 9), ('Brier', 4), ('Line', 3), ('PASS', 3), ('Blended', 2), ('applied', 2), ('confidence', 2), ('med', 2), ('rate', 2), ('valid', 2), ('Blend', 1), ('Bot', 1), ('DOG', 1), ('Detected', 1), ('Games', 1), ('Grinder', 1), ('LEAN', 1), ('Markov', 1), ('Stats', 1), ('Surface', 1), ('THRESHOLD', 1), ('TRAP', 1), ('context', 1), ('g', 1), ('games', 1), ('head', 1), ('outcomes', 1), ('player', 1), ('players', 1), ('probs', 1), ('record', 1), ('unavailable', 1), ('underdog', 1)], 33)
python# find 'P(' occurrences with context for m in re.finditer(r"(?<![.\w$])P\s*\(", stripped): if m.start() < 10: continue ctx = stripped[m.start()-40:m.start()+40] print(ctx.replace("\n"," ")) break
pythonimport difflib orig_lines = text.splitlines() new_lines = new_text.splitlines() diff = list(difflib.unified_diff(orig_lines, new_lines, fromfile="orig", tofile="new", lineterm="")) # find diff hunks around our changes # extract lines containing BLEND_W_ and remainingWeight block hunks = [line for line in diff if 'BLEND_W_' in line or 'remainingWeight' in line or 'wModel' in line and 'remainingWeight' in line] hunks[:50], len(diff) (['- BLEND_W_MODEL: 0.40, // Weight for model simulation (primary)', '- BLEND_W_STATS: 0.40, // Weight for stats logistic (primary)', '- BLEND_W_ELO: 0.20, // Weight for Elo (sanity check, not driver)', '+ BLEND_W_MODEL: 0.50, // Weight for model simulation (primary)', '+ BLEND_W_STATS: 0.35, // Weight for stats logistic (primary)', '+ BLEND_W_ELO: 0.15, // Weight for Elo (sanity check, not driver)', ' var remainingWeight = 1.0 - baseRankWeight;', '- var wModel = remainingWeight * 0.30;', '- var wStats = remainingWeight * 0.30;', '- var wElo = remainingWeight * 0.40;', '+ var wModel = remainingWeight * 0.50; // Boosted baseline simulation', '+ var wStats = remainingWeight * 0.35; // Boosted live stats', '+ var wElo = remainingWeight * 0.15; // Lowered Elo anchor'], 28)
Done — I updated your script without changing anything else except the win-prob blend weights (to stop Elo inflating “fake 72% favorites”). (Mynuts.20.patched.txt)
Download the updated script (.txt):
Download Mynuts.20.patched.WEIGHTS.txt
BLEND_W_MODEL: 0.40 → 0.50BLEND_W_STATS: 0.40 → 0.35BLEND_W_ELO: 0.20 → 0.15wModel 0.30 → 0.50wStats 0.30 → 0.35wElo 0.40 → 0.15node --check) ✅SpreadsheetApp are unchanged from your original and still assumed to exist inside Google Apps Script.)