File size: 7,006 Bytes
d86b5a2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import asyncio
import json
import logging
from pathlib import Path
import os
import sys
from typing import Dict, Any, List

# Setup logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
logger = logging.getLogger("Scraper")

# Setup system paths
sys.path.append(str(Path(__file__).resolve().parent.parent))
import config
from core.wq_client import AsyncBrainClient, _parse_retry_after

async def scrape_fields():
    # Active regions, universes, and delays to capture all 100k+ fields
    regions = ["USA", "GLB", "EUR", "ASI", "CHN", "AMR", "IND", "MEA"]
    universes = ["TOP3000", "TOP2000", "TOP1000", "TOP500", "TOP200", "TOPSP500", "GLB3000", "GLB2000"]
    delays = [0, 1]

    # Load existing cache if any to build on top of it
    cache_file = config.DATA_DIR / "data_fields_cache.json"
    all_categorized_fields = {}
    if cache_file.exists():
        try:
            with open(cache_file, "r") as f:
                all_categorized_fields = json.load(f)
            total = sum(len(v) for v in all_categorized_fields.values())
            logger.info(f"Loaded existing cache containing {total} fields.")
        except Exception:
            all_categorized_fields = {}

    async with AsyncBrainClient() as client:
        # Authenticate first
        authenticated = await client.authenticate()
        if not authenticated:
            logger.error("Authentication with WorldQuant BRAIN failed. Please ensure credentials are set.")
            return

        session = await client.get_session()
        
        # Track unique field IDs
        seen_ids = set()
        for cat, fields in all_categorized_fields.items():
            for f in fields:
                seen_ids.add(f["id"])

        logger.info("Starting recursive pagination across all categories, regions, and universes...")
        
        # Paginate through combinations
        for region in regions:
            for universe in universes:
                for delay in delays:
                    logger.info(f"Scraping Region: {region}, Universe: {universe}, Delay: {delay}...")
                    offset = 0
                    limit = 50
                    consecutive_empty = 0
                    
                    while True:
                        url = f"{client.base_url}/data-fields"
                        params = {
                            "instrumentType": "EQUITY",
                            "region": region,
                            "delay": delay,
                            "universe": universe,
                            "limit": limit,
                            "offset": offset
                        }
                        
                        try:
                            async with session.get(url, params=params) as response:
                                if response.status == 401:
                                    logger.info("Session expired. Authenticating...")
                                    await client.authenticate()
                                    continue
                                    
                                if response.status == 429 or "Retry-After" in response.headers:
                                    retry_after = _parse_retry_after(response.headers, 15)
                                    logger.warning(f"Rate limited. Waiting {retry_after} seconds...")
                                    await asyncio.sleep(retry_after)
                                    continue
                                    
                                if response.status != 200:
                                    body = await response.text()
                                    logger.error(f"HTTP {response.status}: {body[:200]}")
                                    break
                                    
                                data = await response.json()
                                results = data.get("results", [])
                                if not results:
                                    consecutive_empty += 1
                                    if consecutive_empty >= 2:
                                        break
                                    offset += limit
                                    continue
                                    
                                consecutive_empty = 0
                                new_added = 0
                                
                                for field in results:
                                    fid = field.get("id")
                                    if not fid or fid in seen_ids:
                                        continue
                                        
                                    desc = field.get("description", "")
                                    category = field.get("category", {})
                                    cat_name = category.get("name", "Other") if isinstance(category, dict) else "Other"
                                    field_type = field.get("type", "MATRIX")
                                    
                                    if cat_name not in all_categorized_fields:
                                        all_categorized_fields[cat_name] = []
                                        
                                    all_categorized_fields[cat_name].append({
                                        "id": fid,
                                        "desc": desc,
                                        "type": field_type
                                    })
                                    seen_ids.add(fid)
                                    new_added += 1
                                    
                                total_now = sum(len(v) for v in all_categorized_fields.values())
                                logger.info(f"[{region}/{universe}] Fetched {len(results)} fields (offset {offset}). Added {new_added} new. Total Cache Size: {total_now}")
                                
                                if len(results) < limit:
                                    break
                                    
                                offset += limit
                                await asyncio.sleep(0.5)  # Politeness interval
                                
                        except Exception as e:
                            logger.error(f"Network error: {e}")
                            await asyncio.sleep(5)
                            break
                            
                    # Save progress after finishing a universe
                    if seen_ids:
                        with open(cache_file, "w") as f:
                            json.dump(all_categorized_fields, f, indent=2)

    total_scraped = sum(len(v) for v in all_categorized_fields.values())
    logger.info(f"Fields scrape complete! Total fields saved to cache: {total_scraped}")

if __name__ == "__main__":
    asyncio.run(scrape_fields())