quantforge-miner / database.py
DevWizard-Vandan
Log loaded Gemini keys on startup and add backup for miner_production.log
5746ed9
Raw
History Blame Contribute Delete
12.7 kB
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()