File size: 12,727 Bytes
ddccbe9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2284f0e
 
 
 
 
 
 
 
 
 
 
 
 
a5a14e0
 
 
 
 
 
 
 
 
 
 
 
 
 
2284f0e
ddccbe9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87f0da7
ddccbe9
 
87f0da7
 
 
 
 
 
ddccbe9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2284f0e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a5a14e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5746ed9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ddccbe9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b1fdb40
ddccbe9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d705f63
ddccbe9
e2599a1
0c4e32b
ddccbe9
 
 
 
 
 
 
 
d705f63
b1fdb40
 
 
 
 
 
 
 
 
 
 
 
 
e2599a1
d705f63
 
 
 
 
 
e2599a1
d705f63
 
 
 
 
e2599a1
 
 
 
 
 
 
0c4e32b
 
 
 
 
 
fdda2e5
 
 
 
 
 
d705f63
 
e2599a1
d705f63
ddccbe9
 
0ae74ed
 
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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
from datetime import datetime, timezone
from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime
from sqlalchemy.orm import sessionmaker, declarative_base
from config import DATA_DIR, logger

import os
import threading
import time
import shutil

# Setup database path
DB_PATH = DATA_DIR / "alpha_results.db"

# Restore database from Hugging Face Dataset if applicable
HF_TOKEN = os.getenv("HF_TOKEN")
SPACE_ID = os.getenv("SPACE_ID")
DEFAULT_DATASET_ID = f"{SPACE_ID}-backup" if SPACE_ID else None
HF_DATASET_ID = os.getenv("HF_DATASET_ID") or DEFAULT_DATASET_ID

def restore_db_from_hf():
    if not HF_TOKEN:
        logger.warning("HF_TOKEN environment variable is not set. Database restore from Hugging Face Dataset skipped.")
        return
    if not HF_DATASET_ID:
        logger.warning("HF_DATASET_ID and SPACE_ID environment variables are not set. Database restore skipped.")
        return
    
    logger.info(f"Checking for database backup in Hugging Face Dataset: {HF_DATASET_ID}...")
    try:
        from huggingface_hub import hf_hub_download
        cached_path = hf_hub_download(
            repo_id=HF_DATASET_ID,
            filename="alpha_results.db",
            repo_type="dataset",
            token=HF_TOKEN
        )
        DATA_DIR.mkdir(parents=True, exist_ok=True)
        shutil.copy(cached_path, DB_PATH)
        logger.info(f"Successfully restored database from Hugging Face Dataset to {DB_PATH}.")
    except ImportError:
        logger.warning("huggingface_hub package is not installed. Database restore skipped.")
    except Exception as e:
        logger.warning(f"Could not restore database from Hugging Face Dataset (this is normal if it is the first run): {e}")

    try:
        from huggingface_hub import hf_hub_download
        settings_path = DATA_DIR / "miner_settings.json"
        cached_settings_path = hf_hub_download(
            repo_id=HF_DATASET_ID,
            filename="miner_settings.json",
            repo_type="dataset",
            token=HF_TOKEN
        )
        shutil.copy(cached_settings_path, settings_path)
        logger.info("Successfully restored miner_settings.json from Hugging Face Dataset.")
    except Exception as e:
        logger.warning(f"Could not restore miner_settings.json from Hugging Face Dataset: {e}")
        
    try:
        from huggingface_hub import hf_hub_download
        cache_path = DATA_DIR / "data_fields_cache.json"
        cached_cache_path = hf_hub_download(
            repo_id=HF_DATASET_ID,
            filename="data_fields_cache.json",
            repo_type="dataset",
            token=HF_TOKEN
        )
        shutil.copy(cached_cache_path, cache_path)
        logger.info("Successfully restored data_fields_cache.json from Hugging Face Dataset.")
    except Exception as e:
        logger.warning(f"Could not restore data_fields_cache.json from Hugging Face Dataset: {e}")

# Try to restore backup before SQLAlchemy engine is initialized
restore_db_from_hf()

DATABASE_URL = f"sqlite:///{DB_PATH}"

