veteroner commited on
Commit
4fa0ced
·
1 Parent(s): 4e27151

feat: scan buttons + daily auto-scan scheduler

Browse files

- Eligible page: Tara dropdown (BIST30/BIST100/Tum BIST) + Yenile
- Scan progress banner with real progress bar (stage2/stage1)
- Auto-scan scheduler: runs BIST100 every day at 00:00 TR time
- bist_all/all universe support in run_bist100_scan.py
- Partial results warning when scan not completed

huggingface-space/app.py CHANGED
@@ -52,6 +52,77 @@ _trading_worker_thread = None
52
  _scan_thread = None
53
  _scan_status = {"running": False, "progress": "", "started_at": None}
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  def _get_trading_status():
57
  """Read trading status from file-based state."""
 
52
  _scan_thread = None
53
  _scan_status = {"running": False, "progress": "", "started_at": None}
54
 
55
+ # ─── Daily Auto-Scan Scheduler ────────────────────────
56
+ # Runs BIST100 scan every day at 00:00 Turkey time (21:00 UTC)
57
+ import threading
58
+ import time as _time
59
+
60
+ def _daily_scan_scheduler():
61
+ """Background thread: triggers a full BIST100 scan daily at midnight TR time."""
62
+ global _scan_thread, _scan_status
63
+ from datetime import timezone, timedelta
64
+
65
+ TR_TZ = timezone(timedelta(hours=3))
66
+
67
+ while True:
68
+ try:
69
+ now_tr = datetime.now(TR_TZ)
70
+ # Next midnight TR
71
+ tomorrow = (now_tr + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
72
+ wait_seconds = (tomorrow - now_tr).total_seconds()
73
+ print(f"[scheduler] Next auto-scan at {tomorrow.isoformat()} (in {wait_seconds/3600:.1f}h)")
74
+ _time.sleep(wait_seconds)
75
+
76
+ # Check if scan already running
77
+ if _scan_status.get("running"):
78
+ print("[scheduler] Scan already running, skipping scheduled scan")
79
+ continue
80
+
81
+ print("[scheduler] Starting daily BIST100 auto-scan...")
82
+ _scan_status = {
83
+ "running": True,
84
+ "progress": "Günlük otomatik tarama başlatılıyor (BIST100)...",
85
+ "started_at": datetime.now().isoformat(),
86
+ "universe": "bist100",
87
+ "scheduled": True,
88
+ }
89
+
90
+ def _auto_scan():
91
+ global _scan_status
92
+ try:
93
+ from run_bist100_scan import run_scan
94
+ _scan_status["progress"] = "Stage 1: Likidite filtresi..."
95
+ run_scan(universe="bist100", force=True, stage1_only=False)
96
+ _scan_status = {
97
+ "running": False,
98
+ "progress": "Günlük tarama tamamlandı!",
99
+ "finished_at": datetime.now().isoformat(),
100
+ "universe": "bist100",
101
+ "scheduled": True,
102
+ }
103
+ print(f"[scheduler] Daily scan completed at {datetime.now().isoformat()}")
104
+ except Exception as e:
105
+ _scan_status = {
106
+ "running": False,
107
+ "progress": f"Günlük tarama hatası: {e}",
108
+ "error": str(e),
109
+ "universe": "bist100",
110
+ "scheduled": True,
111
+ }
112
+ print(f"[scheduler] Daily scan error: {e}")
113
+
114
+ _scan_thread = threading.Thread(target=_auto_scan, daemon=True, name="daily-bist-scan")
115
+ _scan_thread.start()
116
+
117
+ except Exception as e:
118
+ print(f"[scheduler] Scheduler error: {e}")
119
+ _time.sleep(3600) # wait an hour on error and retry
120
+
121
+ # Start scheduler on import
122
+ _scheduler_thread = threading.Thread(target=_daily_scan_scheduler, daemon=True, name="scan-scheduler")
123
+ _scheduler_thread.start()
124
+ print("[scheduler] Daily auto-scan scheduler started (00:00 TR / 21:00 UTC)")
125
+
126
 
127
  def _get_trading_status():
128
  """Read trading status from file-based state."""
huggingface-space/run_bist100_scan.py CHANGED
@@ -242,8 +242,13 @@ def get_universe(name: str) -> List[str]:
242
  """Get stock list from official Borsa Istanbul CSV."""
243
  from data.index_constituents import get_index_constituents
244
 
 
 
 
 
 
245
  try:
246
- result = get_index_constituents(name)
247
  symbols = result.symbols
248
  logger.info("Fetched %d stocks from %s", len(symbols), name)
249
  return symbols
 
242
  """Get stock list from official Borsa Istanbul CSV."""
243
  from data.index_constituents import get_index_constituents
244
 
245
+ # Normalize aliases
246
+ lookup = name.lower().replace("-", "_").replace(" ", "_")
247
+ if lookup in ("bist_all", "all", "tum", "tum_bist"):
248
+ lookup = "all"
249
+
250
  try:
251
+ result = get_index_constituents(lookup)
252
  symbols = result.symbols
253
  logger.info("Fetched %d stocks from %s", len(symbols), name)
254
  return symbols
nextjs-app/src/app/eligible/page.tsx CHANGED
@@ -22,6 +22,8 @@ import {
22
  CircleDot,
23
  Eye,
24
  X,
 
 
25
  } from 'lucide-react'
26
 
27
  // ─── Types ─────────────────────────────────
@@ -232,6 +234,8 @@ export default function EligiblePage() {
232
  const [viewMode, setViewMode] = useState<'cards' | 'table'>('cards')
233
  const [scanLoading, setScanLoading] = useState(false)
234
  const [signalFilter, setSignalFilter] = useState<string>('all')
 
 
235
  const isScanCompleted = Boolean(data?.scanCompleted)
236
  const stage2Done = data?.stage2Done ?? null
237
  const stage1Passed = data?.stage1PassedCount ?? null
@@ -460,18 +464,6 @@ export default function EligiblePage() {
460
  {/* Header */}
461
  <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
462
  <div>
463
-
464
- {data?.ok && !isScanCompleted && (
465
- <div className="mt-4 rounded-md border bg-background p-3 text-sm">
466
- <div className="font-medium">Tarama devam ediyor (kısmi sonuçlar)</div>
467
- <div className="text-muted-foreground">
468
- {data.scanProgress ||
469
- (stage2Done !== null && stage1Passed !== null
470
- ? `Stage 2: ${stage2Done}/${stage1Passed} işlendi`
471
- : "Tarama ilerliyor...")}
472
- </div>
473
- </div>
474
- )}
475
  <h1 className="text-2xl font-bold flex items-center gap-2">
476
  <Shield className="w-7 h-7 text-green-400" />
477
  ML Eligible Hisseler &amp; Trading Sinyalleri
@@ -485,17 +477,102 @@ export default function EligiblePage() {
485
  {signals?.timestamp && ` | Sinyaller: ${new Date(signals.timestamp).toLocaleString('tr-TR')}`}
486
  </p>
487
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
  </div>
489
- <button
490
- onClick={() => { loadEligible().then((res) => { if (res?.eligible?.length) loadSignals(res.eligible.map((e) => e.symbol)) }) }}
491
- disabled={signalsLoading}
492
- className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm disabled:opacity-50"
493
- >
494
- <RefreshCw className={`w-4 h-4 ${signalsLoading ? 'animate-spin' : ''}`} />
495
- {signalsLoading ? 'Analiz ediliyor...' : 'Yenile'}
496
- </button>
497
  </div>
498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  {/* Summary Cards */}
500
  <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-3 mb-6">
501
  <SummaryCard icon={<Shield className="w-5 h-5 text-green-400" />} value={summary.eligibleCount} label="Eligible" color="text-green-400" />
 
22
  CircleDot,
23
  Eye,
24
  X,
25
+ Search,
26
+ Clock,
27
  } from 'lucide-react'
28
 
29
  // ─── Types ─────────────────────────────────
 
234
  const [viewMode, setViewMode] = useState<'cards' | 'table'>('cards')
235
  const [scanLoading, setScanLoading] = useState(false)
236
  const [signalFilter, setSignalFilter] = useState<string>('all')
237
+ const [showScanMenu, setShowScanMenu] = useState(false)
238
+ const isScanRunning = Boolean(data?.scanRunning) || scanLoading
239
  const isScanCompleted = Boolean(data?.scanCompleted)
240
  const stage2Done = data?.stage2Done ?? null
241
  const stage1Passed = data?.stage1PassedCount ?? null
 
464
  {/* Header */}
465
  <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
466
  <div>
 
 
 
 
 
 
 
 
 
 
 
 
467
  <h1 className="text-2xl font-bold flex items-center gap-2">
468
  <Shield className="w-7 h-7 text-green-400" />
469
  ML Eligible Hisseler &amp; Trading Sinyalleri
 
477
  {signals?.timestamp && ` | Sinyaller: ${new Date(signals.timestamp).toLocaleString('tr-TR')}`}
478
  </p>
479
  )}
480
+ <p className="text-gray-400 text-xs mt-0.5 flex items-center gap-1">
481
+ <Clock className="w-3 h-3" /> Otomatik tarama: Her gün 00:00 (TR)
482
+ </p>
483
+ </div>
484
+ <div className="flex items-center gap-2">
485
+ {/* Scan Button */}
486
+ <div className="relative">
487
+ <button
488
+ onClick={() => setShowScanMenu(!showScanMenu)}
489
+ disabled={isScanRunning}
490
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
491
+ isScanRunning
492
+ ? 'bg-blue-100 text-blue-600 cursor-wait'
493
+ : 'bg-blue-600 hover:bg-blue-700 text-white'
494
+ }`}
495
+ >
496
+ {isScanRunning ? (
497
+ <><RefreshCw className="w-4 h-4 animate-spin" /> Taranıyor...</>
498
+ ) : (
499
+ <><Search className="w-4 h-4" /> Tara</>
500
+ )}
501
+ </button>
502
+ {showScanMenu && !isScanRunning && (
503
+ <div className="absolute right-0 top-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-20 py-1 min-w-[200px]">
504
+ <button
505
+ onClick={() => { setShowScanMenu(false); startScan('bist30') }}
506
+ className="w-full px-4 py-2.5 text-left text-sm hover:bg-gray-50 flex items-center gap-2"
507
+ >
508
+ <Zap className="w-4 h-4 text-blue-500" />
509
+ <div>
510
+ <div className="font-medium">BIST30 Hızlı Tara</div>
511
+ <div className="text-xs text-gray-400">~15-30 dakika</div>
512
+ </div>
513
+ </button>
514
+ <button
515
+ onClick={() => { setShowScanMenu(false); startScan('bist100') }}
516
+ className="w-full px-4 py-2.5 text-left text-sm hover:bg-gray-50 flex items-center gap-2"
517
+ >
518
+ <BarChart3 className="w-4 h-4 text-green-500" />
519
+ <div>
520
+ <div className="font-medium">BIST100 Tam Tara</div>
521
+ <div className="text-xs text-gray-400">~60-90 dakika</div>
522
+ </div>
523
+ </button>
524
+ <button
525
+ onClick={() => { setShowScanMenu(false); startScan('bist_all') }}
526
+ className="w-full px-4 py-2.5 text-left text-sm hover:bg-gray-50 flex items-center gap-2 border-t border-gray-100"
527
+ >
528
+ <Activity className="w-4 h-4 text-purple-500" />
529
+ <div>
530
+ <div className="font-medium">Tüm BIST Tara</div>
531
+ <div className="text-xs text-gray-400">~2-4 saat</div>
532
+ </div>
533
+ </button>
534
+ </div>
535
+ )}
536
+ </div>
537
+ {/* Refresh Button */}
538
+ <button
539
+ onClick={() => { loadEligible().then((res) => { if (res?.eligible?.length) loadSignals(res.eligible.map((e) => e.symbol)) }) }}
540
+ disabled={signalsLoading}
541
+ className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm disabled:opacity-50"
542
+ >
543
+ <RefreshCw className={`w-4 h-4 ${signalsLoading ? 'animate-spin' : ''}`} />
544
+ {signalsLoading ? 'Analiz ediliyor...' : 'Yenile'}
545
+ </button>
546
  </div>
 
 
 
 
 
 
 
 
547
  </div>
548
 
549
+ {/* Scan progress banner */}
550
+ {isScanRunning && (
551
+ <div className="mb-4 rounded-lg border border-blue-200 bg-blue-50 p-3 flex items-center gap-3">
552
+ <RefreshCw className="w-5 h-5 text-blue-500 animate-spin flex-shrink-0" />
553
+ <div className="flex-1 min-w-0">
554
+ <div className="text-sm font-medium text-blue-800">Tarama devam ediyor</div>
555
+ <div className="text-xs text-blue-600">
556
+ {data.scanProgress ||
557
+ (stage2Done !== null && stage1Passed !== null
558
+ ? `Stage 2: ${stage2Done}/${stage1Passed} hisse işlendi`
559
+ : 'Tarama ilerliyor...')}
560
+ </div>
561
+ </div>
562
+ {stage2Done !== null && stage1Passed !== null && stage1Passed > 0 && (
563
+ <div className="w-24 h-2 bg-blue-200 rounded-full overflow-hidden flex-shrink-0">
564
+ <div className="h-full bg-blue-500 rounded-full transition-all" style={{ width: `${Math.round((stage2Done / stage1Passed) * 100)}%` }} />
565
+ </div>
566
+ )}
567
+ </div>
568
+ )}
569
+
570
+ {!isScanRunning && !isScanCompleted && data?.ok && (
571
+ <div className="mb-4 rounded-lg border border-yellow-200 bg-yellow-50 p-3 text-sm text-yellow-800">
572
+ Kısmi sonuçlar gösteriliyor. Tam sonuçlar için yeni tarama başlatın.
573
+ </div>
574
+ )}
575
+
576
  {/* Summary Cards */}
577
  <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-3 mb-6">
578
  <SummaryCard icon={<Shield className="w-5 h-5 text-green-400" />} value={summary.eligibleCount} label="Eligible" color="text-green-400" />