Spaces:
Running
Running
<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> | |