def backup_db_to_hf():
    if not HF_TOKEN:
        logger.warning("HF_TOKEN environment variable is not set. Database backup to Hugging Face Dataset skipped.")
        return
    if not HF_DATASET_ID:
        logger.warning("HF_DATASET_ID and SPACE_ID environment variables are not set. Database backup skipped.")
        return

    try:
        from huggingface_hub import HfApi
    except ImportError:
        logger.warning("huggingface_hub package is not installed. Background backup thread will not start.")
        return

    logger.info(f"Starting background database backup thread (interval: 15 mins) to: {HF_DATASET_ID}...")
    api = HfApi(token=HF_TOKEN)
    
    # Create the dataset repo if it doesn't exist
    try:
        api.create_repo(
            repo_id=HF_DATASET_ID,
            repo_type="dataset",
            private=True,
            exist_ok=True
        )
    except Exception as e:
        logger.warning(f"Could not verify/create Hugging Face Dataset repository: {e}")

    is_first_backup = True
    while True:
        try:
            # Sleep for 60 seconds on the first loop, and 15 minutes thereafter
            if is_first_backup:
                time.sleep(60)
                is_first_backup = False
            else:
                time.sleep(15 * 60)
            
            if not DB_PATH.exists():
                logger.debug("Database file does not exist yet. Skipping backup cycle.")
                continue
                
            logger.info(f"Backing up database to Hugging Face Dataset: {HF_DATASET_ID}...")
            temp_db_path = DB_PATH.parent / "alpha_results_backup_temp.db"
            try:
                shutil.copy2(DB_PATH, temp_db_path)
                api.upload_file(
                    path_or_fileobj=str(temp_db_path),
                    path_in_repo="alpha_results.db",
                    repo_id=HF_DATASET_ID,
                    repo_type="dataset",
                )
                logger.info("Database backup successfully uploaded to Hugging Face Dataset.")
            except Exception as upload_error:
                logger.error(f"Failed to upload database file to Hugging Face: {upload_error}")
            finally:
                if temp_db_path.exists():
                    os.remove(temp_db_path)
                    
            # Also backup miner_settings.json if it exists
            settings_path = DATA_DIR / "miner_settings.json"
            if settings_path.exists():
                logger.info("Backing up miner_settings.json to Hugging Face Dataset...")
                try:
                    api.upload_file(
                        path_or_fileobj=str(settings_path),
                        path_in_repo="miner_settings.json",
                        repo_id=HF_DATASET_ID,
                        repo_type="dataset",
                    )
                    logger.info("miner_settings.json backup successfully uploaded to Hugging Face Dataset.")
                except Exception as upload_error:
                    logger.error(f"Failed to upload miner_settings.json to Hugging Face: {upload_error}")
                    
            # Also backup data_fields_cache.json if it exists
            cache_path = DATA_DIR / "data_fields_cache.json"
            if cache_path.exists():
                logger.info("Backing up data_fields_cache.json to Hugging Face Dataset...")
                try:
                    api.upload_file(
                        path_or_fileobj=str(cache_path),
                        path_in_repo="data_fields_cache.json",
                        repo_id=HF_DATASET_ID,
                        repo_type="dataset",
                    )
                    logger.info("data_fields_cache.json backup successfully uploaded to Hugging Face Dataset.")
                except Exception as upload_error:
                    logger.error(f"Failed to upload data_fields_cache.json to Hugging Face: {upload_error}")
                    
            # Also backup miner_production.log if it exists
            prod_log_path = DATA_DIR / "miner_production.log"
            if prod_log_path.exists():
                logger.info("Backing up miner_production.log to Hugging Face Dataset...")
                try:
                    temp_log_path = DATA_DIR / "miner_production_backup_temp.log"
                    shutil.copy2(prod_log_path, temp_log_path)
                    api.upload_file(
                        path_or_fileobj=str(temp_log_path),
                        path_in_repo="miner_production.log",
                        repo_id=HF_DATASET_ID,
                        repo_type="dataset",
                    )
                    logger.info("miner_production.log backup successfully uploaded to Hugging Face Dataset.")
                except Exception as upload_error:
                    logger.error(f"Failed to upload miner_production.log to Hugging Face: {upload_error}")
                finally:
                    if temp_log_path.exists():
                        try:
                            os.remove(temp_log_path)
                        except Exception:
                            pass
                    
        except Exception as e:
            logger.error(f"Error during background database backup to Hugging Face: {e}")

