Spaces:
Runtime error
Runtime error
| <html> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <head> | |
| <link rel="shortcut icon" type="image/x-icon" href="https://images.squarespace-cdn.com/content/v1/64790f5777b5d772678cce83/6d71eaee-f825-4324-be9b-2def32469eac/favicon.ico?format=100w"> | |
| <title>MARCI - NFL Betting</title> | |
| </head> | |
| <style> | |
| body { | |
| max-width: 90vw; | |
| margin: auto; | |
| background-color: black; | |
| font-family: 'Helvetica'; | |
| justify-content: center; | |
| text-align: center; | |
| padding-top: 2%; | |
| padding-bottom: 5%; | |
| } | |
| p { | |
| color: #f2f2f2; | |
| } | |
| h1 { | |
| color: #f2f2f2; | |
| margin-top: 40px; | |
| margin-bottom: 10px; | |
| font-size: xxx-large; | |
| } | |
| h2 { | |
| margin-top: 0px; | |
| color: #f2f2f2; | |
| } | |
| h3 { | |
| color: #f2f2f2; | |
| margin: 0px; | |
| } | |
| table { | |
| transition: 0.3s ease; | |
| margin-top: 20px; | |
| width: 80%; | |
| border-collapse: collapse; | |
| text-align: center; | |
| } | |
| .table-div { | |
| display: flex; | |
| justify-content: center; | |
| } | |
| th, td { | |
| color: #f2f2f2; | |
| border: 1px solid black; | |
| text-align: center; | |
| padding: 8px; | |
| } | |
| th { | |
| background-color: black; | |
| } | |
| tr { | |
| background-color: black; | |
| } | |
| tr:nth-child(even) { | |
| background-color: rgb(10, 10, 5); | |
| } | |
| td img { | |
| display: block; | |
| margin: auto; | |
| } | |
| input[type="text"] { | |
| font-size: 12pt; | |
| width: 45px; | |
| height: 30px; | |
| text-align: center; | |
| background-color: transparent; | |
| border-radius: 5px; | |
| transition: 0.3s ease; | |
| color: #f2f2f2; | |
| border: none; | |
| } | |
| input[type="text"]:hover { | |
| background-color:rgb(30, 30, 30); | |
| } | |
| button { | |
| font-size: 12pt; | |
| background-color: rgb(30, 30, 30); | |
| color: #ffffff; | |
| padding: 10px 20px; | |
| border: none; | |
| border-radius: 5px; | |
| margin-top: 40px; | |
| width: 80%; | |
| transition: all 0.3s ease; | |
| } | |
| button:hover { | |
| color: rgb(0, 0, 0); | |
| background-color: rgb(255, 255, 255); | |
| cursor: pointer; | |
| } | |
| .winner-wrapper { | |
| cursor: default; | |
| position: relative; | |
| width: 100%; | |
| text-align: center; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| transition: 0.3s ease; | |
| } | |
| .winner-image { | |
| height: auto; | |
| margin: 0; | |
| transition: 0.3s ease; | |
| } | |
| .over-under-wrapper { | |
| cursor: default; | |
| position: relative; | |
| width: 100%; | |
| height: 50px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: 0.3s ease; | |
| } | |
| .over-under-text { | |
| display: inline-block; | |
| margin: 0; | |
| margin-right: 2px; | |
| font-weight: bold; | |
| } | |
| .over { | |
| color: rgb(255, 255, 255); | |
| } | |
| .under { | |
| color: rgb(255, 255, 255); | |
| } | |
| .na { | |
| color: white; | |
| } | |
| .highlight { | |
| background: rgb(30, 30, 30) ; | |
| border: 2px solid rgb(30, 30, 30) ; | |
| border-radius: 10px ; | |
| } | |
| .force-repaint { transform: translateZ(0); } | |
| .hidden { | |
| opacity: 0; | |
| } | |
| .section-container { | |
| display: flex; | |
| justify-content: space-between; | |
| } | |
| .section { | |
| padding: 30px; | |
| text-align: left; | |
| border-style: solid; | |
| border-width: 1px; | |
| border-color: rgb(61, 61, 61); | |
| width: 48%; | |
| } | |
| .content { | |
| width: 100%; | |
| } | |
| .content img { | |
| width: 100%; | |
| height: auto; | |
| margin-top: 20px; | |
| margin-bottom: 20px; | |
| } | |
| .divider { | |
| border: 0; | |
| height: 1px; | |
| background: rgb(61, 61, 61); | |
| margin-top: 50px; | |
| margin-bottom: 50px; | |
| width: 80%; | |
| } | |
| .label { | |
| color: rgb(114, 114, 114); | |
| } | |
| .info { | |
| color: white; | |
| } | |
| a { | |
| color: white; | |
| } | |
| .scroll-banner { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| z-index: 999; | |
| width: 100%; | |
| display: flex; | |
| align-items: center; | |
| height: 30px; | |
| background-color: green; | |
| overflow: hidden; | |
| visibility: hidden; | |
| } | |
| .scroll-text { | |
| font-family: 'Helvetica'; | |
| color: white; | |
| display: inline-block; | |
| animation: scrolling 10s linear infinite; | |
| white-space: nowrap; | |
| } | |
| @keyframes scrolling { | |
| 0% { transform: translateX(100vw); } | |
| 100% { transform: translateX(-100%); } | |
| } | |
| .emoji { | |
| margin-left: 5px; | |
| color: rgb(255, 255, 255); | |
| transition: 0.3s ease; | |
| } | |
| .spinner { | |
| margin: auto; | |
| display: block; | |
| border: 2px solid transparent; | |
| border-radius: 50%; | |
| border-top: 2px solid #6a6a6a; | |
| width: 16px; | |
| height: 16px; | |
| animation: spin 1s linear infinite; | |
| } | |
| #gradient { | |
| background: red; | |
| background: -webkit-linear-gradient(left, orange , yellow, green, cyan, blue, violet); | |
| background: -o-linear-gradient(right, orange, yellow, green, cyan, blue, violet); | |
| background: -moz-linear-gradient(right, orange, yellow, green, cyan, blue, violet); | |
| background: linear-gradient(to right, orange , yellow, green, cyan, blue, violet); | |
| background-clip: text; | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| font-weight: bold; | |
| } | |
| .modelDetails { | |
| width: 80%; | |
| display: inline-block; | |
| margin-bottom: 40px; | |
| } | |
| #weekSelector { | |
| transition: 0.3s ease; | |
| border-radius: 10px; | |
| padding: 5px; | |
| color: white; | |
| background: rgb(30, 30, 30) ; | |
| font-family: Arial, Helvetica, sans-serif; | |
| } | |
| #weekSelector:hover { | |
| opacity: 0.5; | |
| } | |
| @keyframes spin { | |
| 0% { | |
| transform: rotate(0deg); | |
| } | |
| 100% { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| @media screen and (max-width: 768px) { | |
| .table-div { | |
| display: block; | |
| justify-content: center; | |
| } | |
| .winner-image { | |
| margin: 0; | |
| } | |
| .emoji { | |
| margin: 0; | |
| } | |
| .table-div{ | |
| overflow-x: scroll; | |
| } | |
| .divider { | |
| width: 90%; | |
| } | |
| #modelDetails { | |
| width: 90%; | |
| } | |
| .button { | |
| width: 90%; | |
| } | |
| .section-container { | |
| display: inline; | |
| } | |
| .section { | |
| padding: 15px; | |
| width: auto; | |
| } | |
| } | |
| </style> | |
| <div class="scroll-banner"> | |
| <div class="scroll-text"> | |
| Predictions will begin at the conclusion of Week 1. Bet at your own risk. Know your limits. And most importantly, have fun! | |
| </div> | |
| </div> | |
| <body> | |
| <h1>M A R C I</h1> | |
| <div class="info"> | |
| <span class="label"><i>Moore's Algorithm for Risky Capital Investments</i></span><br><br> | |
| <span id="gradient">Nobody said it was easy!</span><br><br> | |
| <span class="label"><b>Record through {{ latest_game }}</b></span><br> | |
| <span class="label">Winners:</span> {{ winners_correct }}-{{winners_incorrect}}{{winners_tie}}<span class="label"> ({{ winners_return }})</span><br> | |
| <span class="label">Over/Unders:</span> {{over_unders_correct}}-{{over_unders_incorrect}}{{over_unders_push}}<span class="label"> ({{over_unders_return}})</span><br><br> | |
| </div> | |
| <select id="weekSelector"> | |
| </select> | |
| <div class="table-div"> | |
| <table id="gameTable"> | |
| <tr> | |
| <th>Date</th> | |
| <th>Away</th> | |
| <th>Home</th> | |
| <th>O/U</th> | |
| <th>Predicted Winner</th> | |
| <th>Predicted O/U</th> | |
| </tr> | |
| </table> | |
| </div> | |
| <button id="submitButton"> | |
| Predict | |
| </button> | |
| <hr class="divider"> | |
| <div class="modelDetails"> | |
| <h2>Model Train/Test Details</h2> | |
| <div class="section-container"> | |
| <div class="section"> | |
| <h3>Moneyline</h3> | |
| <div class="info"></h3><span class="label">Test Accuracy:</span> 71.4%<br></div> | |
| <div class="content"> | |
| <img src="/Static/xgboost_ML_no_odds_71.4%25_dark.png" alt="Moneyline Model"> | |
| <div class="info"> | |
| <span class="label">Model:</span> XGBoost<br> | |
| <span class="label">Train/Test Split:</span> 1782/199<br> | |
| <span class="label">Max Depth:</span> 2<br> | |
| <span class="label">Learning Rate:</span> 0.01<br> | |
| <span class="label">Epochs:</span> 500 | |
| </div> | |
| </div> | |
| </div> | |
| <div class="section"> | |
| <h3>Over/Under</h3> | |
| <div class="content"> | |
| <div class="info"></h3><span class="label">Test Accuracy:</span> 59.8%<br></div> | |
| <img src="/Static/xgboost_OU_no_odds_59.8%25_dark.png" alt="Over/Under Model"> | |
| <div class="info"> | |
| <span class="label">Model:</span> XGBoost<br> | |
| <span class="label">Train/Test Split:</span> 1782/199<br> | |
| <span class="label">Max Depth:</span> 6<br> | |
| <span class="label">Learning Rate:</span> 0.05<br> | |
| <span class="label">Epochs:</span> 300 | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="modelDetails"> | |
| <h2>Winnings This Year</h2> | |
| <div class="section-container"> | |
| <div class="section"> | |
| <h3>Moneyline</h3> | |
| <div class="content"> | |
| <img src="/Static/Winner_Cumsum_dark.png" alt="Moneyline Model"> | |
| </div> | |
| </div> | |
| <div class="section"> | |
| <h3>Over/Under</h3> | |
| <div class="content"> | |
| <img src="/Static/Over_Under_Cumsum_dark.png" alt="Over/Under Model"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="modelDetails"> | |
| <h2>Predictive Accuracy This Year</h2> | |
| <div class="section-container"> | |
| <div class="section"> | |
| <h3>Moneyline</h3> | |
| <div class="info">{{ winners_return }}.</div> | |
| <div class="content"> | |
| <img src="/Static/Winner_Predictions_dark.png" alt="Moneyline Accuracy"> | |
| </div> | |
| <div class="info"><span class="label">{{ winners_binom }}</span><br></div> | |
| </div> | |
| <div class="section"> | |
| <h3>Over/Under</h3> | |
| <div class="info">{{ over_unders_return }}.</div> | |
| <div class="content"> | |
| <img src="/Static/Over_Under_Predictions_dark.png" alt="Over/Under Model"> | |
| </div> | |
| <div class="info"><span class="label">{{ over_unders_binom }}</span><br></div> | |
| </div> | |
| </div> | |
| </div> | |
| <p>🤗<a href="https://huggingface.co/spaces/BraydenMoore/MARCI-NFL-Betting/tree/main">See the Code</a></p> | |
| <script> | |
| async function fetchGames(selectedWeek) { | |
| const response = await fetch(`/get_games?week=${selectedWeek}`); | |
| const pulled_games = await response.json(); | |
| const table = document.getElementById('gameTable'); | |
| for(let i = table.rows.length - 1; i > 0; i--) { | |
| table.deleteRow(i); | |
| } | |
| const columns = ['Date','Away Team', 'Home Team']; | |
| let lines; | |
| try { | |
| const lines_response = await fetch('/get_lines'); | |
| if (!lines_response.ok) { | |
| throw new Error(`HTTP error! status: ${lines_response.status}`); | |
| } | |
| lines = await lines_response.json(); | |
| } | |
| catch (error) { | |
| lines = new Array(20).fill(0); | |
| } | |
| pulled_games.forEach((game, index) => { | |
| const row = table.insertRow(-1); | |
| columns.forEach((column) => { | |
| const cell = row.insertCell(-1); | |
| if (column === 'Away Team' || column === 'Home Team') { | |
| const img = document.createElement('img'); | |
| img.src = `/Static/${game[column]}.webp`; | |
| img.alt = game[column]; | |
| img.width = 50; | |
| cell.appendChild(img); | |
| } else { | |
| cell.textContent = game[column]; | |
| cell.style.color = "rgb(114, 114, 114)"; | |
| } | |
| }); | |
| for (let i = 0; i < 3; i++) { | |
| const cell = row.insertCell(-1); | |
| if (i<1) { | |
| const input = document.createElement('input'); | |
| input.type = 'text'; | |
| input.value = lines[index]; | |
| cell.appendChild(input); | |
| } | |
| } | |
| }); | |
| } | |
| function submitData() { | |
| const predictButton = document.getElementById('submitButton'); | |
| const table = document.getElementById('gameTable'); | |
| const rows = table.querySelectorAll('tr'); | |
| const games = []; | |
| rows.forEach((row, index) => { | |
| if (index === 0) return; | |
| const winnerCell = row.cells[row.cells.length - 2]; | |
| const overUnderCell = row.cells[row.cells.length - 1]; | |
| const spinnerDiv = document.createElement('div'); | |
| spinnerDiv.className = 'spinner'; | |
| winnerCell.innerHTML = ''; | |
| overUnderCell.innerHTML = ''; | |
| winnerCell.appendChild(spinnerDiv); | |
| overUnderCell.appendChild(spinnerDiv.cloneNode(true)); | |
| const cells = row.querySelectorAll('td'); | |
| const game = {}; | |
| game.Date = cells[0].textContent; | |
| game.AwayTeam = cells[1].querySelector('img').alt; | |
| game.HomeTeam = cells[2].querySelector('img').alt; | |
| game.OverUnderLine = cells[3].querySelector('input').value; | |
| game.rowIndex = index - 1; | |
| games.push(game); | |
| }); | |
| fetch('/submit_games', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(games), | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.moneylines && data.over_unders) { | |
| const table = document.getElementById('gameTable'); | |
| const rows = table.querySelectorAll('tr'); | |
| data.moneylines.forEach((moneyline, index) => { | |
| const row = rows[parseInt(moneyline.rowIndex) + 1]; | |
| const winnerCell = row.cells[row.cells.length - 2]; | |
| winnerCell.removeChild(winnerCell.querySelector('.spinner')); | |
| winnerCell.innerHTML = ''; | |
| const wrapperDiv = document.createElement('div'); | |
| wrapperDiv.className = 'winner-wrapper'; | |
| if (moneyline.Probabilities[0] > 0.6){ | |
| wrapperDiv.classList.add("highlight"); | |
| } | |
| else { | |
| wrapperDiv.style.opacity = "0.5"; | |
| } | |
| const winnerImg = document.createElement('img'); | |
| winnerImg.src = `/Static/${moneyline.Winner}.webp`; | |
| winnerImg.alt = moneyline.Winner; | |
| winnerImg.width = 50; | |
| winnerImg.className = 'winner-image hidden'; | |
| wrapperDiv.appendChild(winnerImg); | |
| const winnerEmojiDiv = document.createElement('div'); | |
| winnerEmojiDiv.className = 'emoji'; | |
| wrapperDiv.dataset.proba = Math.floor(moneyline.Probabilities[0] * 100).toFixed(0); | |
| if (moneyline.Winner[0] === moneyline.Result) { | |
| winnerEmojiDiv.textContent = '✅'; | |
| } | |
| else if (moneyline.Result === 'Tie') { | |
| winnerEmojiDiv.textContent = '🔵'; | |
| } | |
| else { | |
| winnerEmojiDiv.textContent = '❌'; | |
| } | |
| if (moneyline.Result === 'N/A') { | |
| winnerEmojiDiv.textContent = `(${wrapperDiv.dataset.proba}%)`; | |
| } | |
| wrapperDiv.appendChild(winnerEmojiDiv); | |
| setTimeout(() => { | |
| winnerImg.classList.remove('hidden'); | |
| }, 10); | |
| winnerCell.appendChild(wrapperDiv); | |
| const overUnderCell = row.cells[row.cells.length - 1]; | |
| overUnderCell.removeChild(overUnderCell.querySelector('.spinner')); | |
| overUnderCell.innerHTML = ''; | |
| const overUnderDiv = document.createElement('div'); | |
| overUnderDiv.className = 'over-under-wrapper hidden'; | |
| if (data.over_unders[index]['Probability'][0] > 0.6){ | |
| overUnderDiv.classList.add("highlight"); | |
| } | |
| else { | |
| overUnderDiv.style.opacity = "0.5"; | |
| } | |
| const textDiv = document.createElement('div'); | |
| textDiv.className = 'over-under-text'; | |
| textDiv.textContent = data.over_unders[index]['Over/Under']; | |
| if (textDiv.textContent === 'Over') { | |
| overUnderDiv.className += ' over'; | |
| } else if (textDiv.textContent === 'Under') { | |
| overUnderDiv.className += ' under'; | |
| } else { | |
| overUnderDiv.className += ' na'; | |
| } | |
| overUnderDiv.appendChild(textDiv); | |
| const overEmojiDiv = document.createElement('div'); | |
| overEmojiDiv.className = 'emoji'; | |
| overUnderDiv.dataset.proba = Math.floor(data.over_unders[index]['Probability'][0] * 100).toFixed(0); | |
| if (data.over_unders[index]['Over/Under'][0] === data.over_unders[index]['Result']) { | |
| overEmojiDiv.textContent = '✅'; | |
| } | |
| else if (data.over_unders[index]['Result'] === 'Push') { | |
| overEmojiDiv.textContent = '🔵'; | |
| } | |
| else { | |
| overEmojiDiv.textContent = '❌'; | |
| } | |
| if (data.over_unders[index]['Result'] === 'N/A') { | |
| overEmojiDiv.textContent = `(${overUnderDiv.dataset.proba}%)`; | |
| } | |
| overUnderDiv.appendChild(overEmojiDiv); | |
| setTimeout(() => { | |
| overUnderDiv.classList.remove('hidden'); | |
| }, 10); | |
| overUnderCell.appendChild(overUnderDiv); | |
| showProbabilityOnHover(wrapperDiv); | |
| showProbabilityOnHover(overUnderDiv); | |
| }); | |
| } | |
| }); | |
| } | |
| //Hover listener | |
| function showProbabilityOnHover(div) { | |
| let previousValue; | |
| let divText = div.children[1]; | |
| let eventProcessed = false; | |
| function handleEnter() { | |
| if (eventProcessed) return; // Skip if an event has already been processed | |
| eventProcessed = true; | |
| if (divText.textContent !== `(${div.dataset.proba}%)`) { | |
| divText.style.opacity = 0; | |
| setTimeout(() => { | |
| previousValue = divText.textContent; | |
| divText.textContent = `(${div.dataset.proba}%)`; | |
| divText.style.opacity = 1; | |
| }, 300); | |
| setTimeout(() => { | |
| divText.style.opacity = 0; | |
| setTimeout(() => { | |
| divText.textContent = previousValue; | |
| divText.style.opacity = 1; | |
| eventProcessed = false; // Reset the flag | |
| }, 300); | |
| }, 1000); | |
| } | |
| } | |
| // For desktop | |
| div.addEventListener('mouseenter', handleEnter); | |
| // For mobile | |
| div.addEventListener('touchstart', handleEnter); | |
| } | |
| // Populate dropdown | |
| let selectedWeek; | |
| async function populateDropdown() { | |
| const weekSelector = document.getElementById('weekSelector'); | |
| weekSelector.innerHTML = ""; | |
| const response = await fetch('/get_weeks'); | |
| const data = await response.json(); | |
| data.forEach((week, index) => { | |
| const option = document.createElement('option'); | |
| option.value = week; | |
| option.text = `Week ${week}`; | |
| weekSelector.appendChild(option); | |
| if (index === 0) { | |
| selectedWeek = week; | |
| } | |
| }); | |
| } | |
| // Get new games when new week selected | |
| document.getElementById('weekSelector').addEventListener('change', function(event) { | |
| selectedWeek = event.target.value; | |
| getNew(); | |
| }); | |
| // Initial load | |
| function loadThings() { | |
| populateDropdown() | |
| .then(() => fetchGames(selectedWeek)) | |
| .then(() => submitData()) | |
| .catch(error => console.error(error)); | |
| } | |
| // Get new | |
| async function getNew() { | |
| const table = document.getElementById('gameTable'); | |
| table.style.opacity = "0.5"; | |
| try { | |
| await fetchGames(selectedWeek); | |
| await submitData(); | |
| table.style.opacity = "1"; | |
| } catch (error) { | |
| console.error(error); | |
| } | |
| } | |
| // Submit on click, enter, and pageload | |
| loadThings(); | |
| document.getElementById('submitButton').addEventListener('click', submitData); | |
| document.addEventListener('keydown', function(event) { | |
| if (event.keyCode === 13) { | |
| submitData(); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |