Add daily auto-scan: runs on startup if stale, then every 24h
Browse files
app.py
CHANGED
|
@@ -9,7 +9,8 @@ from __future__ import annotations
|
|
| 9 |
import os
|
| 10 |
import sys
|
| 11 |
import threading
|
| 12 |
-
|
|
|
|
| 13 |
from typing import Optional
|
| 14 |
|
| 15 |
import numpy as np
|
|
@@ -77,9 +78,21 @@ RESULTS_STATE: dict = {
|
|
| 77 |
"last_run": None,
|
| 78 |
"last_msg": "No scan run yet.",
|
| 79 |
"scanned_universe": [],
|
|
|
|
| 80 |
}
|
| 81 |
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
# ---------------------------------------------------------------------------
|
| 84 |
# Helpers
|
| 85 |
# ---------------------------------------------------------------------------
|
|
@@ -87,7 +100,11 @@ RESULTS_STATE: dict = {
|
|
| 87 |
def _format_status() -> str:
|
| 88 |
n = 0 if RESULTS_STATE["df"] is None else len(RESULTS_STATE["df"])
|
| 89 |
last = RESULTS_STATE["last_run"].strftime("%Y-%m-%d %H:%M:%S") if RESULTS_STATE["last_run"] else "never"
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
|
| 93 |
def _result_columns() -> list[str]:
|
|
@@ -305,6 +322,76 @@ def _start_auto_improve(base_weights: dict) -> None:
|
|
| 305 |
threading.Thread(target=_worker, daemon=True).start()
|
| 306 |
|
| 307 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
# ---------------------------------------------------------------------------
|
| 309 |
# Scan logic
|
| 310 |
# ---------------------------------------------------------------------------
|
|
@@ -771,6 +858,10 @@ def build_ui() -> gr.Blocks:
|
|
| 771 |
demo = build_ui()
|
| 772 |
|
| 773 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 774 |
if __name__ == "__main__":
|
| 775 |
demo.queue(max_size=8).launch(
|
| 776 |
server_name="0.0.0.0",
|
|
|
|
| 9 |
import os
|
| 10 |
import sys
|
| 11 |
import threading
|
| 12 |
+
import time
|
| 13 |
+
from datetime import datetime, timedelta
|
| 14 |
from typing import Optional
|
| 15 |
|
| 16 |
import numpy as np
|
|
|
|
| 78 |
"last_run": None,
|
| 79 |
"last_msg": "No scan run yet.",
|
| 80 |
"scanned_universe": [],
|
| 81 |
+
"next_daily_run": None, # datetime of the next scheduled auto-scan
|
| 82 |
}
|
| 83 |
|
| 84 |
|
| 85 |
+
# Daily auto-scan configuration
|
| 86 |
+
DAILY_SCAN_INITIAL_DELAY_SEC = 20 # let the app + Gradio queue boot
|
| 87 |
+
DAILY_SCAN_PERIOD_SEC = 24 * 3600 # 24 hours between scheduled scans
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class _NoOpProgress:
|
| 91 |
+
"""No-op stand-in for ``gr.Progress()`` used by background scans."""
|
| 92 |
+
def __call__(self, frac, desc=None):
|
| 93 |
+
pass
|
| 94 |
+
|
| 95 |
+
|
| 96 |
# ---------------------------------------------------------------------------
|
| 97 |
# Helpers
|
| 98 |
# ---------------------------------------------------------------------------
|
|
|
|
| 100 |
def _format_status() -> str:
|
| 101 |
n = 0 if RESULTS_STATE["df"] is None else len(RESULTS_STATE["df"])
|
| 102 |
last = RESULTS_STATE["last_run"].strftime("%Y-%m-%d %H:%M:%S") if RESULTS_STATE["last_run"] else "never"
|
| 103 |
+
nxt = RESULTS_STATE.get("next_daily_run")
|
| 104 |
+
nxt_str = ""
|
| 105 |
+
if nxt is not None:
|
| 106 |
+
nxt_str = f" • **Next auto-scan:** {nxt.strftime('%Y-%m-%d %H:%M:%S')} UTC"
|
| 107 |
+
return f"**Last run:** {last} • {n} stocks • {RESULTS_STATE['last_msg']}{nxt_str}"
|
| 108 |
|
| 109 |
|
| 110 |
def _result_columns() -> list[str]:
|
|
|
|
| 322 |
threading.Thread(target=_worker, daemon=True).start()
|
| 323 |
|
| 324 |
|
| 325 |
+
# ---------------------------------------------------------------------------
|
| 326 |
+
# Daily auto-scan
|
| 327 |
+
# ---------------------------------------------------------------------------
|
| 328 |
+
|
| 329 |
+
def _run_scheduled_scan(reason: str) -> None:
|
| 330 |
+
"""Run a single full-universe-Filtered scan with the slider defaults
|
| 331 |
+
and update :data:`RESULTS_STATE`. Used by :func:`_start_daily_scan`.
|
| 332 |
+
"""
|
| 333 |
+
weights, _ = _resolve_weights(
|
| 334 |
+
False,
|
| 335 |
+
DEFAULT_WEIGHTS["cmf"],
|
| 336 |
+
DEFAULT_WEIGHTS["obv_slope"],
|
| 337 |
+
DEFAULT_WEIGHTS["big_bar_ratio"],
|
| 338 |
+
DEFAULT_WEIGHTS["vwap_dev"],
|
| 339 |
+
DEFAULT_WEIGHTS["rvol_signed"],
|
| 340 |
+
)
|
| 341 |
+
try:
|
| 342 |
+
df, frames, scanned, msg = _do_scan(
|
| 343 |
+
"Filtered (default)", 5.0, 5_000_000.0, "All", "All",
|
| 344 |
+
weights, _NoOpProgress(),
|
| 345 |
+
)
|
| 346 |
+
except Exception as e:
|
| 347 |
+
RESULTS_STATE["last_msg"] = f"Error ({reason}): {e}"
|
| 348 |
+
return
|
| 349 |
+
|
| 350 |
+
suffix = f" (auto: {reason})"
|
| 351 |
+
RESULTS_STATE["df"] = df
|
| 352 |
+
RESULTS_STATE["frames"] = frames
|
| 353 |
+
RESULTS_STATE["weights"] = weights
|
| 354 |
+
RESULTS_STATE["last_run"] = datetime.utcnow()
|
| 355 |
+
RESULTS_STATE["last_msg"] = f"{msg}{suffix}"
|
| 356 |
+
RESULTS_STATE["scanned_universe"] = scanned
|
| 357 |
+
if df is not None and not df.empty:
|
| 358 |
+
_start_auto_improve(weights)
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def _start_daily_scan() -> None:
|
| 362 |
+
"""Background thread: run a scan on startup (if the last one is
|
| 363 |
+
older than :data:`DAILY_SCAN_PERIOD_SEC`), then re-run once every
|
| 364 |
+
24 hours. HF Spaces can sleep after 48h of inactivity, so the
|
| 365 |
+
startup gate is what actually delivers the "one scan per day"
|
| 366 |
+
guarantee: a fresh visit always triggers a scan if the cached one
|
| 367 |
+
is stale.
|
| 368 |
+
"""
|
| 369 |
+
def _worker():
|
| 370 |
+
time.sleep(DAILY_SCAN_INITIAL_DELAY_SEC)
|
| 371 |
+
# Initial scan: only if last successful scan is stale
|
| 372 |
+
last = RESULTS_STATE.get("last_run")
|
| 373 |
+
stale = (
|
| 374 |
+
last is None
|
| 375 |
+
or (datetime.utcnow() - last).total_seconds() > DAILY_SCAN_PERIOD_SEC
|
| 376 |
+
)
|
| 377 |
+
if stale:
|
| 378 |
+
try:
|
| 379 |
+
_run_scheduled_scan("startup")
|
| 380 |
+
except Exception as e:
|
| 381 |
+
RESULTS_STATE["last_msg"] = f"Auto-scan (startup) failed: {e}"
|
| 382 |
+
# Then loop, scheduling a scan every 24h
|
| 383 |
+
while True:
|
| 384 |
+
RESULTS_STATE["next_daily_run"] = (
|
| 385 |
+
datetime.utcnow() + timedelta(seconds=DAILY_SCAN_PERIOD_SEC)
|
| 386 |
+
)
|
| 387 |
+
time.sleep(DAILY_SCAN_PERIOD_SEC)
|
| 388 |
+
try:
|
| 389 |
+
_run_scheduled_scan("daily")
|
| 390 |
+
except Exception as e:
|
| 391 |
+
RESULTS_STATE["last_msg"] = f"Auto-scan (daily) failed: {e}"
|
| 392 |
+
threading.Thread(target=_worker, daemon=True).start()
|
| 393 |
+
|
| 394 |
+
|
| 395 |
# ---------------------------------------------------------------------------
|
| 396 |
# Scan logic
|
| 397 |
# ---------------------------------------------------------------------------
|
|
|
|
| 858 |
demo = build_ui()
|
| 859 |
|
| 860 |
|
| 861 |
+
# Kick off the once-per-day auto-scan thread
|
| 862 |
+
_start_daily_scan()
|
| 863 |
+
|
| 864 |
+
|
| 865 |
if __name__ == "__main__":
|
| 866 |
demo.queue(max_size=8).launch(
|
| 867 |
server_name="0.0.0.0",
|