def start_backup_thread():
    if HF_TOKEN and HF_DATASET_ID:
        t = threading.Thread(target=backup_db_to_hf, daemon=True, name="HF_DB_Backup")
        t.start()
        logger.info("Database backup thread started successfully.")
    else:
        logger.warning("HF_TOKEN or HF_DATASET_ID/SPACE_ID not configured. Backup thread not started.")

# Start the periodic background backup thread
start_backup_thread()

# Initialize SQLAlchemy Engine and Base
engine = create_engine(DATABASE_URL, echo=False, connect_args={"timeout": 30})
Base = declarative_base()
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

class AlphaRecord(Base):
    __tablename__ = "alpha_records"

    id = Column(Integer, primary_key=True, index=True)
    expression = Column(String, unique=True, nullable=False)
    region = Column(String, default='USA')
    universe = Column(String, default='TOP3000')
    delay = Column(Integer, default=1)
    decay = Column(Integer, default=10)
    truncation = Column(Float, default=0.01)
    neutralization = Column(String, default='SUBINDUSTRY')
    
    # Results
    sharpe = Column(Float, nullable=True)
    fitness = Column(Float, nullable=True)
    turnover = Column(Float, nullable=True)
    returns = Column(Float, nullable=True)
    
    # Metadata
    status = Column(String, default='GENERATED') # e.g., 'GENERATED', 'SIMULATING', 'FAILED', 'PASSED', 'SUBMITTED'
    error_log = Column(String, nullable=True)
    alpha_id = Column(String, nullable=True)
    date_created = Column(DateTime, default=lambda: datetime.now(timezone.utc))
    date_submitted = Column(DateTime, nullable=True)
    retry_count = Column(Integer, default=0)

    def __repr__(self):
        return f"<AlphaRecord(id={self.id}, expression='{self.expression}', status='{self.status}')>"

def init_db():
    """Initializes the SQLite database and creates all tables."""
    logger.info(f"Initializing database at {DB_PATH}...")
    Base.metadata.create_all(bind=engine)
    
    # Enable WAL mode for better concurrency
    try:
        import sqlite3
        conn = sqlite3.connect(str(DB_PATH))
        cursor = conn.cursor()
        cursor.execute("PRAGMA journal_mode=WAL")
        cursor.execute("PRAGMA synchronous=NORMAL")
        conn.commit()
        conn.close()
        logger.info("SQLite WAL mode and synchronous settings configured successfully.")
    except Exception as e:
        logger.error(f"Failed to configure SQLite WAL mode: {e}")
        
    # Check if alpha_id or date_submitted columns exist, if not, add them safely
    try:
        import sqlite3
        conn = sqlite3.connect(str(DB_PATH))
        cursor = conn.cursor()
        cursor.execute("PRAGMA table_info(alpha_records)")
        columns = [col[1] for col in cursor.fetchall()]
        
        if "alpha_id" not in columns:
            logger.info("Adding alpha_id column to alpha_records table...")
            cursor.execute("ALTER TABLE alpha_records ADD COLUMN alpha_id VARCHAR")
            conn.commit()
            logger.info("alpha_id column added successfully.")
            
        if "date_submitted" not in columns:
            logger.info("Adding date_submitted column to alpha_records table...")
            cursor.execute("ALTER TABLE alpha_records ADD COLUMN date_submitted TIMESTAMP")
            conn.commit()
            logger.info("date_submitted column added successfully.")
            
        if "retry_count" not in columns:
            logger.info("Adding retry_count column to alpha_records table...")
            cursor.execute("ALTER TABLE alpha_records ADD COLUMN retry_count INTEGER DEFAULT 0")
            conn.commit()
            logger.info("retry_count column added successfully.")
            
        # Migrate SP500 to TOPSP500 in existing records
        logger.info("Running database migration to replace SP500 with TOPSP500...")
        cursor.execute("UPDATE alpha_records SET universe = 'TOPSP500' WHERE universe = 'SP500'")
        conn.commit()
        logger.info(f"Universe migration query completed. Rows modified: {cursor.rowcount}")
        
        conn.close()
    except Exception as e:
        logger.error(f"Error checking/adding columns to database: {e}")
        
    logger.info("Database initialized successfully.")

# Automatically initialize the database and apply migrations on import
init_db()