Init Of work
This commit is contained in:
parent
d0fc01b98f
commit
96e6ae3bfc
|
|
@ -0,0 +1,4 @@
|
|||
.env
|
||||
/__pycache__
|
||||
/__pycache__
|
||||
*.pyc
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lua",
|
||||
"request": "launch",
|
||||
"name": "Debug",
|
||||
"program": "${workspaceFolder}/Untitled-1.lua"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
README.md
17
README.md
|
|
@ -1,3 +1,16 @@
|
|||
# STONKS
|
||||
# stonk GUI App
|
||||
|
||||
Dev for handling stonk data so i can figure all this out web wise.
|
||||
## Setup
|
||||
|
||||
1. Install dependencies:
|
||||
pip install -r requirements.txt
|
||||
|
||||
2. Update .env with your Authentik details
|
||||
|
||||
3. Update DB connection string in main.py
|
||||
|
||||
4. Run:
|
||||
uvicorn main:app --reload
|
||||
|
||||
5. Open:
|
||||
http://localhost:8000
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy the rest of your code (do not copy .env)
|
||||
COPY . .
|
||||
|
||||
# Expose port if web app
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["python", "main.py"]
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
import os
|
||||
from fastapi import FastAPI, Depends, Query
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse
|
||||
from sqlalchemy import create_engine, text
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
DB_USER = os.getenv("DB_USER")
|
||||
DB_PASS = os.getenv("DB_PASS")
|
||||
DB_HOST = os.getenv("DB_HOST")
|
||||
DB_NAME = os.getenv("DB_NAME")
|
||||
|
||||
if not all([DB_USER, DB_PASS, DB_HOST, DB_NAME]):
|
||||
raise EnvironmentError("Missing DB environment variables")
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
engine = create_engine(
|
||||
f"mysql+pymysql://{DB_USER}:{DB_PASS}@{DB_HOST}/{DB_NAME}",
|
||||
pool_pre_ping=True
|
||||
)
|
||||
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
|
||||
# -----------------------
|
||||
# PAGE 1: DEFAULT /
|
||||
# -----------------------
|
||||
@app.get("/")
|
||||
async def index():
|
||||
with open("templates/index.html", "r", encoding="utf-8") as f:
|
||||
return HTMLResponse(f.read())
|
||||
|
||||
@app.get("/embed")
|
||||
async def index():
|
||||
with open("templates/embed.html", "r", encoding="utf-8") as f:
|
||||
return HTMLResponse(f.read())
|
||||
|
||||
# -----------------------
|
||||
# PAGE 2: /graphs
|
||||
# -----------------------
|
||||
@app.get("/graphs")
|
||||
async def graphs():
|
||||
with open("templates/graphs.html", "r", encoding="utf-8") as f:
|
||||
return HTMLResponse(f.read())
|
||||
|
||||
|
||||
# -----------------------
|
||||
# API 1 (used by /)
|
||||
# -----------------------
|
||||
@app.get("/api/data")
|
||||
async def api_data():
|
||||
query = text("SELECT * FROM STOCKMARKET_HISTORY WHERE Ticker = 'FAT' ORDER BY ChangeID DESC LIMIT 28")
|
||||
|
||||
with engine.connect() as conn:
|
||||
result = conn.execute(query)
|
||||
rows = [dict(r._mapping) for r in result]
|
||||
|
||||
return {
|
||||
"columns": list(rows[0].keys()) if rows else [],
|
||||
"rows": rows
|
||||
}
|
||||
|
||||
|
||||
# -----------------------
|
||||
# API 2 (used by /graphs)
|
||||
# -----------------------
|
||||
from fastapi import Depends, Query
|
||||
|
||||
@app.get("/api/graphs")
|
||||
async def api_graphs(
|
||||
ticker: str = Query(...),
|
||||
limit: int = Query(28),
|
||||
):
|
||||
tickers = [t.strip().upper() for t in ticker.split(",") if t.strip()]
|
||||
|
||||
with engine.connect() as conn:
|
||||
results = {}
|
||||
|
||||
for t in tickers:
|
||||
query = text("""
|
||||
SELECT
|
||||
h.*,
|
||||
s.CompanyName
|
||||
FROM STOCKMARKET_HISTORY h
|
||||
JOIN STOCKMARKET_STOCKS s
|
||||
ON s.Ticker = h.Ticker
|
||||
WHERE h.Ticker = :ticker
|
||||
ORDER BY h.ChangeID DESC
|
||||
LIMIT :limit;
|
||||
""")
|
||||
|
||||
result = conn.execute(query, {
|
||||
"ticker": t,
|
||||
"limit": limit * 4
|
||||
})
|
||||
|
||||
rows = [dict(r._mapping) for r in result]
|
||||
results[t] = rows
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# -----------------------
|
||||
# RUN
|
||||
# -----------------------
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
fastapi
|
||||
uvicorn
|
||||
sqlalchemy
|
||||
pymysql
|
||||
python-jose[cryptography]
|
||||
authlib
|
||||
httpx
|
||||
python-dotenv
|
||||
itsdangerous
|
||||
cryptography
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #ffffff;
|
||||
color: #ffffff;
|
||||
font-family: Arial, sans-serif;
|
||||
overflow: hidden; /* prevents page scroll messing with chart height */
|
||||
}
|
||||
|
||||
/* Header area */
|
||||
h2 {
|
||||
margin: 10px 15px 5px 15px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #4da3ff;
|
||||
margin: 0 15px 10px 15px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* FULL SCREEN CHART CONTAINER */
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 80px); /* subtract header space */
|
||||
display: flex;
|
||||
padding: 10px 15px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* CRITICAL: canvas must stretch */
|
||||
#chart {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Dashboard</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h2>Main Dashboard</h2>
|
||||
|
||||
<iframe
|
||||
src="/graphs?ticker=FAT,ALB,TSLA&limit=28"
|
||||
style="width:33%; height:500px; border:0;"
|
||||
></iframe>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Graphs</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h2 id="title">Graphs</h2>
|
||||
<a href="/">Back</a>
|
||||
|
||||
<div class="chart-container">
|
||||
<canvas id="chart"></canvas>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function getParams() {
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
return {
|
||||
tickers: url.searchParams.get("ticker"),
|
||||
limit: parseInt(url.searchParams.get("limit") || 28)
|
||||
};
|
||||
}
|
||||
|
||||
async function loadGraph() {
|
||||
const { tickers, limit } = getParams();
|
||||
|
||||
if (!tickers) {
|
||||
document.body.innerHTML = "<h3>Missing ?ticker= parameter</h3>";
|
||||
return;
|
||||
}
|
||||
|
||||
const tickerList = tickers.split(",").map(t => t.trim());
|
||||
|
||||
const res = await fetch(
|
||||
`/api/graphs?ticker=${tickers}&limit=${limit}`
|
||||
);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
const datasets = [];
|
||||
let basePrices = [];
|
||||
|
||||
tickerList.forEach((ticker, index) => {
|
||||
|
||||
const raw = data[ticker] || [];
|
||||
|
||||
if (raw.length === 0) return;
|
||||
|
||||
// Company name comes from FIRST row
|
||||
const companyName = `${raw[0].CompanyName} (${ticker})` || ticker;
|
||||
|
||||
const prices = raw
|
||||
.map(r => parseFloat(r.NewPrice))
|
||||
.reverse();
|
||||
|
||||
if (index === 0) basePrices = prices;
|
||||
|
||||
const color = `hsl(${(index * 360) / tickerList.length}, 70%, 50%)`;
|
||||
|
||||
const pointColors = prices.map((_, i) =>
|
||||
i % 4 === 0 ? "rgba(255, 99, 132, 1)" : color
|
||||
);
|
||||
|
||||
const pointSizes = prices.map((_, i) =>
|
||||
i % 4 === 0 ? 5 : 3
|
||||
);
|
||||
|
||||
datasets.push({
|
||||
label: companyName,
|
||||
data: prices,
|
||||
|
||||
borderColor: color,
|
||||
backgroundColor: color,
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
|
||||
pointBackgroundColor: pointColors,
|
||||
pointBorderColor: pointColors,
|
||||
pointRadius: pointSizes,
|
||||
pointHoverRadius: 6
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------
|
||||
// X AXIS LABELS (14 → 1 numeric)
|
||||
// -----------------------------
|
||||
const labels = [];
|
||||
const totalDays = Math.ceil(basePrices.length / 4);
|
||||
let dayCounter = totalDays;
|
||||
|
||||
for (let i = 0; i < basePrices.length; i++) {
|
||||
if (i % 4 === 0) {
|
||||
labels.push(String(dayCounter));
|
||||
dayCounter--;
|
||||
} else {
|
||||
labels.push("");
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// CHART
|
||||
// -----------------------------
|
||||
new Chart(document.getElementById("chart"), {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: datasets
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return `${context.dataset.label}: $${context.parsed.y}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: true
|
||||
}
|
||||
},
|
||||
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: "Days"
|
||||
},
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
maxRotation: 0
|
||||
}
|
||||
},
|
||||
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: "Price ($)"
|
||||
},
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return `$${value}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadGraph();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Dashboard</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h2>Main Dashboard</h2>
|
||||
|
||||
<a href="/graphs">Go to Graphs</a>
|
||||
|
||||
<table id="table"></table>
|
||||
|
||||
<script>
|
||||
async function load() {
|
||||
const res = await fetch("/api/data");
|
||||
const data = await res.json();
|
||||
|
||||
const table = document.getElementById("table");
|
||||
|
||||
if (!data.rows.length) {
|
||||
table.innerHTML = "<tr><td>No data</td></tr>";
|
||||
return;
|
||||
}
|
||||
|
||||
const cols = data.columns;
|
||||
|
||||
let html = "<tr>" +
|
||||
cols.map(c => `<th>${c}</th>`).join("") +
|
||||
"</tr>";
|
||||
|
||||
html += data.rows.map(row =>
|
||||
"<tr>" +
|
||||
cols.map(c => `<td>${row[c]}</td>`).join("") +
|
||||
"</tr>"
|
||||
).join("");
|
||||
|
||||
table.innerHTML = html;
|
||||
}
|
||||
|
||||
load();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue