Spaces:
Running
# Sports Betting AI System - Complete Implementation Guide
Browse files## Table of Contents
1. [System Overview](#system-overview)
2. [Architecture](#architecture)
3. [Setup Instructions](#setup-instructions)
4. [Core Components](#core-components)
5. [API Integration](#api-integration)
6. [Machine Learning Pipeline](#machine-learning-pipeline)
7. [Risk Management](#risk-management)
8. [Dashboard Interface](#dashboard-interface)
9. [AI Agent Enhancements](#ai-agent-enhancements)
10. [Deployment & Operations](#deployment--operations)
---
## System Overview
A production-ready, Dockerized sports betting AI agent that:
- Ingests historical and live game/odds data via APIs
- Engineers features and trains ML models
- Runs inference for value pick selection
- Implements Kelly criterion risk management
- Sends formatted picks via Telegram
- Provides interactive web dashboard
- Self-evaluates performance with backtesting
### Key Features
- **ML Models**: XGBoost/LightGBM ensemble with calibration
- **Risk Controls**: Kelly criterion, portfolio optimization, stop-loss
- **Data Source**: The Odds API integration
- **Automation**: Scheduled pipelines via APScheduler/n8n
- **Notifications**: Telegram bot for picks and reports
- **Dashboard**: Modern web interface for monitoring
### Tech Stack
- **Backend**: Python 3.11, FastAPI, PostgreSQL, Redis
- **ML**: scikit-learn, XGBoost, LightGBM
- **Frontend**: HTML5, CSS3 (glassmorphism), JavaScript
- **Orchestration**: Docker Compose, n8n
- **Messaging**: Telegram Bot API
---
## Architecture
```
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Web Dashboard β
β (HTML/CSS/JS - Glassmorphism UI) β
ββββββββββββββββββββββ¬ββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββΌββββββββββββββββββββββββββββ
β FastAPI Backend β
β ββββββββββββ ββββββββββββ ββββββββββββ β
β βIngestion β β ML β β Pick β β
β β Service β β Pipeline β βSelection β β
β ββββββββββββ ββββββββββββ ββββββββββββ β
ββββββββββββββββββββββ¬ββββββββββββββββββββββββββββ
β
ββββββββββββββ΄βββββββββββββ
β β
βββββββββΌβββββββββ βββββββββΌβββββββββ
β PostgreSQL β β Redis β
β (Main DB) β β (Cache) β
ββββββββββββββββββ ββββββββββββββββββ
β
βββββββββΌβββββββββββββββββββββββββββββββββ
β External Services β
β ββββββββββββ ββββββββββββ β
β β Odds API β β Telegram β β
β ββββββββββββ ββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββ
```
---
## Setup Instructions
### Prerequisites
- Docker & Docker Compose
- Python 3.11+
- Telegram Bot Token
- The Odds API Key
### Environment Configuration
Create `.env` file:
```bash
# API Keys
ODDS_API_KEY=327b7a158175a6f471f8e41fa0acfe5f # Your paid key
TELEGRAM_BOT_TOKEN=your_telegram_bot_token
TELEGRAM_CHAT_ID=your_chat_id
# Database
DB_HOST=postgres
DB_PORT=5432
DB_NAME=sports_betting
DB_USER=betting_user
DB_PASSWORD=secure_password_123
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
# Risk Management
MAX_KELLY_FRACTION=0.25
MAX_BANKROLL_PER_BET=0.005
MIN_EDGE_THRESHOLD=0.02
MAX_PICKS_PER_DAY=10
INITIAL_BANKROLL=1000
# Time Zone
TZ=America/Kentucky/Louisville
```
### Quick Start Commands
```bash
# 1. Clone repository
git clone <repository-url>
cd sports-betting-ai
# 2. Build and start services
docker-compose up -d --build
# 3. Initialize database
docker-compose exec app python -c "from src.database import init_database; init_database()"
# 4. Backfill historical data
docker-compose exec app python scripts/backfill.py --days 30
# 5. Train initial models
docker-compose exec app python -c "from src.ml_training import ModelTrainer; ModelTrainer().train_all_models()"
# 6. Run pipeline
docker-compose exec app python scripts/daily_pipeline.py
# 7. Open dashboard
open dashboard.html
```
---
## Core Components
### 1. Docker Compose Configuration
```yaml
version: '3.8'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/setup_db.sql:/docker-entrypoint-initdb.d/01-init.sql
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
app:
build: .
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
env_file:
- .env
volumes:
- ./src:/app/src
- ./scripts:/app/scripts
ports:
- "8000:8000"
command: python -m src.main
restart: unless-stopped
n8n:
image: n8nio/n8n
environment:
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER=admin
- N8N_BASIC_AUTH_PASSWORD=admin123
- N8N_HOST=localhost
- N8N_PORT=5678
- WEBHOOK_URL=http://app:8000
- TZ=${TZ}
volumes:
- n8n_data:/home/node/.n8n
ports:
- "5678:5678"
depends_on:
- app
volumes:
postgres_data:
redis_data:
n8n_data:
```
### 2. Database Schema
```sql
-- PostgreSQL Schema
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE leagues (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(100) NOT NULL UNIQUE,
sport VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE games (
id VARCHAR(100) PRIMARY KEY,
league_id UUID REFERENCES leagues(id),
home_team VARCHAR(200) NOT NULL,
away_team VARCHAR(200) NOT NULL,
commence_time TIMESTAMP NOT NULL,
completed BOOLEAN DEFAULT FALSE,
home_score INTEGER,
away_score INTEGER,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE odds (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
game_id VARCHAR(100) REFERENCES games(id),
bookmaker VARCHAR(100) NOT NULL,
market_type VARCHAR(50) NOT NULL,
last_update TIMESTAMP NOT NULL,
home_odds DECIMAL(10,3),
away_odds DECIMAL(10,3),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE features (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
game_id VARCHAR(100) REFERENCES games(id),
feature_data JSONB NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE predictions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
game_id VARCHAR(100) REFERENCES games(id),
model_version VARCHAR(50) NOT NULL,
prob_home DECIMAL(5,4),
prob_away DECIMAL(5,4),
confidence DECIMAL(5,4),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE picks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
game_id VARCHAR(100) REFERENCES games(id),
prediction_id UUID REFERENCES predictions(id),
pick_type VARCHAR(50) NOT NULL,
pick_value VARCHAR(100) NOT NULL,
odds DECIMAL(10,3) NOT NULL,
bookmaker VARCHAR(100),
edge DECIMAL(5,4),
kelly_fraction DECIMAL(5,4),
stake_units DECIMAL(10,4),
confidence_tier CHAR(1),
sent_at TIMESTAMP,
result VARCHAR(20),
pnl DECIMAL(10,2),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE bankroll (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
balance DECIMAL(15,2) NOT NULL,
transaction_type VARCHAR(50),
amount DECIMAL(10,2),
description TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_games_commence ON games(commence_time);
CREATE INDEX idx_odds_game ON odds(game_id, last_update DESC);
CREATE INDEX idx_predictions_game ON predictions(game_id);
CREATE INDEX idx_picks_created ON picks(created_at DESC);
-- Initial data
INSERT INTO bankroll (balance, transaction_type, description)
VALUES (1000.00, 'INITIAL', 'Starting bankroll');
INSERT INTO leagues (name, sport) VALUES
('NFL', 'American Football'),
('NBA', 'Basketball'),
('MLB', 'Baseball'),
('NHL', 'Hockey');
```
---
## API Integration
### The Odds API Client
```python
import httpx
from typing import List, Dict
class OddsAPIClient:
def __init__(self):
self.api_key = "327b7a158175a6f471f8e41fa0acfe5f"
self.base_url = "https://api.the-odds-api.com/v4"
self.client = httpx.AsyncClient(timeout=30.0)
async def get_sports(self):
"""Get list of in-season sports"""
url = f"{self.base_url}/sports"
params = {"apiKey": self.api_key}
response = await self.client.get(url, params=params)
return response.json()
async def get_odds(self, sport: str, markets: List[str] = None):
"""Get odds for upcoming games"""
url = f"{self.base_url}/sports/{sport}/odds"
params = {
"apiKey": self.api_key,
"regions": "
- index.html +76 -8
- style.css +144 -18
|
@@ -1,19 +1,87 @@
|
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
<html>
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<meta name="viewport" content="width=device-width" />
|
| 6 |
-
<title>
|
| 7 |
<link rel="stylesheet" href="style.css" />
|
|
|
|
|
|
|
| 8 |
</head>
|
| 9 |
<body>
|
| 10 |
-
<div class="
|
| 11 |
-
<
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
</body>
|
| 19 |
</html>
|
|
|
|
| 1 |
+
|
| 2 |
<!doctype html>
|
| 3 |
<html>
|
| 4 |
<head>
|
| 5 |
<meta charset="utf-8" />
|
| 6 |
<meta name="viewport" content="width=device-width" />
|
| 7 |
+
<title>Sports Betting AI Dashboard</title>
|
| 8 |
<link rel="stylesheet" href="style.css" />
|
| 9 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 10 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 11 |
</head>
|
| 12 |
<body>
|
| 13 |
+
<div class="dashboard">
|
| 14 |
+
<header>
|
| 15 |
+
<h1>Sports Betting AI Dashboard</h1>
|
| 16 |
+
<nav>
|
| 17 |
+
<a href="/" class="active"><i class="fas fa-home"></i> Home</a>
|
| 18 |
+
<a href="/picks"><i class="fas fa-list"></i> Today's Picks</a>
|
| 19 |
+
<a href="/performance"><i class="fas fa-chart-line"></i> Performance</a>
|
| 20 |
+
<a href="/settings"><i class="fas fa-cog"></i> Settings</a>
|
| 21 |
+
</nav>
|
| 22 |
+
</header>
|
| 23 |
+
|
| 24 |
+
<div class="stats-grid">
|
| 25 |
+
<div class="stat-card">
|
| 26 |
+
<div class="stat-label">Bankroll</div>
|
| 27 |
+
<div class="stat-value">$1,250</div>
|
| 28 |
+
<div class="stat-change positive">β +12.5% this week</div>
|
| 29 |
+
</div>
|
| 30 |
+
<div class="stat-card">
|
| 31 |
+
<div class="stat-label">Win Rate</div>
|
| 32 |
+
<div class="stat-value">58.2%</div>
|
| 33 |
+
<div class="stat-change positive">β +3.2% from last month</div>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
|
| 37 |
+
<div class="content-section">
|
| 38 |
+
<h2>Recent Picks</h2>
|
| 39 |
+
<table class="picks-table">
|
| 40 |
+
<thead>
|
| 41 |
+
<tr>
|
| 42 |
+
<th>Game</th>
|
| 43 |
+
<th>Pick</th>
|
| 44 |
+
<th>Odds</th>
|
| 45 |
+
<th>Edge</th>
|
| 46 |
+
<th>Stake</th>
|
| 47 |
+
<th>Result</th>
|
| 48 |
+
</tr>
|
| 49 |
+
</thead>
|
| 50 |
+
<tbody id="picksTableBody">
|
| 51 |
+
<tr>
|
| 52 |
+
<td>Lakers vs Warriors</td>
|
| 53 |
+
<td>Lakers ML</td>
|
| 54 |
+
<td>+120</td>
|
| 55 |
+
<td>3.2%</td>
|
| 56 |
+
<td>$25</td>
|
| 57 |
+
<td class="win">W (+$30)</td>
|
| 58 |
+
</tr>
|
| 59 |
+
</tbody>
|
| 60 |
+
</table>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<div class="content-section">
|
| 64 |
+
<h2>Performance Trends</h2>
|
| 65 |
+
<canvas id="performanceChart"></canvas>
|
| 66 |
+
</div>
|
| 67 |
</div>
|
| 68 |
+
|
| 69 |
+
<script>
|
| 70 |
+
// Initialize chart
|
| 71 |
+
const ctx = document.getElementById('performanceChart').getContext('2d');
|
| 72 |
+
const performanceChart = new Chart(ctx, {
|
| 73 |
+
type: 'line',
|
| 74 |
+
data: {
|
| 75 |
+
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
|
| 76 |
+
datasets: [{
|
| 77 |
+
label: 'Bankroll',
|
| 78 |
+
data: [1000, 1050, 1150, 1200, 1250, 1300],
|
| 79 |
+
borderColor: 'rgba(75, 192, 192, 1)',
|
| 80 |
+
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
| 81 |
+
tension: 0.1
|
| 82 |
+
}]
|
| 83 |
+
}
|
| 84 |
+
});
|
| 85 |
+
</script>
|
| 86 |
</body>
|
| 87 |
</html>
|
|
@@ -1,28 +1,154 @@
|
|
|
|
|
|
|
|
| 1 |
body {
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
}
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
| 16 |
}
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
padding: 16px;
|
| 22 |
-
border: 1px solid lightgray;
|
| 23 |
-
border-radius: 16px;
|
| 24 |
}
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
```css
|
| 2 |
+
/* Base Styles */
|
| 3 |
body {
|
| 4 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 5 |
+
margin: 0;
|
| 6 |
+
padding: 0;
|
| 7 |
+
background: linear-gradient(135deg, #1a1a2e, #16213e);
|
| 8 |
+
color: #fff;
|
| 9 |
+
min-height: 100vh;
|
| 10 |
}
|
| 11 |
|
| 12 |
+
.dashboard {
|
| 13 |
+
max-width: 1200px;
|
| 14 |
+
margin: 0 auto;
|
| 15 |
+
padding: 20px;
|
| 16 |
}
|
| 17 |
|
| 18 |
+
/* Header Styles */
|
| 19 |
+
header {
|
| 20 |
+
display: flex;
|
| 21 |
+
justify-content: space-between;
|
| 22 |
+
align-items: center;
|
| 23 |
+
margin-bottom: 30px;
|
| 24 |
+
padding-bottom: 15px;
|
| 25 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
| 26 |
}
|
| 27 |
|
| 28 |
+
header h1 {
|
| 29 |
+
margin: 0;
|
| 30 |
+
font-size: 28px;
|
|
|
|
|
|
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
+
nav a {
|
| 34 |
+
color: #fff;
|
| 35 |
+
text-decoration: none;
|
| 36 |
+
margin-left: 20px;
|
| 37 |
+
font-size: 16px;
|
| 38 |
+
opacity: 0.7;
|
| 39 |
+
transition: opacity 0.3s;
|
| 40 |
}
|
| 41 |
+
|
| 42 |
+
nav a:hover {
|
| 43 |
+
opacity: 1;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
nav a.active {
|
| 47 |
+
opacity: 1;
|
| 48 |
+
font-weight: bold;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* Stat Cards */
|
| 52 |
+
.stats-grid {
|
| 53 |
+
display: grid;
|
| 54 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 55 |
+
gap: 20px;
|
| 56 |
+
margin-bottom: 30px;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.stat-card {
|
| 60 |
+
background: rgba(255, 255, 255, 0.1);
|
| 61 |
+
backdrop-filter: blur(10px);
|
| 62 |
+
border-radius: 10px;
|
| 63 |
+
padding: 20px;
|
| 64 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.stat-label {
|
| 68 |
+
font-size: 14px;
|
| 69 |
+
opacity: 0.7;
|
| 70 |
+
margin-bottom: 5px;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.stat-value {
|
| 74 |
+
font-size: 24px;
|
| 75 |
+
font-weight: bold;
|
| 76 |
+
margin-bottom: 5px;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.stat-change {
|
| 80 |
+
font-size: 12px;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.positive {
|
| 84 |
+
color: #4ade80;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.negative {
|
| 88 |
+
color: #f87171;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
/* Content Sections */
|
| 92 |
+
.content-section {
|
| 93 |
+
background: rgba(255, 255, 255, 0.05);
|
| 94 |
+
border-radius: 10px;
|
| 95 |
+
padding: 20px;
|
| 96 |
+
margin-bottom: 30px;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.content-section h2 {
|
| 100 |
+
margin-top: 0;
|
| 101 |
+
font-size: 20px;
|
| 102 |
+
margin-bottom: 15px;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
/* Tables */
|
| 106 |
+
.picks-table {
|
| 107 |
+
width: 100%;
|
| 108 |
+
border-collapse: collapse;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.picks-table th {
|
| 112 |
+
text-align: left;
|
| 113 |
+
padding: 10px;
|
| 114 |
+
font-size: 14px;
|
| 115 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.picks-table td {
|
| 119 |
+
padding: 10px;
|
| 120 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.win {
|
| 124 |
+
color: #4ade80;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.loss {
|
| 128 |
+
color: #f87171;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
/* Chart Container */
|
| 132 |
+
#performanceChart {
|
| 133 |
+
background: rgba(255, 255, 255, 0.05);
|
| 134 |
+
border-radius: 10px;
|
| 135 |
+
padding: 20px;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
/* Responsive Design */
|
| 139 |
+
@media (max-width: 768px) {
|
| 140 |
+
header {
|
| 141 |
+
flex-direction: column;
|
| 142 |
+
align-items: flex-start;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
nav {
|
| 146 |
+
margin-top: 15px;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
nav a {
|
| 150 |
+
margin-left: 0;
|
| 151 |
+
margin-right: 15px;
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
```
|