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