/** * Correlation engine. * * Pearson correlation between two stat streams. Caller feeds in pairs of * arrays (same player or same team) and we return the coefficient plus * the implied SGP adjustment for value flagging. */ function pearson(xs, ys) { if (!Array.isArray(xs) || !Array.isArray(ys) || xs.length !== ys.length || xs.length < 3) return null; let sx = 0, sy = 0; for (let i = 0; i < xs.length; i++) { sx += xs[i]; sy += ys[i]; } const mx = sx / xs.length, my = sy / ys.length; let num = 0, dx = 0, dy = 0; for (let i = 0; i < xs.length; i++) { const a = xs[i] - mx; const b = ys[i] - my; num += a * b; dx += a * a; dy += b * b; } const den = Math.sqrt(dx * dy); if (den === 0) return 0; return num / den; } /** * Compare measured correlation to the book's implicit SGP adjustment. * `bookAdjustment` is the multiplier the book applies to the joint price * vs the independent-events price. >1 means the book over-prices the * correlation; <1 means under-priced (VALUE). */ function flagValue(measuredR, bookAdjustment) { if (measuredR == null || bookAdjustment == null) return null; if (bookAdjustment < 1 && measuredR > 0.15) return 'VALUE'; if (bookAdjustment > 1.2 && measuredR < 0.1) return 'OVERPRICED'; return null; } module.exports = { pearson, flagValue };