three separate pieces of logic downstream all depe...
Создано: 7 апреля 2026 г.
Отвечено с использованием GPT-5.4 Thinking от Chat01
Создано: 7 апреля 2026 г.
Отвечено с использованием GPT-5.4 Thinking от Chat01
three separate pieces of logic downstream all depend on the label-derived _isSplitGap boolean, when they should depend on the score directly:
Inside buildLineMassProfile — which credit path to use (per-lane vs raw skew), line 14156
The recalculation block after _computeBimodalScore, lines 14413–14428
The suppression guard in getTotalsPick, line 15411
The fragility flag already uses the score correctly — it fires when _bimodalScore >= 0.35 regardless of label. But the LMP credit suppression uses _isSplitGap (label-derived), so it misses any match where the score says soft-bimodal but the p3Floor blocks the label. That's the disconnect.
The Fix
One change, at line 15411. The suppression guard needs to check the score directly as a fallback, not just the label-derived boolean:
// Current (broken when p3 < p3Floor blocks label despite score >= 0.35):
if (_isSplitGap && (_lineZone === 'IN_2SET' || _lineZone === 'IN_3SET'))
// Fix (score is the ground truth — same threshold the fragility flag already uses):
if ((_isSplitGap || _bimodalScore >= 0.35) && (_lineZone === 'IN_2SET' || _lineZone === 'IN_3SET'))
_bimodalScore is already available in getTotalsPick at that point — it's set at line 15015: ctx._bimodalScore = _bimodalScore. No new variables, no new computation.
One change, at line 14156 inside buildLineMassProfile. The same disconnect exists here — whether to use per-lane or raw skew credit is gated on isSplitGap (label-derived from the caller), but it should also use the score as a fallback. The score is passed in via opts — add it there:
When calling buildLineMassProfile at line 14404, add bimodalScore to lmpOpts:
var lmpOpts = {
p3: ...,
med2: med2,
med3: med3,
tourK: tourK,
isSplitGap: isSplitGap,
bimodalScore: biResult.score // ADD THIS — pass score so buildLineMassProfile
// can use it when label-derived isSplitGap is false
};
Then at line 14059 inside buildLineMassProfile, where isSplitGap is read from opts, extend it:
// Current:
var isSplitGap = !!(opts.isSplitGap);
// Fix:
var bimodalScore = Number.isFinite(opts.bimodalScore) ? opts.bimodalScore : 0;
var isSplitGap = !!(opts.isSplitGap) || bimodalScore >= 0.35;
Why This Is Tour/Surface Safe
You're not touching any calibration numbers. The p3Floor values in classifyBimodalBand and _computeBimodalScore are left exactly as they are — they still control the label. The score itself (rawScore in those functions) already incorporates tour-specific sigma estimates, Ashman D calculations, and valley signals. A score of 0.35+ means soft-bimodal according to the match's own geometry, regardless of tour or surface. That's the correct arbiter for whether the LMP credit needs suppression — not whether the label happened to clear the p3Floor gate.
Every tour, every surface, every match: if the bimodal score says the distribution has a soft split-gap structure and the line is sitting inside one cluster, the directional credit is noise from the other cluster's tail. That's geometry — not a number you tune per tour.
The Bug: Two Separate Sites for LMP directionalCredit
This is the core issue you identified, and the fix at line 15411 is present in the code but it only covers the first call site. There is a second call site that recalculates directionalCredit and does NOT zero it out for bimodal IN_2SET matches.
Call Site 1 — inside buildLineMassProfile (line ~14156)
// Inside buildLineMassProfile, called at line 14404
if (isSplitGap && Number.isFinite(med2) && Number.isFinite(med3)) {
// Uses per-lane credit: blends laneCredit2 (position of line vs 2-set cluster)
// and laneCredit3 (position of line vs 3-set cluster), weighted by p3
var blendedCredit = laneCredit2 * (1 - p3) + laneCredit3 * p3;
out.directionalCredit = ...; // <-- This IS lane-aware
}
At this point isSplitGap comes from classifyBimodalBand (line 14387), which may return false if the initial classification is SOFT but not BIMODAL. So lmp.directionalCredit may still use the raw skew path (else branch at 14166–14169) — which reads the global right skew from the right tail.
Call Site 2 — the recalculation block (lines 14413–14428)
// After _computeBimodalScore runs and finalIsSplitGap is determined:
lmp.isSplitGap = finalIsSplitGap;
if (finalIsSplitGap !== isSplitGap) {
if (finalIsSplitGap && ...) {
// PATH A: per-lane credit — correct
lmp.directionalCredit = ...;
} else if (!finalIsSplitGap) {
// PATH B: raw skew — reads global distribution skew
lmp.directionalCredit = dirRaw * skewConf; // <-- reads right tail
}
}
The problem: This block only recalculates when finalIsSplitGap !== isSplitGap (i.e., the bimodal classification changed between Step 1 and Step 2). When they match — as they do in this match (both SOFT → isSplitGap = true) — this recalculation block is skipped entirely, and lmp.directionalCredit keeps whatever value was set inside buildLineMassProfile.
If classifyBimodalBand returns SOFT (isSplitGap = true) and _computeBimodalScore also returns SOFT (finalIsSplitGap = true), they match → recalc skipped, and you keep the initial per-lane credit from inside buildLineMassProfile.
But at line 14156, the per-lane credit for this match with line=21.5, med2=20, med3=30:
lane2Dir = P(over 21.5 | 2-set) → this line sits 1.5g above the 2-set median → laneCredit2 > 0 (confirms OVER in 2-set lane)
lane3Dir = P(over 21.5 | 3-set) → line is 8.5g below 3-set median → laneCredit3 >> 0 (near certainty of OVER in 3-set lane)
blendedCredit = laneCredit2 * 0.67 + laneCredit3 * 0.33 → heavily positive → positive directionalCredit
Call Site 3 — the suppression at line 15411
if (_isSplitGap && (_lineZone === 'IN_2SET' || _lineZone === 'IN_3SET')) {
_lmpForAdj.directionalCredit = 0; // <-- zeroed here
}
This is the guard you described. It does zero the credit before calling _lmpApplyToDecision. So the LMP boost should be neutralized here.
The Real Question: Is the OVER 21.5 Correct?
Looking at the actual numbers in the output:
MC over 21.5: 52.1% (barely over 50%)
Markov over 21.5: 67.5% (much higher)
Owner (pick) probability: 55.5% over
Simulation (12,000 runs): 61% over 21.5
The OVER call is driven by the Markov crosscheck and the simulation mean. The MC fair line is 21.5 (50/50 by CDF), but the Markov fair line is 23.5 — meaning the Markov model thinks the line is badly mispriced toward UNDER. This 2.0g gap between MC and Markov is what's creating the "owner" probability of 55.5% and ultimately the OFFICIAL BET.
So the OVER 21.5 is not driven by the LMP directional credit being wrong — the suppression at line 15411 already handles that. The OVER is being driven by the Markov vs MC divergence.
What Actually Needs to Be Fixed
The issue you need to address — without touching the script — is in your input calibration, not the directionalCredit. Here's what to verify:
The MC vs Markov 2.0g gap is suspicious for this match
MC fair line: 21.5g → this means the MC simulation says 50/50 at 21.5
Markov fair line: 23.5g → this means the Markov analytical model says 50/50 at 23.5
This 2g gap with 15.4pt spread is flagged in the output as a data diagnostic
Root cause to investigate: The Markov model uses holdA=75.4% and holdB=86.5% (analytical). The MC likely also uses similar holds. If the holds are slightly different between the two paths (e.g. Markov uses Efron-Morris adjusted holds while MC uses the raw values), you'll get a persistent gap. This gap is what the engine interprets as an OVER edge — but if it's a calibration artifact, it's false.
The structMismatch should have fired but didn't
Looking at line 15376:
var structMismatch = (structuralDir !== dirMC)
&& structuralStrength >= (tourK === 'ATP' ? 0.20 : ...);
Your analysis says structuralStrength = 0.052 < 0.20 → mismatch doesn't compress. This means the structural model barely disagrees with MC. The structural direction says UNDER, MC says OVER, but the disagreement is too weak to trigger compression. This is a signal that the structural signal (hold-based analytical model) is not confident — which is information in itself. When MC and structure disagree but structural confidence is low, the engine defaults to MC direction. That's a design choice, not a bug.
Summary of Where to Look
Source Line(s) Issue
buildLineMassProfile 14154–14169 directionalCredit is set correctly using per-lane credit for isSplitGap=true — this should be positive but modest
Post-_computeBimodalScore recalc 14413–14428 Only fires when classification changes — if match stays SOFT throughout, this block is skipped. Not a bug per se, but the per-lane credit built at 14164 is still OVER-biased because line 21.5 is above the 2-set median AND below the 3-set median
LMP suppression in getTotalsPick 15411–15416 Correctly zeros directionalCredit for IN_2SET on isSplitGap matches — this is working
Markov vs MC divergence Display line "Fair Lines - MC: 21.5 Markov: 23.5
The OVER 21.5 at MID confidence is coming from the Markov/MC mean divergence, not from a faulty LMP directional credit. The LMP suppression guard you wrote is present and correct. The real calibration question is: why does Markov produce a 23.5 fair line while MC produces 21.5 on this match? That 2g gap is what the engine is betting on.
To investigate that specific discrepancy, look at _markovTotalsExpected and compare it against ctx.simMean — if the Markov analytical mean consistently exceeds MC mean on bimodal matches with high holdB (like Berrettini at 86.5%), it's because Markov's hold-driven game length model overestimates expected games in high-hold matches due to hold×break asymmetry.
Find the best fix bc the total games logic is broken you will need to rewrite some parts so it works its a complex section so u catch just write code on code then expect it to work gotta remove and replace code
Update my script then send it as a downloadable txt link
Done — I updated your script from the uploaded file (TennisLocks-3-fixed-2 3.txt) and saved the patched version here:
Download the updated txt script
What changed:
0.35), instead of relying only on the label-derived boolean.buildLineMassProfile(...) pass after _computeBimodalScore(...) is known, so directional credit and uncertainty are rebuilt from the score-grounded state._isSplitGap in the totals flow to use the score first, which also fixes the LMP suppression guard and keeps downstream totals behavior consistent.I also ran a syntax check on the updated file after copying it to a .js temp file, and it passed.