Papaflessas commited on
Commit
18b3eb0
·
1 Parent(s): f4ede1d

Deploy Signal Generator app

Browse files
.env.example ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # API Security
2
+ API_SECRET=change_this_to_a_secure_random_string
3
+
4
+ # Database Configuration (TiDB / MySQL)
5
+ DB_HOST=gateway01.us-west-2.prod.aws.tidbcloud.com
6
+ DB_PORT=4000
7
+ DB_USERNAME=your_username
8
+ DB_PASSWORD=your_password
9
+ DB_DATABASE=gotti
10
+ DB_SSL_CA=src/db/isrgrootx1.pem
requirements.txt CHANGED
@@ -18,4 +18,5 @@ schedule
18
  fastapi
19
  uvicorn
20
  pydantic
21
- openai
 
 
18
  fastapi
19
  uvicorn
20
  pydantic
21
+ openai
22
+ jinja2
src/db/local_database.py CHANGED
@@ -843,6 +843,43 @@ class LocalDatabase:
843
  cursor.close()
844
  if 'conn' in locals():
845
  conn.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
846
 
847
  def get(self, date_str: str, data_type: str, ticker: str) -> Optional[DatabaseEntry]:
848
  """
 
843
  cursor.close()
844
  if 'conn' in locals():
845
  conn.close()
846
+
847
+ def get_recent_signals(self, limit: int = 50) -> List[Dict]:
848
+ """Get most recent signals"""
849
+ try:
850
+ conn = self._create_connection()
851
+ if not conn:
852
+ return []
853
+ cursor = conn.cursor(dictionary=True)
854
+
855
+ cursor.execute('''
856
+ SELECT * FROM signals
857
+ ORDER BY created_at DESC
858
+ LIMIT %s
859
+ ''', (limit,))
860
+
861
+ results = cursor.fetchall()
862
+
863
+ parsed_results = []
864
+ for result in results:
865
+ # Parse JSON fields
866
+ for field in ['calendar_event_keys', 'news_keys', 'metadata', 'sentiment']:
867
+ if result.get(field) and isinstance(result[field], str):
868
+ try:
869
+ result[field] = json.loads(result[field])
870
+ except:
871
+ pass
872
+ parsed_results.append(result)
873
+
874
+ return parsed_results
875
+ except Error as e:
876
+ print(f"❌ Error getting recent signals: {e}")
877
+ return []
878
+ finally:
879
+ if 'cursor' in locals():
880
+ cursor.close()
881
+ if 'conn' in locals():
882
+ conn.close()
883
 
884
  def get(self, date_str: str, data_type: str, ticker: str) -> Optional[DatabaseEntry]:
885
  """
src/main.py CHANGED
@@ -200,6 +200,38 @@ async def health_check():
200
  logger.info(f"Health Check: {vitals}")
201
  return vitals
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  if __name__ == "__main__":
204
  import uvicorn
205
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
200
  logger.info(f"Health Check: {vitals}")
201
  return vitals
202
 
203
+ from fastapi import FastAPI, HTTPException, Header, BackgroundTasks, Depends, Request
204
+ from fastapi.responses import HTMLResponse
205
+ from fastapi.templating import Jinja2Templates
206
+ from fastapi.staticfiles import StaticFiles
207
+
208
+ # ... imports ...
209
+
210
+ app = FastAPI(title="Stock Alchemist Signal Generator")
211
+
212
+ # Setup templates
213
+ templates = Jinja2Templates(directory="src/templates")
214
+
215
+ # ... existing code ...
216
+
217
+ @app.get("/api/signals", dependencies=[Depends(verify_api_secret)])
218
+ async def get_signals():
219
+ """Get recent signals"""
220
+ try:
221
+ db = LocalDatabase()
222
+ signals = db.get_recent_signals(limit=50)
223
+ return signals
224
+ except Exception as e:
225
+ logger.error(f"Error fetching signals: {e}")
226
+ raise HTTPException(status_code=500, detail="Database error")
227
+
228
+ @app.get("/", response_class=HTMLResponse)
229
+ async def root(request: Request):
230
+ """
231
+ Serve the Home Screen Dashboard
232
+ """
233
+ return templates.TemplateResponse("index.html", {"request": request})
234
+
235
  if __name__ == "__main__":
