add last sync time in index, fix csv file caching time in fetching new data

This commit is contained in:
2026-02-06 03:56:55 +08:00
parent 2d9fa9a47b
commit c0f158684f
29 changed files with 53379 additions and 704 deletions
Binary file not shown.
+1259
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4
View File
@@ -5293,3 +5293,7 @@ date,close
2026-01-27,37.18 2026-01-27,37.18
2026-01-28,37.23 2026-01-28,37.23
2026-01-29,37.2 2026-01-29,37.2
2026-01-30,37.11
2026-02-02,36.99
2026-02-03,37.17
2026-02-04,37.12
1 date close
5293 2026-01-27 37.18
5294 2026-01-28 37.23
5295 2026-01-29 37.2
5296 2026-01-30 37.11
5297 2026-02-02 36.99
5298 2026-02-03 37.17
5299 2026-02-04 37.12
+1259
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
date,close
2026-01-05,22.87
2026-01-06,22.77
2026-01-07,22.61
2026-01-08,22.56
2026-01-09,22.49
2026-01-12,22.54
2026-01-13,22.66
2026-01-14,22.66
2026-01-15,22.93
2026-01-16,22.9
2026-01-19,23.01
2026-01-20,23.0
2026-01-21,22.66
2026-01-22,23.02
2026-01-23,23.25
2026-01-26,23.34
2026-01-27,23.04
2026-01-28,21.99
2026-01-29,21.97
2026-01-30,22.46
2026-02-02,22.51
2026-02-03,22.78
2026-02-04,22.85
1 date close
2 2026-01-05 22.87
3 2026-01-06 22.77
4 2026-01-07 22.61
5 2026-01-08 22.56
6 2026-01-09 22.49
7 2026-01-12 22.54
8 2026-01-13 22.66
9 2026-01-14 22.66
10 2026-01-15 22.93
11 2026-01-16 22.9
12 2026-01-19 23.01
13 2026-01-20 23.0
14 2026-01-21 22.66
15 2026-01-22 23.02
16 2026-01-23 23.25
17 2026-01-26 23.34
18 2026-01-27 23.04
19 2026-01-28 21.99
20 2026-01-29 21.97
21 2026-01-30 22.46
22 2026-02-02 22.51
23 2026-02-03 22.78
24 2026-02-04 22.85
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3
View File
@@ -4365,3 +4365,6 @@ date,close
2026-01-28,28.72 2026-01-28,28.72
2026-01-29,29.1 2026-01-29,29.1
2026-01-30,28.99 2026-01-30,28.99
2026-02-02,27.73
2026-02-03,28.9
2026-02-04,29.0
1 date close
4365 2026-01-28 28.72
4366 2026-01-29 29.1
4367 2026-01-30 28.99
4368 2026-02-02 27.73
4369 2026-02-03 28.9
4370 2026-02-04 29.0
@@ -4343,3 +4343,11 @@ date,close
2026-01-22,279.27 2026-01-22,279.27
2026-01-23,279.99 2026-01-23,279.99
2026-01-26,283.53 2026-01-26,283.53
2026-01-27,285.51
2026-01-28,290.19
2026-01-29,290.78
2026-01-30,288.15
2026-02-02,281.4
2026-02-03,290.03
2026-02-04,289.6
2026-02-05,283.7
1 date close
4343 2026-01-22 279.27
4344 2026-01-23 279.99
4345 2026-01-26 283.53
4346 2026-01-27 285.51
4347 2026-01-28 290.19
4348 2026-01-29 290.78
4349 2026-01-30 288.15
4350 2026-02-02 281.4
4351 2026-02-03 290.03
4352 2026-02-04 289.6
4353 2026-02-05 283.7
@@ -1,5 +1,4 @@
date,close date,close
2026-01-02,258.96
2026-01-05,263.47 2026-01-05,263.47
2026-01-06,268.56 2026-01-06,268.56
2026-01-07,267.9 2026-01-07,267.9
@@ -20,3 +19,6 @@ date,close
2026-01-28,290.19 2026-01-28,290.19
2026-01-29,290.78 2026-01-29,290.78
2026-01-30,288.15 2026-01-30,288.15
2026-02-02,281.4
2026-02-03,290.03
2026-02-04,289.6
1 date close
2026-01-02 258.96
2 2026-01-05 263.47
3 2026-01-06 268.56
4 2026-01-07 267.9
19 2026-01-28 290.19
20 2026-01-29 290.78
21 2026-01-30 288.15
22 2026-02-02 281.4
23 2026-02-03 290.03
24 2026-02-04 289.6
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+328 -323
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1259
View File
File diff suppressed because it is too large Load Diff
+12 -13
View File
@@ -16,6 +16,9 @@ class DataEngine:
self.symbol = symbol.strip().upper() if symbol else None self.symbol = symbol.strip().upper() if symbol else None
self.name = name self.name = name
# --- Fetch frequently ---
self.cache_expiry = 1 * 3600 # Set to 0 for forced refresh, 24 for normal
# 2. Setup the directory variable FIRST # 2. Setup the directory variable FIRST
# (This was likely below the file_path line, causing the crash) # (This was likely below the file_path line, causing the crash)
base_path = os.path.dirname(os.path.abspath(__file__)) base_path = os.path.dirname(os.path.abspath(__file__))
@@ -50,21 +53,17 @@ class DataEngine:
def ensure_data(self): def ensure_data(self):
"""Checks if file exists and is fresh (less than 24h old).""" """Checks if file exists and is fresh (less than 24h old)."""
CACHE_EXPIRY = 24 * 3600 # 24 hours # Force expiry to 0
CACHE_EXPIRY = 0
if os.path.exists(self.file_path): if os.path.exists(self.file_path):
# NEW: Check how old the file is # By changing this to 'if False', we force it to ignore the cache every time
file_age = time.time() - os.path.getmtime(self.file_path) if False: # file_age < CACHE_EXPIRY:
if file_age < CACHE_EXPIRY: return True
return True # Data is actually fresh
else: else:
print(f"DEBUG: {self.symbol} cache is stale ({round(file_age/3600)}h old). Refreshing...") print(f"DEBUG: {self.symbol} refreshing now...")
else:
print(f"DEBUG: {self.symbol} not found in cache. Attempting download...") # This calls your actual downloader
# If we reached here, it means we either have NO file or a STALE file
# Instead of just yfinance, call your specialized fetch_data()
# which uses the URLs from your TEMPLATES
return self.fetch_data() return self.fetch_data()
def load_instruments_from_csv(self, file_path='instruments.csv'): def load_instruments_from_csv(self, file_path='instruments.csv'):
@@ -276,7 +275,7 @@ class DataEngine:
def fetch_data(self): def fetch_data(self):
local_df = pd.DataFrame() local_df = pd.DataFrame()
CACHE_EXPIRY = 24 * 3600 CACHE_EXPIRY = 0
file_exists = os.path.exists(self.file_path) file_exists = os.path.exists(self.file_path)
# 1. Load Local Cache & Check Age # 1. Load Local Cache & Check Age
+16 -4
View File
@@ -1,5 +1,17 @@
name,cusip,provider name,cusip,provider
JPMorgan Evergreen,HK0000055829,jpm JPM Vietnam,HK0000055811,jpm
Allianz Oriental Income Cl A,LU0348783233:USD,agi JPM Pacific ,HK0000055746,jpm
SPMO ETF - USD,SPMO,yahoo JPM Korea ,LU0301634860,jpm
JPM Korea,LU0301634860,jpm JPM India ,MU0129U00005,jpm
JPM Evergreen,HK0000055829,jpm
JPM ASEAN,HK0000055555,jpm
JPM Japan,LU0129465034,jpm
JPM G HYB,LU0356780857,jpm
JPM Europe,LU0119078227,jpm
INDA INDIA,INDA,yahoo
Fidelity Indonesia,LU0055114457,ft
AGI Oriental Income,LU0348783233,ft
VT,VT,yahoo
VHYD,VHYD.L,yahoo
SPMO,SPMO,yahoo
EWJV,EWJV,yahoo
1 name cusip provider
2 JPMorgan Evergreen JPM Vietnam HK0000055829 HK0000055811 jpm
3 Allianz Oriental Income Cl A JPM Pacific LU0348783233:USD HK0000055746 agi jpm
4 SPMO ETF - USD JPM Korea SPMO LU0301634860 yahoo jpm
5 JPM Korea JPM India LU0301634860 MU0129U00005 jpm
6 JPM Evergreen HK0000055829 jpm
7 JPM ASEAN HK0000055555 jpm
8 JPM Japan LU0129465034 jpm
9 JPM G HYB LU0356780857 jpm
10 JPM Europe LU0119078227 jpm
11 INDA INDIA INDA yahoo
12 Fidelity Indonesia LU0055114457 ft
13 AGI Oriental Income LU0348783233 ft
14 VT VT yahoo
15 VHYD VHYD.L yahoo
16 SPMO SPMO yahoo
17 EWJV EWJV yahoo
+36
View File
@@ -0,0 +1,36 @@
beautifulsoup4==4.14.3
blinker==1.9.0
cachelib==0.13.0
certifi==2026.1.4
cffi==2.0.0
charset-normalizer==3.4.4
click==8.3.1
curl_cffi==0.13.0
Flask==3.1.2
Flask-Caching==2.3.1
frozendict==2.4.7
html5lib==1.1
idna==3.11
itsdangerous==2.2.0
Jinja2==3.1.6
lxml==6.0.2
MarkupSafe==3.0.3
multitasking==0.0.12
numpy==2.4.1
pandas==3.0.0
peewee==3.19.0
platformdirs==4.5.1
protobuf==6.33.4
pycparser==3.0
python-dateutil==2.9.0.post0
pytz==2025.2
requests==2.32.5
six==1.17.0
soupsieve==2.8.3
ta==0.11.0
typing_extensions==4.15.0
urllib3==2.6.3
webencodings==0.5.1
websockets==16.0
Werkzeug==3.1.5
yfinance==1.1.0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 B

