Spaces:
Sleeping
Sleeping
| import logging | |
| import threading | |
| import time | |
| from django.core.cache import cache | |
| from django.utils import timezone | |
| logger = logging.getLogger(__name__) | |
| BATCH_SIZE = 500 | |
| BATCH_DELAY_SECONDS = 1.5 | |
| FETCH_INTERVAL = 600 # 10 minutes | |
| REDIS_ALL_STOCKS_KEY = "screener:all_stocks" | |
| REDIS_ALL_STOCKS_TTL = 900 # 15 minutes | |
| _fetcher_lock = threading.Lock() | |
| _fetcher_started = False | |
| def fetch_all_stocks(): | |
| from .models import ScreenerSymbolCache | |
| from .views import ( | |
| TradingViewError, | |
| _build_tradingview_scan_payload, | |
| _normalize_tradingview_row, | |
| _scan_tradingview, | |
| ) | |
| filter_list = [{"left": "is_primary", "operation": "equal", "right": True}] | |
| all_rows = [] | |
| offset = 0 | |
| total_count = None | |
| while True: | |
| payload = _build_tradingview_scan_payload( | |
| filter_list, | |
| offset=offset, | |
| limit=BATCH_SIZE, | |
| sort_by="market_cap_basic", | |
| sort_order="desc", | |
| ) | |
| try: | |
| result = _scan_tradingview(payload) | |
| except TradingViewError as exc: | |
| logger.warning("TradingView error at offset %d: %s", offset, exc) | |
| break | |
| if total_count is None: | |
| total_count = result.get("totalCount", 0) | |
| logger.info("TradingView reports %d total stocks", total_count) | |
| data = result.get("data") or [] | |
| if not data: | |
| break | |
| for raw_row in data: | |
| normalized = _normalize_tradingview_row(raw_row) | |
| if normalized: | |
| all_rows.append(normalized) | |
| offset += BATCH_SIZE | |
| if total_count and offset >= total_count: | |
| break | |
| time.sleep(BATCH_DELAY_SECONDS) | |
| if all_rows: | |
| cache.set(REDIS_ALL_STOCKS_KEY, all_rows, timeout=REDIS_ALL_STOCKS_TTL) | |
| now = timezone.now() | |
| db_objects = [] | |
| for row in all_rows: | |
| symbol = (row.get("symbol") or "").strip().upper() | |
| if not symbol: | |
| continue | |
| db_objects.append( | |
| ScreenerSymbolCache( | |
| symbol=symbol, | |
| data=row, | |
| last_fetched_at=now, | |
| ) | |
| ) | |
| chunk_size = 500 | |
| for i in range(0, len(db_objects), chunk_size): | |
| ScreenerSymbolCache.objects.bulk_create( | |
| db_objects[i : i + chunk_size], | |
| update_conflicts=True, | |
| unique_fields=["symbol"], | |
| update_fields=["data", "last_fetched_at"], | |
| ) | |
| logger.info("Stored %d stocks in Redis + DB", len(all_rows)) | |
| return {"fetched": len(all_rows), "total_reported": total_count} | |
| def _fetcher_loop(): | |
| from django.conf import settings | |
| interval = getattr(settings, "STOCK_FETCHER_INTERVAL", FETCH_INTERVAL) | |
| time.sleep(5) # let Django finish startup | |
| while True: | |
| try: | |
| result = fetch_all_stocks() | |
| logger.info("Stock fetcher completed: %s", result) | |
| except Exception: | |
| logger.exception("Stock fetcher cycle failed") | |
| time.sleep(interval) | |
| def start_stock_fetcher(): | |
| global _fetcher_started | |
| with _fetcher_lock: | |
| if _fetcher_started: | |
| return | |
| _fetcher_started = True | |
| thread = threading.Thread( | |
| target=_fetcher_loop, | |
| daemon=True, | |
| name="stock-fetcher", | |
| ) | |
| thread.start() | |
| logger.info( | |
| "Background stock fetcher started (interval=%ds)", FETCH_INTERVAL | |
| ) | |