Spaces:
Running
Running
Commit
·
4856b42
1
Parent(s):
ff310f2
Deploy Signal Generator app
Browse files- src/main.py +10 -0
- src/orchestrator/coordinator.py +22 -31
- src/templates/index.html +6 -0
- src/templates/logs.html +143 -0
src/main.py
CHANGED
|
@@ -356,6 +356,16 @@ async def root(request: Request):
|
|
| 356 |
"""
|
| 357 |
return templates.TemplateResponse("index.html", {"request": request})
|
| 358 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
if __name__ == "__main__":
|
| 360 |
import uvicorn
|
| 361 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 356 |
"""
|
| 357 |
return templates.TemplateResponse("index.html", {"request": request})
|
| 358 |
|
| 359 |
+
@app.get("/logs", response_class=HTMLResponse)
|
| 360 |
+
async def view_logs(request: Request):
|
| 361 |
+
"""Serve the Logs Page"""
|
| 362 |
+
return templates.TemplateResponse("logs.html", {"request": request})
|
| 363 |
+
|
| 364 |
+
@app.get("/api/logs")
|
| 365 |
+
async def get_logs():
|
| 366 |
+
"""Get recent logs"""
|
| 367 |
+
return {"logs": list(log_buffer)}
|
| 368 |
+
|
| 369 |
if __name__ == "__main__":
|
| 370 |
import uvicorn
|
| 371 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
src/orchestrator/coordinator.py
CHANGED
|
@@ -1,18 +1,15 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Stock Alchemist Coordinator
|
| 3 |
-
Orchestrates the following tasks:
|
| 4 |
-
1. Continuous: News Scraper (runs constantly in a separate thread)
|
| 5 |
-
2. Daily at 08:00: Calendar Scraper (fetches today's events)
|
| 6 |
-
3. Weekly (Saturday): Fundamental Analysis (runs for all tickers)
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
import time
|
| 10 |
import threading
|
| 11 |
import sys
|
| 12 |
import schedule
|
|
|
|
| 13 |
from datetime import datetime, date
|
| 14 |
from pathlib import Path
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
# Add src and root to path to ensure imports work correctly
|
| 17 |
SRC_PATH = Path(__file__).parent.parent
|
| 18 |
ROOT_PATH = SRC_PATH.parent
|
|
@@ -26,8 +23,8 @@ try:
|
|
| 26 |
# Import run_saturday_analysis dynamically or add root to path
|
| 27 |
import run_saturday_analysis
|
| 28 |
except ImportError as e:
|
| 29 |
-
|
| 30 |
-
|
| 31 |
sys.exit(1)
|
| 32 |
|
| 33 |
class Coordinator:
|
|
@@ -38,50 +35,46 @@ class Coordinator:
|
|
| 38 |
|
| 39 |
def start_news_scraper(self):
|
| 40 |
"""Runs the news scraper in a separate daemon thread"""
|
| 41 |
-
|
| 42 |
try:
|
| 43 |
# Run news scraper in a separate thread since it runs constantly
|
| 44 |
self.news_thread = threading.Thread(target=run_news_scraper_main, daemon=True)
|
| 45 |
self.news_thread.start()
|
| 46 |
-
|
| 47 |
except Exception as e:
|
| 48 |
-
|
| 49 |
|
| 50 |
def run_calendar_scraper(self):
|
| 51 |
"""Runs the calendar scraper for today"""
|
| 52 |
-
|
| 53 |
try:
|
| 54 |
today = date.today()
|
| 55 |
events = get_events(today)
|
| 56 |
save_events_to_db(events, today)
|
| 57 |
-
|
| 58 |
except Exception as e:
|
| 59 |
-
|
| 60 |
|
| 61 |
def run_saturday_analysis_task(self):
|
| 62 |
"""Runs the Saturday analysis"""
|
| 63 |
-
|
| 64 |
try:
|
| 65 |
run_saturday_analysis.run_saturday_analysis()
|
| 66 |
-
|
| 67 |
except Exception as e:
|
| 68 |
-
|
| 69 |
|
| 70 |
def scheduler_loop(self):
|
| 71 |
"""Main scheduler loop"""
|
| 72 |
-
|
| 73 |
|
| 74 |
# Schedule Daily Calendar Scraper at 08:00
|
| 75 |
schedule.every().day.at("08:00").do(self.run_calendar_scraper)
|
| 76 |
-
|
| 77 |
|
| 78 |
# Schedule Saturday Analysis every Saturday
|
| 79 |
-
# Note: 'at' time is optional, defaulting to running when the script starts if it's Saturday?
|
| 80 |
-
# No, schedule.every().saturday.do(...) runs it every saturday.
|
| 81 |
-
# Let's pick a time, say 10:00 AM, or just let it run.
|
| 82 |
-
# The user said "every saturday run", let's assume a reasonable time like 09:00 AM
|
| 83 |
schedule.every().saturday.at("09:00").do(self.run_saturday_analysis_task)
|
| 84 |
-
|
| 85 |
|
| 86 |
while self.is_running:
|
| 87 |
schedule.run_pending()
|
|
@@ -90,14 +83,12 @@ class Coordinator:
|
|
| 90 |
def start(self):
|
| 91 |
"""Start the coordinator"""
|
| 92 |
self.is_running = True
|
| 93 |
-
|
| 94 |
|
| 95 |
# 1. Start News Scraper (Continuous)
|
| 96 |
self.start_news_scraper()
|
| 97 |
|
| 98 |
# 2. Start Scheduler (Blocking or Threaded?)
|
| 99 |
-
# Since news scraper is in a thread, we can run the scheduler in the main thread
|
| 100 |
-
# OR run scheduler in a thread and keep main thread for control.
|
| 101 |
# Let's run scheduler in main thread for simplicity as it just loops.
|
| 102 |
|
| 103 |
try:
|
|
@@ -107,9 +98,9 @@ class Coordinator:
|
|
| 107 |
|
| 108 |
def stop(self):
|
| 109 |
"""Stop the coordinator"""
|
| 110 |
-
|
| 111 |
self.is_running = False
|
| 112 |
-
|
| 113 |
|
| 114 |
if __name__ == "__main__":
|
| 115 |
coordinator = Coordinator()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import time
|
| 2 |
import threading
|
| 3 |
import sys
|
| 4 |
import schedule
|
| 5 |
+
import logging
|
| 6 |
from datetime import datetime, date
|
| 7 |
from pathlib import Path
|
| 8 |
|
| 9 |
+
# Setup logging
|
| 10 |
+
logger = logging.getLogger("coordinator")
|
| 11 |
+
logger.setLevel(logging.INFO)
|
| 12 |
+
|
| 13 |
# Add src and root to path to ensure imports work correctly
|
| 14 |
SRC_PATH = Path(__file__).parent.parent
|
| 15 |
ROOT_PATH = SRC_PATH.parent
|
|
|
|
| 23 |
# Import run_saturday_analysis dynamically or add root to path
|
| 24 |
import run_saturday_analysis
|
| 25 |
except ImportError as e:
|
| 26 |
+
logger.error(f"❌ Import Error: {e}")
|
| 27 |
+
logger.error("Ensure you are running from the project root or src directory.")
|
| 28 |
sys.exit(1)
|
| 29 |
|
| 30 |
class Coordinator:
|
|
|
|
| 35 |
|
| 36 |
def start_news_scraper(self):
|
| 37 |
"""Runs the news scraper in a separate daemon thread"""
|
| 38 |
+
logger.info("📰 Starting News Scraper...")
|
| 39 |
try:
|
| 40 |
# Run news scraper in a separate thread since it runs constantly
|
| 41 |
self.news_thread = threading.Thread(target=run_news_scraper_main, daemon=True)
|
| 42 |
self.news_thread.start()
|
| 43 |
+
logger.info("✅ News Scraper started")
|
| 44 |
except Exception as e:
|
| 45 |
+
logger.error(f"❌ Error starting News Scraper: {e}")
|
| 46 |
|
| 47 |
def run_calendar_scraper(self):
|
| 48 |
"""Runs the calendar scraper for today"""
|
| 49 |
+
logger.info(f"\n📅 Running Calendar Scraper for {date.today()}...")
|
| 50 |
try:
|
| 51 |
today = date.today()
|
| 52 |
events = get_events(today)
|
| 53 |
save_events_to_db(events, today)
|
| 54 |
+
logger.info("✅ Calendar Scraper finished")
|
| 55 |
except Exception as e:
|
| 56 |
+
logger.error(f"❌ Error running Calendar Scraper: {e}")
|
| 57 |
|
| 58 |
def run_saturday_analysis_task(self):
|
| 59 |
"""Runs the Saturday analysis"""
|
| 60 |
+
logger.info("\n📊 Running Saturday Fundamental Analysis...")
|
| 61 |
try:
|
| 62 |
run_saturday_analysis.run_saturday_analysis()
|
| 63 |
+
logger.info("✅ Saturday Analysis finished")
|
| 64 |
except Exception as e:
|
| 65 |
+
logger.error(f"❌ Error running Saturday Analysis: {e}")
|
| 66 |
|
| 67 |
def scheduler_loop(self):
|
| 68 |
"""Main scheduler loop"""
|
| 69 |
+
logger.info("⏰ Scheduler started")
|
| 70 |
|
| 71 |
# Schedule Daily Calendar Scraper at 08:00
|
| 72 |
schedule.every().day.at("08:00").do(self.run_calendar_scraper)
|
| 73 |
+
logger.info(" - Scheduled Calendar Scraper daily at 08:00")
|
| 74 |
|
| 75 |
# Schedule Saturday Analysis every Saturday
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
schedule.every().saturday.at("09:00").do(self.run_saturday_analysis_task)
|
| 77 |
+
logger.info(" - Scheduled Saturday Analysis every Saturday at 09:00")
|
| 78 |
|
| 79 |
while self.is_running:
|
| 80 |
schedule.run_pending()
|
|
|
|
| 83 |
def start(self):
|
| 84 |
"""Start the coordinator"""
|
| 85 |
self.is_running = True
|
| 86 |
+
logger.info("🚀 Stock Alchemist Coordinator Starting...")
|
| 87 |
|
| 88 |
# 1. Start News Scraper (Continuous)
|
| 89 |
self.start_news_scraper()
|
| 90 |
|
| 91 |
# 2. Start Scheduler (Blocking or Threaded?)
|
|
|
|
|
|
|
| 92 |
# Let's run scheduler in main thread for simplicity as it just loops.
|
| 93 |
|
| 94 |
try:
|
|
|
|
| 98 |
|
| 99 |
def stop(self):
|
| 100 |
"""Stop the coordinator"""
|
| 101 |
+
logger.info("\n🛑 Stopping Coordinator...")
|
| 102 |
self.is_running = False
|
| 103 |
+
logger.info("✅ Coordinator stopped")
|
| 104 |
|
| 105 |
if __name__ == "__main__":
|
| 106 |
coordinator = Coordinator()
|
src/templates/index.html
CHANGED
|
@@ -165,6 +165,12 @@
|
|
| 165 |
</button>
|
| 166 |
</div>
|
| 167 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
<!-- System Stats -->
|
| 169 |
<div class="card">
|
| 170 |
<h2>📊 System Status</h2>
|
|
|
|
| 165 |
</button>
|
| 166 |
</div>
|
| 167 |
|
| 168 |
+
<button class="btn" onclick="window.location.href='/logs'" style="flex: 1; background: #334155;">
|
| 169 |
+
View Logs
|
| 170 |
+
</button>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
|
| 174 |
<!-- System Stats -->
|
| 175 |
<div class="card">
|
| 176 |
<h2>📊 System Status</h2>
|
src/templates/logs.html
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Stock Alchemist | System Logs</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;500&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 8 |
+
<style>
|
| 9 |
+
:root {
|
| 10 |
+
--bg-color: #0F172A;
|
| 11 |
+
--card-bg: #1E293B;
|
| 12 |
+
--text-primary: #F8FAFC;
|
| 13 |
+
--text-secondary: #94A3B8;
|
| 14 |
+
--accent-green: #10B981;
|
| 15 |
+
--accent-blue: #3B82F6;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 19 |
+
|
| 20 |
+
body {
|
| 21 |
+
font-family: 'Outfit', sans-serif;
|
| 22 |
+
background-color: var(--bg-color);
|
| 23 |
+
color: var(--text-primary);
|
| 24 |
+
height: 100vh;
|
| 25 |
+
display: flex;
|
| 26 |
+
flex-direction: column;
|
| 27 |
+
padding: 2rem;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
header {
|
| 31 |
+
display: flex;
|
| 32 |
+
justify-content: space-between;
|
| 33 |
+
align-items: center;
|
| 34 |
+
margin-bottom: 1rem;
|
| 35 |
+
padding-bottom: 1rem;
|
| 36 |
+
border-bottom: 1px solid rgba(255,255,255,0.1);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
h1 { font-weight: 700; font-size: 1.5rem; }
|
| 40 |
+
|
| 41 |
+
.btn {
|
| 42 |
+
background: var(--card-bg);
|
| 43 |
+
color: var(--text-primary);
|
| 44 |
+
border: 1px solid rgba(255,255,255,0.1);
|
| 45 |
+
padding: 0.5rem 1rem;
|
| 46 |
+
border-radius: 6px;
|
| 47 |
+
cursor: pointer;
|
| 48 |
+
text-decoration: none;
|
| 49 |
+
font-size: 0.9rem;
|
| 50 |
+
transition: all 0.2s;
|
| 51 |
+
}
|
| 52 |
+
.btn:hover { background: #334155; }
|
| 53 |
+
|
| 54 |
+
#log-container {
|
| 55 |
+
flex: 1;
|
| 56 |
+
background: #000;
|
| 57 |
+
color: #d4d4d4;
|
| 58 |
+
font-family: 'Roboto Mono', monospace;
|
| 59 |
+
font-size: 0.85rem;
|
| 60 |
+
padding: 1rem;
|
| 61 |
+
border-radius: 8px;
|
| 62 |
+
overflow-y: auto;
|
| 63 |
+
border: 1px solid rgba(255,255,255,0.1);
|
| 64 |
+
white-space: pre-wrap;
|
| 65 |
+
box-shadow: inset 0 0 20px rgba(0,0,0,0.5);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.log-entry { margin-bottom: 2px; }
|
| 69 |
+
.log-info { color: #d4d4d4; }
|
| 70 |
+
.log-error { color: #f87171; }
|
| 71 |
+
.log-warning { color: #fbbf24; }
|
| 72 |
+
|
| 73 |
+
.controls {
|
| 74 |
+
display: flex;
|
| 75 |
+
gap: 1rem;
|
| 76 |
+
margin-bottom: 1rem;
|
| 77 |
+
align-items: center;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
#status-dot {
|
| 81 |
+
width: 8px; height: 8px;
|
| 82 |
+
border-radius: 50%;
|
| 83 |
+
background: var(--accent-green);
|
| 84 |
+
display: inline-block;
|
| 85 |
+
margin-right: 5px;
|
| 86 |
+
}
|
| 87 |
+
</style>
|
| 88 |
+
</head>
|
| 89 |
+
<body>
|
| 90 |
+
|
| 91 |
+
<header>
|
| 92 |
+
<div style="display:flex; align-items:center; gap:1rem;">
|
| 93 |
+
<h1>📜 System Logs</h1>
|
| 94 |
+
<span style="font-size: 0.9rem; color: var(--text-secondary);"><span id="status-dot"></span>Live</span>
|
| 95 |
+
</div>
|
| 96 |
+
<a href="/" class="btn">← Back to Dashboard</a>
|
| 97 |
+
</header>
|
| 98 |
+
|
| 99 |
+
<div class="controls">
|
| 100 |
+
<button class="btn" onclick="fetchLogs()">Refresh Now</button>
|
| 101 |
+
<label style="font-size: 0.9rem; display:flex; align-items:center; gap:0.5rem; cursor:pointer;">
|
| 102 |
+
<input type="checkbox" id="autoScroll" checked> Auto-scroll
|
| 103 |
+
</label>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<div id="log-container">Loading logs...</div>
|
| 107 |
+
|
| 108 |
+
<script>
|
| 109 |
+
const container = document.getElementById('log-container');
|
| 110 |
+
let autoScroll = true;
|
| 111 |
+
|
| 112 |
+
document.getElementById('autoScroll').addEventListener('change', (e) => {
|
| 113 |
+
autoScroll = e.target.checked;
|
| 114 |
+
});
|
| 115 |
+
|
| 116 |
+
async function fetchLogs() {
|
| 117 |
+
try {
|
| 118 |
+
const res = await fetch('/api/logs');
|
| 119 |
+
const data = await res.json();
|
| 120 |
+
|
| 121 |
+
if (data.logs && data.logs.length > 0) {
|
| 122 |
+
container.innerHTML = data.logs.map(line => {
|
| 123 |
+
let cls = 'log-info';
|
| 124 |
+
if (line.includes('ERROR') || line.includes('❌')) cls = 'log-error';
|
| 125 |
+
else if (line.includes('WARNING') || line.includes('⚠️')) cls = 'log-warning';
|
| 126 |
+
return `<div class="log-entry ${cls}">${line}</div>`;
|
| 127 |
+
}).join('');
|
| 128 |
+
|
| 129 |
+
if (autoScroll) {
|
| 130 |
+
container.scrollTop = container.scrollHeight;
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
} catch (e) {
|
| 134 |
+
console.error("Failed to fetch logs", e);
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
// Poll every 3 seconds
|
| 139 |
+
fetchLogs();
|
| 140 |
+
setInterval(fetchLogs, 3000);
|
| 141 |
+
</script>
|
| 142 |
+
</body>
|
| 143 |
+
</html>
|