236
  import uvicorn
237
  uvicorn.run(app, host="0.0.0.0", port=7860)
src/templates/index.html ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 | Signal Generator</title>
7
+ <link href="https://fonts.googleapis.com/css2?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-red: #EF4444;
16
+ --accent-yellow: #F59E0B;
17
+ --accent-blue: #3B82F6;
18
+ }
19
+
20
+ * { margin: 0; padding: 0; box-sizing: border-box; }
21
+
22
+ body {
23
+ font-family: 'Outfit', sans-serif;
24
+ background-color: var(--bg-color);
25
+ color: var(--text-primary);
26
+ min-height: 100vh;
27
+ padding: 2rem;
28
+ }
29
+
30
+ .container {
31
+ max-width: 1200px;
32
+ margin: 0 auto;
33
+ }
34
+
35
+ header {
36
+ display: flex;
37
+ justify-content: space-between;
38
+ align-items: center;
39
+ margin-bottom: 2rem;
40
+ padding-bottom: 1rem;
41
+ border-bottom: 1px solid rgba(255,255,255,0.1);
42
+ }
43
+
44
+ h1 { font-weight: 700; font-size: 1.8rem; background: linear-gradient(to right, var(--accent-blue), var(--accent-green)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
45
+
46
+ .status-badge {
47
+ padding: 0.5rem 1rem;
48
+ border-radius: 20px;
49
+ font-size: 0.875rem;
50
+ font-weight: 500;
51
+ background: rgba(16, 185, 129, 0.2);
52
+ color: var(--accent-green);
53
+ }
54
+
55
+ .actions {
56
+ display: grid;
57
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
58
+ gap: 1rem;
59
+ margin-bottom: 2rem;
60
+ }
61
+
62
+ .card {
63
+ background: var(--card-bg);
64
+ border-radius: 12px;
65
+ padding: 1.5rem;
66
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
67
+ border: 1px solid rgba(255,255,255,0.05);
68
+ }
69
+
70
+ h2 { font-size: 1.1rem; margin-bottom: 1rem; color: var(--text-secondary); }
71
+
72
+ .btn {
73
+ display: inline-flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ width: 100%;
77
+ padding: 0.75rem 1rem;
78
+ background: var(--accent-blue);
79
+ color: white;
80
+ border: none;
81
+ border-radius: 8px;
82
+ font-weight: 600;
83
+ cursor: pointer;
84
+ transition: all 0.2s;
85
+ text-decoration: none;
86
+ font-size: 0.95rem;
87
+ }
88
+
89
+ .btn:hover { filter: brightness(1.1); transform: translateY(-1px); }
90
+ .btn:disabled { opacity: 0.7; cursor: not-allowed; }
91
+ .btn-green { background: var(--accent-green); }
92
+ .btn-purple { background: #8B5CF6; }
93
+
94
+ input[type="text"] {
95
+ width: 100%;
96
+ padding: 0.75rem;
97
+ border-radius: 8px;
98
+ border: 1px solid #334155;
99
+ background: #0F172A;
100
+ color: white;
101
+ margin-bottom: 1rem;
102
+ font-family: inherit;
103
+ }
104
+ input:focus { outline: 2px solid var(--accent-blue); border-color: transparent; }
105
+
106
+ .signals-table {
107
+ width: 100%;
108
+ border-collapse: collapse;
109
+ font-size: 0.95rem;
110
+ }
111
+
112
+ .signals-table th {
113
+ text-align: left;
114
+ padding: 1rem;
115
+ color: var(--text-secondary);
116
+ font-weight: 600;
117
+ border-bottom: 1px solid rgba(255,255,255,0.1);
118
+ }
119
+
120
+ .signals-table td {
121
+ padding: 1rem;
122
+ border-bottom: 1px solid rgba(255,255,255,0.05);
123
+ }
124
+
125
+ .tag {
126
+ padding: 0.25rem 0.75rem;
127
+ border-radius: 12px;
128
+ font-size: 0.75rem;
129
+ font-weight: 600;
130
+ display: inline-block;
131
+ }
132
+
133
+ .tag-buy { background: rgba(16, 185, 129, 0.2); color: var(--accent-green); }
134
+ .tag-sell { background: rgba(239, 68, 68, 0.2); color: var(--accent-red); }
135
+ .tag-hold { background: rgba(245, 158, 11, 0.2); color: var(--accent-yellow); }
136
+
137
+ .empty-state {
138
+ text-align: center;
139
+ padding: 3rem;
140
+ color: var(--text-secondary);
141
+ }
142
+
143
+ /* Modal for secrets */
144
+ #secretModal {
145
+ display: flex;
146
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
147
+ background: rgba(0,0,0,0.8);
148
+ justify-content: center; align-items: center;
149
+ z-index: 100;
150
+ }
151
+ </style>
152
+ </head>
153
+ <body>
154
+
155
+ <div id="secretModal">
156
+ <div class="card" style="width: 400px; text-align: center;">
157
+ <h2>🔐 Enter API Secret</h2>
158
+ <p style="margin-bottom: 1rem; color: var(--text-secondary); font-size: 0.9rem;">Required to perform actions.</p>
159
+ <input type="password" id="apiSecretInput" placeholder="API_SECRET" />
160
+ <button class="btn" onclick="saveSecret()">Access Dashboard</button>
161
+ </div>
162
+ </div>
163
+
164
+ <div class="container" id="mainContent" style="display:none; filter: blur(5px);">
165
+ <header>
166
+ <h1>🧪 Stock Alchemist</h1>
167
+ <div class="status-badge" id="systemStatus">● System Online</div>
168
+ </header>
169
+
170
+ <div class="actions">
171
+ <!-- Generate Signal Card -->
172
+ <div class="card">
173
+ <h2>⚡ Generate Signal</h2>
174
+ <input type="text" id="tickerInput" placeholder="Enter Ticker (e.g. AAPL)" />
175
+ <button class="btn btn-green" onclick="generateSignal()" id="genBtn">
176
+ Generate AI Signal
177
+ </button>
178
+ </div>
179
+
180
+ <!-- System Stats -->
181
+ <div class="card">
182
+ <h2>📊 System Status</h2>
183
+ <div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
184
+ <span style="color:var(--text-secondary)">Database</span>
185
+ <span id="dbStatus">Checking...</span>
186
+ </div>
187
+ <div style="display: flex; justify-content: space-between; margin-bottom: 1.5rem;">
188
+ <span style="color:var(--text-secondary)">Ollama</span>
189
+ <span id="ollamaStatus">Checking...</span>
190
+ </div>
191
+ <button class="btn btn-purple" onclick="runAnalysis()">
192
+ Run Weekly Analysis
193
+ </button>
194
+ </div>
195
+ </div>
196
+
197
+ <!-- Recent Signals -->
198
+ <div class="card">
199
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
200
+ <h2>📡 Recent Signals</h2>
201
+ <button class="btn" style="width:auto; padding: 0.5rem 1rem; font-size: 0.8rem; background: #334155;" onclick="loadSignals()">Refresh</button>
202
+ </div>
203
+
204
+ <table class="signals-table">
205
+ <thead>
206
+ <tr>
207
+ <th>Ticker</th>
208
+ <th>Date</th>
209
+ <th>Signal</th>
210
+ <th>Sentiment</th>
211
+ <th>Time (UTC)</th>
212
+ </tr>
213
+ </thead>
214
+ <tbody id="signalsBody">
215
+ <tr><td colspan="5" class="empty-state">Loading signals...</td></tr>
216
+ </tbody>
217
+ </table>
218
+ </div>
219
+ </div>
220
+
221
+ <script>
222
+ let API_SECRET = localStorage.getItem('api_secret');
223
+
224
+ function saveSecret() {
225
+ const input = document.getElementById('apiSecretInput').value;
226
+ if (input) {
227
+ localStorage.setItem('api_secret', input);
228
+ API_SECRET = input;
229
+ document.getElementById('secretModal').style.display = 'none';
230
+ document.getElementById('mainContent').style.display = 'block';
231
+ document.getElementById('mainContent').style.filter = 'none';
232
+ init();
233
+ }
234
+ }
235
+
236
+ if (API_SECRET) {
237
+ document.getElementById('secretModal').style.display = 'none';
238
+ document.getElementById('mainContent').style.display = 'block';
239
+ document.getElementById('mainContent').style.filter = 'none';
240
+ init();
241
+ }
242
+
243
+ async function init() {
244
+ checkHealth();
245
+ loadSignals();
246
+ }
247
+
248
+ async function checkHealth() {
249
+ try {
250
+ const res = await fetch('/health');
251
+ const data = await res.json();
252
+
253
+ const dbEl = document.getElementById('dbStatus');
254
+ const ollamaEl = document.getElementById('ollamaStatus');
255
+
256
+ dbEl.innerText = data.database === 'connected' ? 'Connected' : 'Disconnected';
257
+ dbEl.style.color = data.database === 'connected' ? '#10B981' : '#EF4444';
258
+
259
+ ollamaEl.innerText = data.ollama === 'running' ? 'Active' : 'Offline';
260
+ ollamaEl.style.color = data.ollama === 'running' ? '#10B981' : '#EF4444';
261
+ } catch (e) {
262
+ console.error(e);
263
+ }
264
+ }
265
+
266
+ async function loadSignals() {
267
+ const tbody = document.getElementById('signalsBody');
268
+ try {
269
+ const res = await fetch('/api/signals', {
270
+ headers: { 'X-API-Secret': API_SECRET }
271
+ });
272
+ if (!res.ok) throw new Error('Auth failed');
273
+
274
+ const signals = await res.json();
275
+
276
+ if (signals.length === 0) {
277
+ tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No signals generated yet.</td></tr>';
278
+ return;
279
+ }
280
+
281
+ tbody.innerHTML = signals.map(s => {
282
+ const pos = s.signal_position || 'HOLD';
283
+ const cls = pos === 'BUY' ? 'tag-buy' : (pos === 'SELL' ? 'tag-sell' : 'tag-hold');
284
+ const sentiment = s.sentiment ? (s.sentiment.confidence ? `${(s.sentiment.confidence * 100).toFixed(0)}%` : '-') : '-';
285
+ // Try format date
286
+ const date = new Date(s.created_at).toLocaleString();
287
+
288
+ return `
289
+ <tr>
290
+ <td style="font-weight:600; color:white;">${s.ticker}</td>
291
+ <td>${s.signal_date}</td>
292
+ <td><span class="tag ${cls}">${pos}</span></td>
293
+ <td>${sentiment}</td>
294
+ <td style="color:var(--text-secondary); font-size: 0.85rem;">${date}</td>
295
+ </tr>
296
+ `;
297
+ }).join('');
298
+
299
+ } catch (e) {
300
+ tbody.innerHTML = '<tr><td colspan="5" class="empty-state" style="color:#EF4444">Failed to load signals. Check API Secret.</td></tr>';
301
+ }
302
+ }
303
+
304
+ async function generateSignal() {
305
+ const ticker = document.getElementById('tickerInput').value.toUpperCase();
306
+ const btn = document.getElementById('genBtn');
307
+ if (!ticker) return alert('Please enter a ticker');
308
+
309
+ btn.disabled = true;
310
+ btn.innerText = 'Generating...';
311
+
312
+ try {
313
+ const res = await fetch('/generate-signal', {
314
+ method: 'POST',
315
+ headers: {
316
+ 'Content-Type': 'application/json',
317
+ 'X-API-Secret': API_SECRET
318
+ },
319
+ body: JSON.stringify({ ticker: ticker })
320
+ });
321
+
322
+ if (res.ok) {
323
+ alert(`Signal generation started for ${ticker}. Check back in a few seconds.`);
324
+ setTimeout(loadSignals, 2000);
325
+ } else {
326
+ const err = await res.json();
327
+ alert('Error: ' + err.detail);
328
+ }
329
+ } catch (e) {
330
+ alert('Request failed');
331
+ } finally {
332
+ btn.disabled = false;
333
+ btn.innerText = 'Generate AI Signal';
334
+ }
335
+ }
336
+
337
+ async function runAnalysis() {
338
+ if (!confirm('Run Saturday Analysis? This is a heavy background task.')) return;
339
+
340
+ try {
341
+ const res = await fetch('/saturday-analysis', {
342
+ method: 'POST',
343
+ headers: { 'X-API-Secret': API_SECRET }
344
+ });
345
+ alert('Analysis started in background.');
346
+ } catch (e) {
347
+ alert('Failed to start analysis');
348
+ }
349
+ }
350
+ </script>
351
+ </body>
352
+ </html>