After

Width:  |  Height:  |  Size: 858 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 B

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.
+439 -363
View File
@@ -1,364 +1,440 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="manifest" href="{{ url_for('static', filename='site.webmanifest') }}?v=1"> <link rel="manifest" href="{{ url_for('static', filename='site.webmanifest') }}?v=1">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon.png') }}?v=1"> <link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon.png') }}?v=1">
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='favicon-32x32.png') }}?v=1"> <link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='favicon-32x32.png') }}?v=1">
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='favicon-16x16.png') }}?v=1"> <link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='favicon-16x16.png') }}?v=1">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}?v=1"> <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}?v=1">
<title>Financial Dashboard</title> <title>Financial Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style> <style>
:root { :root {
--header-bg: #4a5568; --header-bg: #4a5568;
--text-up: #28a745; --text-up: #28a745;
--text-down: #dc3545; --text-down: #dc3545;
--sticky-bg: #ffffff; --sticky-bg: #ffffff;
--sticky-bg-alt: #f9f9f9; /* For zebra stripes */ --sticky-bg-alt: #f9f9f9; /* For zebra stripes */
} }
body { body {
background-color: #f4f7f6; background-color: #f4f7f6;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto; font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto;
margin: 0; margin: 0;
padding: 10px; padding: 10px;
} }
/* Card Styling */ /* Card Styling */
.card { .card {
/*max-width: 1100px; /* Limits how wide the table grows on a desktop */ /*max-width: 1100px; /* Limits how wide the table grows on a desktop */
/*margin: 0 auto; /* Centers the table on the screen */ /*margin: 0 auto; /* Centers the table on the screen */
border: none; border: none;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1); box-shadow: 0 4px 15px rgba(0,0,0,0.1);
overflow: hidden; overflow: hidden;
} }
.card-header { .card-header {
background-color: white !important; background-color: white !important;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
padding: 15px; padding: 15px;
} }
/* 1. Fixed Table Layout Strategy */ /* 1. Fixed Table Layout Strategy */
.table-responsive { .table-responsive {
border-radius: 8px; border-radius: 8px;
overflow-x: auto; overflow-x: auto;
} }
.table { .table {
/* table-layout: auto; */ /* Default - let it be auto for data safety */ /* table-layout: auto; */ /* Default - let it be auto for data safety */
width: 100%; width: 100%;
/* This MUST match the sum of your column min-widths */ /* This MUST match the sum of your column min-widths */
min-width: 600px; min-width: 600px;
/* Critical for the 'Sticky' column borders to look right */ /* Critical for the 'Sticky' column borders to look right */
border-collapse: separate; border-collapse: separate;
border-spacing: 0; border-spacing: 0;
} }
/* 2. Header & Cell Styling */ /* 2. Header & Cell Styling */
.table thead th { .table thead th {
background-color: var(--header-bg) !important; background-color: var(--header-bg) !important;
color: white !important; color: white !important;
font-size: 0.7rem; font-size: 0.7rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
text-align: center; text-align: center;
padding: 12px 8px; padding: 12px 8px;
border: none; border: none;
white-space: nowrap; white-space: nowrap;
} }
.table td { .table td {
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
font-size: 0.85rem; font-size: 0.85rem;
white-space: nowrap; white-space: nowrap;
border-bottom: 1px solid #f1f1f1; border-bottom: 1px solid #f1f1f1;
padding: 10px 8px; padding: 10px 8px;
} }
/* 3. Sticky Column Logic (Instrument) */ /* 3. Sticky Column Logic (Instrument) */
.table td:first-child, .table td:first-child,
.table th:first-child { .table th:first-child {
position: sticky; position: sticky;
text-align: left !important; /* Force left alignment */ text-align: left !important; /* Force left alignment */
padding-left: 15px; /* Add space so text doesn't touch the edge */ padding-left: 15px;
left: 0; color: #2d3748/* Add space so text doesn't touch the edge */
z-index: 10; left: 0;
background-color: var(--sticky-bg); z-index: 10;
font-weight: 700; background-color: var(--sticky-bg);
font-weight: 700;
/* THE SHADOW EFFECT */
/* This adds a 4px blur shadow to the right side of the column */ /* THE SHADOW EFFECT */
box-shadow: 4px 0 8px -2px rgba(0, 0, 0, 0.15); /* This adds a 4px blur shadow to the right side of the column */
box-shadow: 4px 0 8px -2px rgba(0, 0, 0, 0.15);
/* Clean border to define the edge */
border-right: 1px solid #ddd; /* Clean border to define the edge */
} border-right: 1px solid #ddd;
}
/* 4. Zebra Striping + Sticky Fix */ /* Specific fix for Close and Chg% columns readability */
.table-striped tbody tr:nth-of-type(odd) { .table td:nth-child(3),
background-color: rgba(0, 0, 0, 0.03); .table td:nth-child(4) {
} font-weight: 600;
color: #1a202c !important; /* Extra bold and dark */
.table-striped tbody tr:nth-of-type(odd) td:first-child { }
background-color: var(--sticky-bg-alt); /* Matches stripe color */
} /* Navbar fix: Ensure buttons don't disappear on tiny screens */
.navbar-nav {
/* Custom Colors & UI */ flex-direction: row !important; /* Keep links side-by-side on mobile */
.text-up { color: var(--text-up); font-weight: 600; } gap: 10px;
.text-down { color: var(--text-down); font-weight: 600; } }
@media (prefers-color-scheme: dark) { .nav-link {
:root { padding: 0.5rem !important;
--header-bg: #1a202c; /* Darker header */ font-size: 0.85rem;
--sticky-bg: #2d3748; /* Dark background for sticky column */ }
--sticky-bg-alt: #1a202c;
background-color: #121212; /* Main page background */ /* 4. Zebra Striping + Sticky Fix */
color: #e2e8f0; /* Light text */ .table-striped tbody tr:nth-of-type(odd) {
} background-color: rgba(0, 0, 0, 0.03);
.card, .card-header { }
background-color: #2d3748 !important;
color: white; .table-striped tbody tr:nth-of-type(odd) td:first-child {
} background-color: var(--sticky-bg-alt); /* Matches stripe color */
.table td { border-color: #4a5568; color: #e2e8f0; } }
}
/* Custom Colors & UI */
#loading { display: none; margin-left: 10px; font-size: 0.8rem; color: #666; } .text-up { color: var(--text-up); font-weight: 600; }
.text-down { color: var(--text-down); font-weight: 600; }
</style>
</head> .table-date {
<body> font-size: 0.8rem !important; /* Increased slightly to make it obvious */
font-weight: 800 !important; /* Extra bold */
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4"> font-family: 'Courier New', monospace !important;
<div class="container-fluid"> display: inline-block; /* Sometimes helps with sizing */
<a class="navbar-brand" href="/"> white-space: nowrap; /* Prevents the date from snapping to 2 lines */
<i class="bi bi-graph-up-arrow me-2"></i>Finance Suite }
</a> .status-dot {
<div class="collapse navbar-collapse" id="navbarNav"> width: 8px;
<ul class="navbar-nav"> height: 8px;
<li class="nav-item"><a class="nav-link" href="/">Dashboard</a></li> background-color: #39FF14; /* Neon green for better visibility */
<li class="nav-item"><a class="nav-link" href="/backtest">Backtester</a></li> border-radius: 50%;
</ul> display: inline-block;
</div> box-shadow: 0 0 10px rgba(57, 255, 20, 0.4);
<span class="navbar-text text-light small d-none d-md-inline"> animation: status-pulse 2s infinite;
System Status: <span class="text-success">● Online</span> }
</span>
</div> /* Custom text class if Bootstrap classes aren't bright enough */
</nav> .text-white-50 {
color: rgba(255, 255, 255, 0.7) !important; /* Increased from 0.5 to 0.7 for clarity */
<div class="container-fluid p-0"> }
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center"> @keyframes status-pulse {
<h5 class="mb-0 text-dark">Portfolio Signals</h5> 0% { transform: scale(0.95); opacity: 1; }
<div class="d-flex align-items-center gap-2"> 50% { transform: scale(1.1); opacity: 0.7; }
<span id="loading">Updating...</span> 100% { transform: scale(0.95); opacity: 1; }
<button class="btn btn-outline-secondary btn-sm" onclick="loadData()"> }
<i class="bi bi-arrow-clockwise"></i> Refresh
</button> #lastSyncTime {
<button id="syncBtn" class="btn btn-primary btn-sm" onclick="runGlobalSync()"> font-size: 0.85rem;
<i class="bi bi-cloud-download"></i> Sync Data letter-spacing: 0.5px;
</button> }
</div>
</div> @media (prefers-color-scheme: dark) {
<div class="card-body p-0"> :root {
<div class="table-responsive"> --header-bg: #1a202c; /* Darker header */
<table class="table table-hover table-striped mb-0"> --sticky-bg: #2d3748; /* Dark background for sticky column */
<thead> --sticky-bg-alt: #1a202c;
<tr> background-color: #121212; /* Main page background */
<th style="min-width: 50px;">Instrument</th> color: #e2e8f0; /* Light text */
<th style="min-width: 25px;">Date</th> }
<th style="min-width: 25px;">Close</th> .card, .card-header {
<th style="min-width: 25px;">Chg%</th> background-color: #2d3748 !important;
<th style="min-width: 100px;">52W Range</th> color: white;
<th style="min-width: 85px;">20 EMA</th> }
<th style="min-width: 25px;">50 EMA</th> .table td { border-color: #4a5568; color: #e2e8f0; }
<th style="min-width: 25px;">100 EMA</th> }
<th style="min-width: 25px;">200 EMA</th>
<th style="min-width: 60px;">K/D</th> #loading { display: none; margin-left: 10px; font-size: 0.8rem; color: #666; }
</tr>
</thead> </style>
<tbody id="tableBody"> </head>
<tr><td colspan="10" class="p-4">Initializing data engine...</td></tr> <body>
</tbody> <nav class="navbar navbar-dark mb-4" style="background-color: #1a202c !important;">
</table> <div class="container-fluid d-flex justify-content-between align-items-center">
</div> <div class="d-flex align-items-center">
</div> <a class="navbar-brand me-4" href="/">
</div> <i class="bi bi-graph-up-arrow me-2"></i>Finance Suite
</div> </a>
<script> <div class="d-flex gap-2">
console.log("Script block loaded successfully."); <a class="btn btn-sm btn-outline-light border-secondary" href="/">Dashboard</a>
<a class="btn btn-sm btn-outline-light border-secondary" href="/backtest">Backtest</a>
// --- 1. Helper Function for K/D Styling --- </div>
function formatKD(val) { </div>
if (!val || val === "N/A" || !val.includes('/')) return `<span class="text-muted">N/A</span>`;
const [k, d] = val.split('/').map(v => parseFloat(v)); <div class="d-flex align-items-center" style="font-size: 0.8rem; letter-spacing: 0.3px;">
let colorClass = 'text-dark'; <div class="status-dot me-2"></div>
if (k >= 80) colorClass = 'text-danger fw-bold'; <span class="text-white-50">System Online</span>
else if (k <= 20) colorClass = 'text-success fw-bold'; <span class="mx-2 text-white-50 opacity-25">|</span>
return `<span class="${colorClass}">${val}</span>`; <span class="text-white-50">Last Sync:</span>
} <span id="lastSyncTime" class="ms-1 fw-bold text-white font-monospace">--:--:--</span>
</div>
// --- 2. EMA Formatter --- </div>
const formatEma = (val) => { </nav>
if (val === "N/A" || val === null || val === undefined) return `<span class="text-muted">N/A</span>`; <div class="container-fluid p-0">
const num = parseFloat(val); <div class="card">
const sign = num > 0 ? "+" : ""; <div class="card-header d-flex justify-content-between align-items-center">
const colorClass = num >= 0 ? 'text-up' : 'text-down'; <h5 class="mb-0 text-dark">Portfolio Signals</h5>
return `<span class="${colorClass}">${sign}${num.toFixed(1)}%</span>`; <div class="d-flex align-items-center gap-2">
}; <span id="loading">Updating...</span>
<button class="btn btn-outline-secondary btn-sm" onclick="loadData()">
// --- 3. Load Table Data --- <i class="bi bi-arrow-clockwise"></i> Refresh
async function loadData() { </button>
console.log("Starting loadData..."); <button id="syncBtn" class="btn btn-primary btn-sm" onclick="runGlobalSync()">
const loading = document.getElementById('loading'); <i class="bi bi-cloud-download"></i> Sync Data
const tbody = document.getElementById('tableBody'); </button>
if (loading) loading.style.display = 'inline'; </div>
</div>
try { <div class="card-body p-0">
const response = await fetch('/api/summary'); <div class="table-responsive">
const data = await response.json(); <table class="table table-hover table-striped mb-0">
<thead>
if (!data || data.length === 0) { <tr>
tbody.innerHTML = '<tr><td colspan="10" class="p-4">No data found. Please run Sync.</td></tr>'; <th style="min-width: 50px;">Instrument</th>
return; <th style="min-width: 25px;">Date</th>
} <th style="min-width: 25px;">Close</th>
<th style="min-width: 25px;">Chg%</th>
// Get today's date string (YYYY-MM-DD) to check freshness <th style="min-width: 100px;">52W Range</th>
const todayStr = new Date().toISOString().split('T')[0]; <th style="min-width: 85px;">20 EMA</th>
let htmlContent = ''; <th style="min-width: 25px;">50 EMA</th>
<th style="min-width: 25px;">100 EMA</th>
data.forEach(item => { <th style="min-width: 25px;">200 EMA</th>
// --- 1. Error Row Handling --- <th style="min-width: 60px;">K/D</th>
if (item.error || !item.last_close) { </tr>
htmlContent += ` </thead>
<tr> <tbody id="tableBody">
<td>${item.name || 'Unknown'}</td> <tr><td colspan="10" class="p-4">Initializing data engine...</td></tr>
<td colspan="9" class="text-center p-3"> </tbody>
<span class="badge bg-warning text-dark"><i class="bi bi-exclamation-triangle"></i> Needs Sync</span> </table>
<small class="text-muted ms-2">Local CSV not found or corrupted.</small> </div>
</td> </div>
</tr>`; </div>
return; </div>
} <script>
let displayDate = "N/A"; console.log("Script block loaded successfully.");
if (item.last_date && item.last_date.includes('-')) {
const parts = item.last_date.split('-'); // --- 1. Helper Function for K/D Styling ---
const formatted = `${parts[2]}/${parts[1]}`; function formatKD(val) {
if (!val || val === "N/A" || !val.includes('/')) return `<span class="text-muted">N/A</span>`;
// Get today's date in YYYY-MM-DD format to compare const [k, d] = val.split('/').map(v => parseFloat(v));
const today = new Date().toISOString().split('T')[0]; let colorClass = 'text-dark';
if (k >= 80) colorClass = 'text-danger fw-bold';
// Pick color based on freshness else if (k <= 20) colorClass = 'text-success fw-bold';
const color = (item.last_date === today) ? '#28a745' : '#dc3545'; return `<span class="${colorClass}">${val}</span>`;
}
displayDate = `<span style="color: ${color};">${formatted}</span>`;
} // --- 2. EMA Formatter ---
// --- 2. Calculations & Styling --- const formatEma = (val) => {
const current = parseFloat(item.last_close) || 0; if (val === "N/A" || val === null || val === undefined) return `<span class="text-muted">N/A</span>`;
const low = parseFloat(item.low_52) || 0; const num = parseFloat(val);
const high = parseFloat(item.high_52) || 0; const sign = num > 0 ? "+" : "";
const colorClass = num >= 0 ? 'text-up' : 'text-down';
// 52W Range Progress Bar return `<span class="${colorClass}">${sign}${num.toFixed(1)}%</span>`;
let rangePct = high > low ? Math.min(Math.max(((current - low) / (high - low)) * 100, 0), 100) : 0; };
const rangeColor = rangePct > 80 ? 'text-danger' : (rangePct < 20 ? 'text-success' : 'text-muted');
// --- 3. Load Table Data ---
// Date Freshness check (highlight if date matches today) async function loadData() {
const isFresh = item.last_date === todayStr; console.log("Starting loadData...");
const dateStyle = isFresh ? 'badge bg-success-subtle text-success border border-success-subtle' : 'text-muted'; const loading = document.getElementById('loading');
const tbody = document.getElementById('tableBody');
// --- 3. Construct Row --- const syncDisplay = document.getElementById('lastSyncTime'); // Get our new element
htmlContent += `
<tr> if (loading) loading.style.display = 'inline';
<td>
<div class="fw-bold">${item.name || item.symbol}</div> try {
</td> const response = await fetch('/api/summary');
<td><span class="${dateStyle}" style="font-size: 0.75rem; padding: 2px 5px; border-radius: 4px;">${displayDate}</span></td> const data = await response.json();
<td class="fw-bold">${item.last_close}</td>
<td class="${item.change_pct >= 0 ? 'text-up' : 'text-down'}"> const syncDisplay = document.getElementById('lastSyncTime');
${item.change_pct >= 0 ? '+' : ''}${item.change_pct}% if (syncDisplay) {
</td> const now = new Date();
<td class="${rangeColor} small"> syncDisplay.innerText = now.toLocaleTimeString([], { hour12: false });
<div class="d-flex justify-content-between mb-1" style="min-width: 100px; font-size: 0.7rem;">
<span>${item.low_52}</span><span>${item.high_52}</span> // Brief highlight effect
</div> syncDisplay.classList.add('text-success');
<div class="progress" style="height: 4px;"> setTimeout(() => syncDisplay.classList.remove('text-success'), 2000);
<div class="progress-bar bg-primary" style="width: ${rangePct}%"></div> }
</div> // --- Update the Sync Time Display ---
</td> const now = new Date();
<td>${formatEma(item.last_ema20)}</td> const timeStr = now.toLocaleTimeString([], { hour12: false }); // e.g. 02:45:10
<td>${formatEma(item.last_ema50)}</td> const dateStr = now.toLocaleDateString();
<td>${formatEma(item.last_ema100)}</td> if (syncDisplay) {
<td>${formatEma(item.last_ema200)}</td> syncDisplay.innerHTML = `${dateStr} ${timeStr}`;
<td>${formatKD(item.kd_values)}</td> }
</tr>`; // -----------------------------------------
});
if (!data || data.length === 0) {
// Batch update the DOM once tbody.innerHTML = '<tr><td colspan="10" class="p-4 text-center">No data found. Please run Sync.</td></tr>';
tbody.innerHTML = htmlContent; return;
}
} catch (error) {
console.error("Fetch error:", error); let htmlContent = '';
tbody.innerHTML = '<tr><td colspan="10" class="text-danger p-4">Error loading summary. Check server logs.</td></tr>'; const todayStr = new Date().toISOString().split('T')[0];
} finally {
if (loading) loading.style.display = 'none'; data.forEach(item => {
} // --- 1. Error Row Handling ---
} if (item.error || !item.last_close) {
htmlContent += `
// --- 4. Global Sync --- <tr>
async function runGlobalSync() { <td>${item.name || 'Unknown'}</td>
const syncBtn = document.getElementById('syncBtn'); <td colspan="9" class="text-center p-3">
const loading = document.getElementById('loading'); <span class="badge bg-warning text-dark">Needs Sync</span>
if (!syncBtn) return; <small class="text-muted ms-2">Local CSV not found or corrupted.</small>
</td>
syncBtn.disabled = true; </tr>`;
const originalText = syncBtn.innerHTML; return; // Skip to next item
syncBtn.innerHTML = `<span class="spinner-border spinner-border-sm"></span> Syncing...`; }
if (loading) loading.style.display = 'inline';
// --- 2. Date & Style Calculations ---
try { let displayDate = "N/A";
const response = await fetch('/api/sync', { method: 'POST' }); let dateColor = '#dc3545'; // Default red
if (!response.ok) throw new Error("Sync failed");
await loadData(); if (item.last_date && item.last_date.includes('-')) {
alert("Sync Complete!"); const parts = item.last_date.split('-');
} catch (error) { displayDate = `${parts[2]}/${parts[1]}`;
console.error("Sync error:", error); dateColor = (item.last_date === todayStr) ? '#28a745' : '#dc3545';
alert("Sync failed. Check terminal."); }
} finally {
syncBtn.disabled = false; const current = parseFloat(item.last_close) || 0;
syncBtn.innerHTML = originalText; const low = parseFloat(item.low_52) || 0;
if (loading) loading.style.display = 'none'; const high = parseFloat(item.high_52) || 0;
}
} // 52W Range Progress Bar calculation
// This checks if the Flask server is responding every 30 seconds let rangePct = high > low ? Math.min(Math.max(((current - low) / (high - low)) * 100, 0), 100) : 0;
async function checkStatus() { const rangeColor = rangePct > 80 ? 'text-danger' : (rangePct < 20 ? 'text-success' : 'text-muted');
const indicator = document.getElementById('statusIndicator');
try { // Freshness class for the badge
const response = await fetch('/api/summary'); // Or a dedicated /health endpoint const isFresh = item.last_date === todayStr;
if (response.ok) { const dateBadgeClass = isFresh ? 'badge bg-success-subtle text-success border border-success-subtle' : 'text-muted';
indicator.innerHTML = '● Online';
indicator.className = 'text-success'; // --- 3. Construct Row ---
} else { htmlContent += `
throw new Error(); <tr>
} <td>
} catch (e) { <div class="fw-bold">${item.name || item.symbol}</div>
indicator.innerHTML = '● Offline'; </td>
indicator.className = 'text-danger'; <td>
} <span class="${dateBadgeClass} table-date" style="color: ${dateColor} !important; font-size: 0.75rem; padding: 2px 5px; border-radius: 4px;">
} ${displayDate}
</span>
setInterval(checkStatus, 300000); </td>
checkStatus(); // Initial check <td class="fw-bold">${item.last_close}</td>
<td class="${item.change_pct >= 0 ? 'text-up' : 'text-down'}">
// --- 5. Initial Load --- ${item.change_pct >= 0 ? '+' : ''}${item.change_pct}%
window.addEventListener('load', loadData); </td>
</script> <td class="${rangeColor} small">
<div class="d-flex justify-content-between mb-1" style="min-width: 100px; font-size: 0.7rem;">
</body> <span>${item.low_52}</span><span>${item.high_52}</span>
</div>
<div class="progress" style="height: 4px;">
<div class="progress-bar bg-primary" style="width: ${rangePct}%"></div>
</div>
</td>
<td>${formatEma(item.last_ema20)}</td>
<td>${formatEma(item.last_ema50)}</td>
<td>${formatEma(item.last_ema100)}</td>
<td>${formatEma(item.last_ema200)}</td>
<td>${formatKD(item.kd_values)}</td>
</tr>`;
}); // End forEach
// 4. Final Injection
tbody.innerHTML = htmlContent;
} catch (error) {
console.error("Fetch error:", error);
if (tbody) {
tbody.innerHTML = '<tr><td colspan="10" class="text-danger p-4 text-center">Error loading summary. Check server logs.</td></tr>';
}
} finally {
if (loading) loading.style.display = 'none';
}
}
// --- 4. Global Sync ---
async function runGlobalSync() {
const syncBtn = document.getElementById('syncBtn');
const loading = document.getElementById('loading');
if (!syncBtn) return;
syncBtn.disabled = true;
const originalText = syncBtn.innerHTML;
syncBtn.innerHTML = `<span class="spinner-border spinner-border-sm"></span> Syncing...`;
if (loading) loading.style.display = 'inline';
try {
const response = await fetch('/api/sync', { method: 'POST' });
if (!response.ok) throw new Error("Sync failed");
await loadData();
alert("Sync Complete!");
} catch (error) {
console.error("Sync error:", error);
alert("Sync failed. Check terminal.");
} finally {
syncBtn.disabled = false;
syncBtn.innerHTML = originalText;
if (loading) loading.style.display = 'none';
}
}
// This checks if the Flask server is responding every 30 seconds
async function checkStatus() {
const indicator = document.getElementById('statusIndicator');
try {
const response = await fetch('/api/summary'); // Or a dedicated /health endpoint
if (response.ok) {
indicator.innerHTML = '● Online';
indicator.className = 'text-success';
} else {
throw new Error();
}
} catch (e) {
indicator.innerHTML = '● Offline';
indicator.className = 'text-danger';
}
}
setInterval(checkStatus, 300000);
checkStatus(); // Initial check
// --- 5. Initial Load ---
window.addEventListener('load', loadData);
</script>
</body>
</html> </html>