NamVan56/QuantForge
0
1import asyncio2import logging3import time4import re5from database import init_db, SessionLocal, AlphaRecord6from wq_client import BrainClient7from agent import AlphaAgent, USED_LOCAL_FALLBACK8from submission_engine import SubmissionEngine9import config10import random11 12# Configure standard logger for orchestrator13logger = logging.getLogger("Orchestrator")14 15# Global Control Flags16MINER_RUNNING = False17MINER_TASK = None18GLOBAL_SUBMISSION_ENGINE = None19LAST_GENERATION_TIME = 0.020 21# Dynamic throttle: short for local fallback, longer for LLM22THROTTLE_LOCAL_FALLBACK = 10 # 10 seconds between local fallback generations23THROTTLE_LLM = 120 # 2 minutes between LLM-backed generations24 25def normalize_settings(settings: dict) -> dict:26 """27 Normalizes and validates settings to ensure compatibility with WorldQuant BRAIN API.28 """29 if not isinstance(settings, dict):30 settings = {}31 32 region = settings.get("region", "USA")33 if region == "US" or not region:34 region = "USA"35 else:36 region = str(region).upper()37 if region not in ["USA"]:38 region = "USA"39 40 universe = settings.get("universe", "TOP3000")41 if universe in ["univ_standard", "univ_top_90", "univ_top_2000"] or not universe:42 universe = "TOP3000"43 else:44 universe = str(universe).upper()45 if universe not in ["TOP3000", "TOP2000", "TOP1000", "TOP500", "TOP200", "TOPSP500"]:46 universe = "TOP3000"47 48 neutralization = settings.get("neutralization", "SUBINDUSTRY")49 if neutralization:50 neutralization = str(neutralization).upper()51 if neutralization not in ["SUBINDUSTRY", "INDUSTRY", "SECTOR", "MARKET", "NONE"]:52 neutralization = "SUBINDUSTRY"53 else:54 neutralization = "SUBINDUSTRY"55 56 try:57 delay = int(settings.get("delay", 1))58 if delay not in [0, 1]:59 delay = 160 except (ValueError, TypeError):61 delay = 162 63 64 try:65 decay = int(settings.get("decay", 10))66 if decay > 512:67 decay = 51268 elif decay < 0:69 decay = 070 except (ValueError, TypeError):71 decay = 1072 73 try:74 truncation = float(settings.get("truncation", 0.01))75 except (ValueError, TypeError):76 truncation = 0.0177 78 return {79 "region": region,80 "universe": universe,81 "neutralization": neutralization,82 "delay": delay,83 "decay": decay,84 "truncation": truncation85 }86 87VALID_OPERATORS = {88 # Mathematical89 "abs", "log", "sign", "sqrt", "exp", "power", "min", "max",90 # Cross-sectional91 "rank", "zscore", "group_rank", "group_neutralize", "group_zscore", "vector_neut",92 # Time-series93 "ts_delta", "ts_mean", "ts_std_dev", "ts_decay_linear", "signed_power", 94 "ts_skewness", "ts_kurt", "ts_corr", "ts_covariance", "ts_max", "ts_min", 95 "ts_rank", "ts_sum", "ts_product", "ts_argmax", "ts_argmin", "ts_scale",96 # Vector97 "vec_avg", "vec_sum", "vec_choose",98 # Constants/Groups99 "sector", "subindustry", "industry", "market", "cap",100 # Price-volume/basic fields101 "close", "open", "high", "low", "volume", "vwap", "returns",102 "adv20", "adv60", "sharesout", "adjfactor"103}104 105def sanitize_expression(expr: str) -> str:106 """107 Sanitizes WorldQuant expressions to ensure they are compatible with the platform.108 Converts scientific notation like '1e-9' or '1e-5' to full decimal notation (e.g. '0.000000001').109 """110 if not expr:111 return expr112 113 def replace_sci(match):114 sci_str = match.group(0)115 try:116 val = float(sci_str)117 dec_str = f"{val:.15f}".rstrip('0')118 if dec_str.endswith('.'):119 dec_str += '0'120 return dec_str121 except ValueError:122 return sci_str123 124 pattern = r'\b\d+(?:\.\d+)?[eE][+-]?\d+\b'125 return re.sub(pattern, replace_sci, expr)126 127def validate_expression(expr: str) -> bool:128 """129 Validates basic syntax rules and variable names of a generated alpha formula130 before submitting for simulation.131 """132 if not expr or len(expr.strip()) < 5:133 return False134 135 # Check balanced parentheses and brackets136 if expr.count('(') != expr.count(')'):137 return False138 if expr.count('[') != expr.count(']'):139 return False140 141 # Check for empty wrappers142 if "()" in expr or "[]" in expr:143 return False144 145 # Check for redundant nested wrappers146 if "rank(rank(" in expr or "zscore(zscore(" in expr:147 return False148 149 # Check for invalid variable names and vector variable operator wrapping if the cache exists150 import json151 cache_file = config.DATA_DIR / "data_fields_cache.json"152 if cache_file.exists():153 try:154 with open(cache_file, "r") as f:155 cached_data = json.load(f)156 if isinstance(cached_data, dict) and cached_data:157 # Gather all valid field IDs and vector fields from cache158 valid_fields = set()159 vector_fields = set()160 for cat_fields in cached_data.values():161 for f in cat_fields:162 fid = f.get("id")163 valid_fields.add(fid)164 if f.get("type") == "VECTOR":165 vector_fields.add(fid)166 167 # Extract all word tokens using regex168 tokens = re.findall(r'\b[a-zA-Z_][a-zA-Z0-9_]*\b', expr)169 for t in tokens:170 # Ignore numeric tokens/parameters and built-in operators/constants171 if t.lower() in VALID_OPERATORS or t in VALID_OPERATORS:172 continue173 # Ignore standard indices like sector, industry, etc.174 if t.lower() in ("sector", "subindustry", "industry", "market"):175 continue176 # Check if token is in valid cached fields177 if t not in valid_fields:178 logger.warning(f"Validation failed: Unknown variable '{t}' in formula '{expr}'")179 return False180 # Check if Vector field is wrapped in a vector operator (vec_avg, vec_sum, vec_choose)181 if t in vector_fields:182 pattern = r'\bvec_(avg|sum|choose)\s*\([^)]*\b' + re.escape(t) + r'\b'183 if not re.search(pattern, expr):184 logger.warning(f"Validation failed: Vector field '{t}' in formula '{expr}' is not wrapped in a vector operator.")185 return False186 except Exception as e:187 logger.warning(f"Error loading cache for variable validation: {e}")188 189 return True190 191def is_worth_refining(error_log: str, sharpe: float = None, fitness: float = None, turnover: float = None) -> bool:192 if not error_log:193 return False194 195 err_lower = error_log.lower()196 197 # 1. Syntax, unit, operator, variable, or platform check errors are worth refining198 syntax_keywords = [199 "unit", "variable", "operator", "syntax", "invalid input", 200 "must be an expression", "divide", "expected", "less than or equal to",201 "failed_checks", "failed checks", "concentrated_weight", "low_sub_universe_sharpe",202 "platform_check_failure", "concentrated", "sub-universe"203 ]204 if any(k in err_lower for k in syntax_keywords):205 # If it failed due to check failures, only refine if it has promising metrics206 if "check" in err_lower or "concentrated" in err_lower or "sub_universe" in err_lower or "sub-universe" in err_lower:207 if sharpe is not None and (sharpe < 0.8 or fitness < 0.3):208 return False209 return True210 211 # 2. Near-miss performance alphas (Tier 2 and above) are worth refining212 if sharpe is not None and fitness is not None and turnover is not None:213 is_near_miss = (sharpe >= 0.9 and fitness >= 0.4 and turnover >= 0.01 and turnover <= 0.70)214 if is_near_miss:215 return True216 217 return False218 219def clean_and_normalize_database():220 """221 Sweeps the entire database, normalizes all record settings,222 and resets any records that failed due to payload/settings format validation.223 """224 logger.info("Starting database settings normalization and status cleanup...")225 try:226 db = SessionLocal()227 records = db.query(AlphaRecord).all()228 229 # Phase 1: Group records by their target normalized expression230 groups = {}231 for record in records:232 expr = record.expression233 234 # Normalize invalid variable names to valid counterparts235 normalized = expr236 normalized = re.sub(r'\bmarket_cap(?:_usd)?\b', 'cap', normalized)237 normalized = re.sub(r'\bnet_income\b', 'ebit', normalized)238 normalized = re.sub(r'\bnetincome\b', 'ebit', normalized)239 normalized = re.sub(r'\bincome_q\b', 'ebit', normalized)240 normalized = re.sub(r'\bincome\b', 'ebit', normalized)241 normalized = re.sub(r'\bni\b', 'ebit', normalized)242 normalized = re.sub(r'\bsales\b', 'cashflow', normalized)243 normalized = re.sub(r'\beps_ttm\b', 'ebit', normalized)244 normalized = re.sub(r'\bearnings_per_share_ttm\b', 'ebit', normalized)245 normalized = re.sub(r'\bearnings_income\b', 'ebit', normalized)246 normalized = re.sub(r'\bearnings_ttm_diluted_eps\b', 'ebit', normalized)247 normalized = re.sub(r'\bcash_flow\b', 'cashflow', normalized)248 normalized = sanitize_expression(normalized)249 250 if normalized not in groups:251 groups[normalized] = []252 groups[normalized].append((record, normalized != expr))253 254 deleted_count = 0255 updated_count = 0256 257 records_to_keep = []258 records_to_delete = []259 260 # Phase 2: For each group, choose the best record to keep and delete others261 for normalized_expr, group in groups.items():262 if len(group) == 1:263 records_to_keep.append(group[0])264 else:265 # Group has multiple records. Rank them.266 status_ranks = {267 "SUBMITTED": 5,268 "PASSED_CORRELATION": 4,269 "PENDING_CORRELATION": 3,270 "GENERATED": 2,271 "SIMULATING": 1,272 "FAILED": 0273 }274 275 def sort_key(item):276 rec, is_normalized_different = item277 is_already_normalized = not is_normalized_different278 status_rank = status_ranks.get(rec.status, 0)279 return (is_already_normalized, status_rank, -rec.id)280 281 sorted_group = sorted(group, key=sort_key, reverse=True)282 283 # Keep the first, delete the rest284 records_to_keep.append(sorted_group[0])285 for rec, _ in sorted_group[1:]:286 records_to_delete.append(rec)287 288 # Phase 3: Delete duplicates first and commit to free up the unique constraint289 if records_to_delete:290 logger.info(f"Identifying {len(records_to_delete)} duplicate/conflicting records to delete...")291 for rec in records_to_delete:292 logger.debug(f"Deleting duplicate record: {rec.expression}")293 db.delete(rec)294 db.commit()295 deleted_count = len(records_to_delete)296 297 # Phase 4: Normalize the kept records and update them in the database298 existing_exprs = {rec.expression for rec, _ in records_to_keep}299 for rec, is_normalized_different in records_to_keep:300 mutated = False301 302 if is_normalized_different:303 normalized = rec.expression304 normalized = re.sub(r'\bmarket_cap(?:_usd)?\b', 'cap', normalized)305 normalized = re.sub(r'\bnet_income\b', 'ebit', normalized)306 normalized = re.sub(r'\bnetincome\b', 'ebit', normalized)307 normalized = re.sub(r'\bincome_q\b', 'ebit', normalized)308 normalized = re.sub(r'\bincome\b', 'ebit', normalized)309 normalized = re.sub(r'\bni\b', 'ebit', normalized)310 normalized = re.sub(r'\bsales\b', 'cashflow', normalized)311 normalized = re.sub(r'\beps_ttm\b', 'ebit', normalized)312 normalized = re.sub(r'\bearnings_per_share_ttm\b', 'ebit', normalized)313 normalized = re.sub(r'\bearnings_income\b', 'ebit', normalized)314 normalized = re.sub(r'\bearnings_ttm_diluted_eps\b', 'ebit', normalized)315 normalized = re.sub(r'\bcash_flow\b', 'cashflow', normalized)316 normalized = sanitize_expression(normalized)317 318 logger.debug(f"Normalizing expression: {rec.expression} -> {normalized}")319 rec.expression = normalized320 rec.status = "GENERATED"321 rec.error_log = None322 mutated = True323 324 # Sanitize scientific notation for ALL records (even those without variable renames)325 if not is_normalized_different:326 sanitized = sanitize_expression(rec.expression)327 if sanitized != rec.expression:328 logger.debug(f"Sanitizing sci notation: {rec.expression} -> {sanitized}")329 rec.expression = sanitized330 # Reset to GENERATED so it gets re-simulated with the fixed expression331 if rec.status in ("FAILED", "SIMULATING"):332 rec.status = "GENERATED"333 rec.error_log = None334 mutated = True335 336 # Normalize region337 normalized_region = "USA"338 if rec.region != normalized_region:339 rec.region = normalized_region340 mutated = True341 342 # Normalize universe343 normalized_universe = "TOP3000"344 if rec.universe:345 u_val = rec.universe.upper()346 if u_val in ["TOP3000", "TOP2000", "TOP1000", "TOP500", "TOP200", "TOPSP500"]:347 normalized_universe = u_val348 if rec.universe != normalized_universe:349 rec.universe = normalized_universe350 mutated = True351 352 # Normalize neutralization353 normalized_neut = "SUBINDUSTRY"354 if rec.neutralization:355 normalized_neut = rec.neutralization.upper()356 if normalized_neut not in ["SUBINDUSTRY", "INDUSTRY", "SECTOR", "MARKET", "NONE"]:357 normalized_neut = "SUBINDUSTRY"358 if rec.neutralization != normalized_neut:359 rec.neutralization = normalized_neut360 mutated = True361 362 # Normalize delay363 normalized_delay = 1364 try:365 if rec.delay is not None:366 d_val = int(rec.delay)367 if d_val in [0, 1]:368 normalized_delay = d_val369 except (ValueError, TypeError):370 pass371 if rec.delay != normalized_delay:372 rec.delay = normalized_delay373 mutated = True374 375 376 # Reset SIMULATING or PASSED records back to GENERATED on startup377 if rec.status in ("SIMULATING", "PASSED"):378 rec.status = "GENERATED"379 mutated = True380 381 # Reset FAILED records caused by payload/settings formatting or sci notation issues382 if rec.status == "FAILED" and rec.error_log:383 err_lower = rec.error_log.lower()384 if any(x in err_lower for x in ["settings", "choice", "is not a valid", "field is required", "submit failed: 400", "400 -", "unexpected character"]):385 rec.status = "GENERATED"386 rec.error_log = None387 mutated = True388 389 # Reset/demote any correlation-pending/passed records that don't meet strict requirements390 if rec.status in ("PENDING_CORRELATION", "PASSED_CORRELATION", "QUEUED_FOR_NEXT_BATCH"):391 if rec.sharpe is not None and rec.fitness is not None:392 if rec.sharpe < 1.25 or rec.fitness < 1.0:393 logger.info(f"Demoting sub-threshold alpha {rec.expression} (Sharpe={rec.sharpe:.3f}, Fitness={rec.fitness:.3f}) from status {rec.status} to FAILED.")394 rec.status = "FAILED"395 rec.error_log = f"Demoted: Sharpe={rec.sharpe:.3f} < 1.25 or Fitness={rec.fitness:.3f} < 1.0"396 mutated = True397 398 # Retroactively promote FAILED records that meet the strict criteria399 if rec.status == "FAILED" and rec.sharpe is not None and rec.fitness is not None and rec.turnover is not None:400 sharpe_ok = rec.sharpe >= 1.25401 fitness_ok = rec.fitness >= 1.0402 turnover_ok = 0.01 <= rec.turnover <= 0.70403 if sharpe_ok and fitness_ok and turnover_ok:404 logger.info(f"Retroactively promoting high-performing failed alpha {rec.expression} (Sharpe={rec.sharpe}, Fitness={rec.fitness}) back to GENERATED.")405 rec.status = "GENERATED"406 rec.error_log = None407 mutated = True408 elif abs(rec.sharpe) >= 1.25 and abs(rec.fitness) >= 1.0 and turnover_ok and rec.sharpe < 0:409 orig_expr = rec.expression.strip()410 if orig_expr.startswith("-"):411 if orig_expr.startswith("-("):412 flipped_expr = orig_expr[2:-1]413 else:414 flipped_expr = orig_expr[1:]415 else:416 flipped_expr = f"-({orig_expr})"417 418 if flipped_expr not in existing_exprs:419 logger.info(f"Discovered negative Sharpe alpha with high absolute metrics: {rec.expression} (Sharpe={rec.sharpe}). Adding sign-flipped version to DB: {flipped_expr}")420 new_rec = AlphaRecord(421 expression=flipped_expr,422 region=rec.region,423 universe=rec.universe,424 delay=rec.delay,425 decay=rec.decay,426 truncation=rec.truncation,427 neutralization=rec.neutralization,428 status="GENERATED"429 )430 db.add(new_rec)431 existing_exprs.add(flipped_expr)432 updated_count += 1433 434 if mutated:435 updated_count += 1436 437 if updated_count > 0 or deleted_count > 0:438 db.commit()439 logger.info(f"Successfully cleaned and normalized. Updated: {updated_count}, Deleted: {deleted_count} alpha records.")440 else:441 logger.info("Database is already clean and normalized.")442 443 db.close()444 except Exception as e:445 logger.error(f"Error during database normalization: {e}")446 447async def miner_loop():448 """449 Central execution loop running the autonomous Alpha Mining pipeline.450 """451 global MINER_RUNNING, LAST_GENERATION_TIME452 logger.info("Initializing database...")453 init_db()454 455 # Run database normalization and startup cleanup456 clean_and_normalize_database()457 458 global GLOBAL_SUBMISSION_ENGINE459 460 client = BrainClient()461 agent = AlphaAgent()462 if GLOBAL_SUBMISSION_ENGINE is None:463 GLOBAL_SUBMISSION_ENGINE = SubmissionEngine(client)464 465 # Ensure data fields are cached in the background466 asyncio.create_task(client.fetch_and_cache_data_fields(["USA"], ["TOP3000", "TOP2000", "TOP1000", "TOP500", "TOP200", "TOPSP500"]))467 468 # Run startup synchronization to realign local DB status of submitted alphas469 try:470 await GLOBAL_SUBMISSION_ENGINE.sync_submitted_alphas()471 except Exception as e:472 logger.error(f"Startup synchronization of submitted alphas failed: {e}")473 474 # Process/submit any legacy queued alphas before activating new loop475 try:476 await GLOBAL_SUBMISSION_ENGINE.clear_legacy_queue()477 except Exception as e:478 logger.error(f"Startup clearing of legacy submission queue failed: {e}")479 480 # Default fallback data fields481 data_fields = ["ebit", "capex", "assets", "debt", "equity", "cash", "cashflow"]482 483 while MINER_RUNNING:484 try:485 db = SessionLocal()486 487 # ==========================================488 # PHASE A: Generation489 # ==========================================490 # We first check if we already have sufficient pending/unprocessed alphas491 pending_count = db.query(AlphaRecord).filter(492 AlphaRecord.status.in_(["GENERATED", "REFINED"])493 ).count()494 495 if pending_count < 150:496 now = time.time()497 # Dynamic throttle based on last generation source498 throttle_seconds = THROTTLE_LOCAL_FALLBACK if USED_LOCAL_FALLBACK else THROTTLE_LLM499 500 if now - LAST_GENERATION_TIME >= throttle_seconds:501 logger.info(f"Phase A: Queue size {pending_count} is low (target: 150). Generating new alphas...")502 503 # Rich library of proven seed templates to rotate for diversity and quality504 PROVEN_SEEDS = [505 "-ts_decay_linear(rank(ts_delta(rank(ebit), 10)), 45) * ts_rank(returns, 120)",506 "-ts_decay_linear(rank(ts_delta(rank(capex), 5)), 45) * ts_rank(returns, 252)",507 "-ts_decay_linear(rank(ts_delta(rank(assets), 5)), 40) * ts_rank(returns, 252)",508 "-ts_decay_linear(rank(ts_delta(rank(sales), 5)), 40) * ts_rank(returns, 252)",509 "-ts_decay_linear(rank(ts_delta(rank(ebit / cashflow), 5)), 60) * ts_rank(returns, 252)",510 "-rank(ts_decay_linear(rank(cashflow / (debt + 0.000000001)), 20)) * ts_rank(returns, 20)",511 "-ts_decay_linear(rank(ts_delta(rank(equity), 5)), 45) * ts_rank(returns, 252)",512 "-ts_decay_linear(rank(ts_delta(rank(cashflow), 5)), 40) * ts_rank(returns, 252)",513 "-ts_decay_linear(rank(cashflow / assets), 20) * ts_rank(returns, 20)",514 "-ts_decay_linear(rank(rank(cashflow) / rank(debt)), 40) * ts_rank(returns, 20)",515 "group_neutralize(rank(ebit / assets) - rank(cashflow / debt), SUBINDUSTRY)",516 "-ts_decay_linear(rank(ts_corr(close, volume, 20)) * rank(ts_std_dev(returns, 20)), 120)"517 ]518 519 seed_expression = random.choice(PROVEN_SEEDS)520 try:521 successful_alphas = db.query(AlphaRecord).filter(522 AlphaRecord.status.in_(["PASSED_CORRELATION", "SUBMITTED", "REFERENCE"])523 ).order_by(AlphaRecord.sharpe.desc()).all()524 525 if successful_alphas:526 # 70% chance of using a historical successful alpha, 30% chance of using a proven seed to maintain diversity527 if random.random() < 0.7:528 best_alpha = successful_alphas[0]529 seed_expression = best_alpha.expression530 logger.info(f"Selected best performer as dynamic seed alpha: {seed_expression} (Sharpe: {best_alpha.sharpe})")531 else:532 logger.info(f"Selected proven seed alpha from library for diversity: {seed_expression}")533 else:534 logger.info(f"No successful alphas in DB. Selected proven seed alpha: {seed_expression}")535 except Exception as e:536 logger.warning(f"Error querying dynamic seed alpha: {e}")537 538 # Query up to 5 successful example expressions to guide mutations539 try:540 successful_records = db.query(AlphaRecord).filter(541 AlphaRecord.status.in_(["PASSED_CORRELATION", "SUBMITTED", "REFERENCE"])542 ).order_by(AlphaRecord.sharpe.desc()).limit(5).all()543 successful_examples = [r.expression for r in successful_records]544 if successful_examples:545 logger.info(f"Using {len(successful_examples)} historical successful alphas as few-shot examples.")546 except Exception as e:547 logger.warning(f"Failed to query successful alphas for examples: {e}")548 successful_examples = []549 550 mutations = await agent.generate_mutations(seed_expression, data_fields, successful_examples=successful_examples)551 552 added_count = 0553 for mut in mutations:554 expression = mut.get("expression")555 raw_settings = mut.get("settings", {})556 if not expression:557 continue558 expression = sanitize_expression(expression)559 if not validate_expression(expression):560 continue561 existing = db.query(AlphaRecord).filter_by(expression=expression).first()562 if not existing:563 norm_settings = normalize_settings(raw_settings)564 new_record = AlphaRecord(565 expression=expression,566 region=norm_settings["region"],567 universe=norm_settings["universe"],568 delay=norm_settings["delay"],569 decay=norm_settings["decay"],570 truncation=norm_settings["truncation"],571 neutralization=norm_settings["neutralization"],572 status="GENERATED"573 )574 db.add(new_record)575 added_count += 1576 db.commit()577 LAST_GENERATION_TIME = time.time()578 579 pending_count = db.query(AlphaRecord).filter(580 AlphaRecord.status.in_(["GENERATED", "REFINED"])581 ).count()582 logger.info(f"Phase A complete. Added {added_count} alphas. Current pending: {pending_count}")583 else:584 time_remaining = int(throttle_seconds - (now - LAST_GENERATION_TIME))585 logger.info(f"Phase A: Queue low ({pending_count}), generation throttled. Re-evaluating in {time_remaining}s...")586 else:587 logger.info(f"Phase A: Skipped generation. {pending_count} alphas currently in queue.")588 589 # ==========================================590 # PHASE B: Simulation591 # ==========================================592 logger.info("Phase B: Querying alphas for simulation...")593 batch = db.query(AlphaRecord).filter(594 AlphaRecord.status.in_(["GENERATED", "REFINED"]),595 AlphaRecord.retry_count <= 5596 ).order_by(AlphaRecord.retry_count.asc(), AlphaRecord.id.desc()).limit(config.get_concurrent_limit()).all()597 598 if not batch:599 logger.info("No alphas to simulate. Sleeping for 10 seconds.")600 db.close()601 await asyncio.sleep(10)602 continue603 604 tasks = []605 for record in batch:606 record.status = "SIMULATING"607 norm_settings = normalize_settings({608 "region": record.region,609 "universe": record.universe,610 "delay": record.delay,611 "decay": record.decay,612 "truncation": record.truncation,613 "neutralization": record.neutralization614 })615 alpha_params = {616 "type": "REGULAR",617 "regular": record.expression,618 "settings": {619 "instrumentType": "EQUITY",620 "region": norm_settings["region"],621 "universe": norm_settings["universe"],622 "delay": norm_settings["delay"],623 "decay": norm_settings["decay"],624 "truncation": norm_settings["truncation"],625 "neutralization": norm_settings["neutralization"],626 "pasteurization": "ON",627 "unitHandling": "VERIFY",628 "nanHandling": "OFF",629 "language": "FASTEXPR",630 "visualization": False631 }632 }633 # Stagger submissions by len(tasks) * 4.0 seconds to avoid thundering herd and 429 collisions634 async def run_staggered(params, delay):635 if delay > 0:636 await asyncio.sleep(delay)637 return await client.simulate_alpha_async(params)638 639 tasks.append(run_staggered(alpha_params, len(tasks) * 4.0))640 641 db.commit() # Save 'SIMULATING' status642 643 logger.info(f"Submitting {len(tasks)} alphas for simulation concurrently (staggered)...")644 results_list = await asyncio.gather(*tasks, return_exceptions=True)645 646 # ==========================================647 # PHASE C: Evaluation & Refinement648 # ==========================================649 logger.info("Phase C: Evaluating simulation results...")650 651 for i, result in enumerate(results_list):652 record = batch[i]653 db.refresh(record)654 655 # Retrieve metrics if they were successfully simulated and saved in DB656 sharpe = record.sharpe657 fitness = record.fitness658 turnover = record.turnover659 660 if isinstance(result, Exception):661 logger.error(f"Simulation task for {record.expression} raised an exception: {result}")662 record.status = "FAILED"663 record.error_log = str(result)664 db.commit()665 continue666 667 if isinstance(result, dict) and result.get("status") == "FAILED" and record.status == "SIMULATING":668 record.status = "FAILED"669 record.error_log = result.get("error_message")670 db.commit()671 continue672 673 # AIR-TIGHT PERFORMANCE EVALUATION (Evaluates metrics if they exist, regardless of status)674 if sharpe is not None and fitness is not None and turnover is not None:675 turnover_max_ok = turnover <= 0.70676 turnover_min_ok = turnover >= 0.01677 turnover_ok = turnover_max_ok and turnover_min_ok678 679 tier1_ok = (sharpe >= 1.25) and (fitness >= 1.0) and turnover_ok680 tier2_ok = (sharpe >= 0.9) and (fitness >= 0.4) and turnover_ok681 682 wq_submittable = result.get("is_submittable") if isinstance(result, dict) else None683 684 # Check for platform check failures685 has_check_failures = False686 is_correlation_failure = False687 if record.error_log:688 err_lower = record.error_log.lower()689 has_check_failures = "failed" in err_lower or "fail" in err_lower690 is_correlation_failure = "correlation" in err_lower or "overlap" in err_lower or "similar" in err_lower691 692 if (tier1_ok or wq_submittable) and not has_check_failures:693 submit_reason = "WQ API is_submittable=True" if wq_submittable and not tier1_ok else "local criteria"694 logger.info(695 f"✅ SUCCESS (Tier 1)! Alpha {record.expression} passed ({submit_reason}): "696 f"Sharpe={sharpe:.3f}, Fitness={fitness:.3f}, Turnover={turnover:.3f}"697 )698 record.status = "PENDING_CORRELATION"699 record.error_log = None700 db.commit()701 elif (tier2_ok or tier1_ok) and is_correlation_failure:702 logger.info(703 f"⚠️ Tier 2 or Correlation Failure! Alpha {record.expression}: "704 f"Sharpe={sharpe:.3f}, Fitness={fitness:.3f}, Turnover={turnover:.3f}. "705 f"Queueing for orthogonalization."706 )707 record.status = "QUEUED_FOR_ORTHOGONALIZATION"708 record.error_log = f"TIER2_PASS: Sharpe={sharpe:.3f}, Fitness={fitness:.3f}, Turnover={turnover:.3f}"709 db.commit()710 elif (tier2_ok or tier1_ok) and has_check_failures:711 # Good performance but has a platform check failure (e.g. CONCENTRATED_WEIGHT) -> Refine to fix it!712 error_reason = f"PLATFORM_CHECK_FAILURE: Sharpe={sharpe:.3f}, Fitness={fitness:.3f}, failed: {record.error_log}"713 logger.info(f"⚠️ Promising alpha {record.expression} failed platform checks: {error_reason}")714 record.status = "FAILED"715 record.error_log = error_reason716 db.commit()717 elif tier2_ok:718 logger.info(719 f"⚠️ Tier 2 Pass! Alpha {record.expression}: "720 f"Sharpe={sharpe:.3f}, Fitness={fitness:.3f}, Turnover={turnover:.3f}. "721 f"Queueing for orthogonalization."722 )723 record.status = "QUEUED_FOR_ORTHOGONALIZATION"724 record.error_log = f"TIER2_PASS: Sharpe={sharpe:.3f}, Fitness={fitness:.3f}, Turnover={turnover:.3f}"725 db.commit()726 else:727 reasons = []728 if not (sharpe >= 1.25):729 reasons.append(f"Sharpe={sharpe:.3f} < 1.25")730 if not (fitness >= 1.0):731 reasons.append(f"Fitness={fitness:.3f} < 1.0")732 if not turnover_ok:733 reasons.append(f"Turnover={turnover:.3f} outside [1%, 70%]")734 735 is_near_miss = (sharpe >= 1.0 and fitness >= 0.7 and turnover_ok)736 737 if is_near_miss:738 error_reason = f"NEAR_MISS: Close but failed criteria: {', '.join(reasons)}"739 logger.info(f"⚠️ Near-miss alpha {record.expression}: {error_reason}")740 record.status = "FAILED"741 record.error_log = error_reason742 db.commit()743 else:744 error_reason = f"Failed criteria: {', '.join(reasons)}"745 logger.info(f"Alpha {record.expression} completed but {error_reason} (not worth refining)")746 record.status = "FAILED_REJECTED"747 record.error_log = error_reason748 db.commit()749 else:750 # No metrics returned, failed to simulate or compile751 if record.status == "SIMULATING":752 record.status = "FAILED"753 record.error_log = "Unknown compilation or simulation failure."754 db.commit()755 756 db.close()757 758 # ==========================================759 # PHASE D: Submission & Correlation Check760 # ==========================================761 logger.info("Phase D: Submission & Correlation Check...")762 await GLOBAL_SUBMISSION_ENGINE.process_pending_correlations()763 764 # ==========================================765 # PHASE E: Orthogonalization766 # ==========================================767 logger.info("Phase E: Processing alphas queued for orthogonalization...")768 db = SessionLocal()769 try:770 ortho_candidates = db.query(AlphaRecord).filter_by(771 status="QUEUED_FOR_ORTHOGONALIZATION"772 ).limit(5).all() # Process up to 5 at a time to prevent rate limits773 774 for record in ortho_candidates:775 logger.info(f"Orthogonalizing alpha: {record.expression} (correlation failure: {record.error_log})")776 777 alpha_dict = {778 "expression": record.expression,779 "settings": {780 "region": record.region,781 "universe": record.universe,782 "delay": record.delay,783 "decay": record.decay,784 "truncation": record.truncation,785 "neutralization": record.neutralization786 }787 }788 789 refined_alpha = await agent.orthogonalize_alpha(alpha_dict, record.error_log or "Correlation failure")790 if refined_alpha:791 new_expr = refined_alpha.get("expression")792 raw_settings = refined_alpha.get("settings", {})793 794 if new_expr and new_expr != record.expression:795 new_expr = sanitize_expression(new_expr)796 if not validate_expression(new_expr):797 logger.info(f"Skipping orthogonalized formula with syntax/variable errors: {new_expr}")798 record.status = "FAILED_CORRELATION" # Fail original and don't retry799 continue800 801 existing_refined = db.query(AlphaRecord).filter_by(expression=new_expr).first()802 if not existing_refined:803 norm_settings = normalize_settings(raw_settings)804 new_record = AlphaRecord(805 expression=new_expr,806 region=norm_settings["region"],807 universe=norm_settings["universe"],808 delay=norm_settings["delay"],809 decay=norm_settings["decay"],810 truncation=norm_settings["truncation"],811 neutralization=norm_settings["neutralization"],812 status="GENERATED"813 )814 db.add(new_record)815 logger.info(f"Successfully added orthogonalized alpha to database: {new_expr}")816 817 # Mark original alpha as FAILED_CORRELATION so we don't orthogonalize it again818 record.status = "FAILED_CORRELATION"819 db.commit()820 except Exception as e:821 logger.error(f"Error during Phase E Orthogonalization: {e}")822 db.rollback()823 finally:824 db.close()825 826 # ==========================================827 # PHASE F: Refinement of Failed Alphas828 # ==========================================829 logger.info("Phase F: Processing failed alphas for refinement...")830 db = SessionLocal()831 try:832 failed_candidates = db.query(AlphaRecord).filter_by(833 status="FAILED"834 ).limit(5).all() # Process up to 5 at a time835 836 for record in failed_candidates:837 # Differentiate between rate limit errors and other failures838 if record.error_log and any(x in record.error_log for x in ["CONCURRENT_SIMULATION_LIMIT_EXCEEDED", "rate limit", "Too Many Requests"]):839 record.retry_count = (record.retry_count or 0) + 1840 logger.info(f"Alpha {record.expression} failed due to rate limits/transient error (retry count: {record.retry_count}). Resetting to GENERATED.")841 record.status = "GENERATED"842 record.error_log = None843 db.commit()844 continue845 846 if not is_worth_refining(record.error_log, record.sharpe, record.fitness, record.turnover):847 logger.info(f"Alpha {record.expression} is not worth refining. Demoting to FAILED_REJECTED.")848 record.status = "FAILED_REJECTED"849 db.commit()850 continue851 852 logger.info(f"Alpha {record.expression} is worth refining. Triggering LLM self-correction...")853 alpha_dict = {854 "expression": record.expression,855 "settings": {856 "region": record.region,857 "universe": record.universe,858 "delay": record.delay,859 "decay": record.decay,860 "truncation": record.truncation,861 "neutralization": record.neutralization862 }863 }864 refined_alpha = await agent.refine_error(alpha_dict, record.error_log or "Unknown Error")865 if refined_alpha:866 new_expr = refined_alpha.get("expression")867 raw_settings = refined_alpha.get("settings", {})868 869 if new_expr and new_expr != record.expression:870 new_expr = sanitize_expression(new_expr)871 if not validate_expression(new_expr):872 logger.info(f"Skipping refined formula with syntax/variable errors: {new_expr}")873 record.status = "FAILED_REJECTED"874 db.commit()875 continue876 877 existing_refined = db.query(AlphaRecord).filter_by(expression=new_expr).first()878 if not existing_refined:879 norm_settings = normalize_settings(raw_settings)880 new_record = AlphaRecord(881 expression=new_expr,882 region=norm_settings["region"],883 universe=norm_settings["universe"],884 delay=norm_settings["delay"],885 decay=norm_settings["decay"],886 truncation=norm_settings["truncation"],887 neutralization=norm_settings["neutralization"],888 status="REFINED"889 )890 db.add(new_record)891 logger.info(f"Added successfully refined alpha to database: {new_expr}")892 893 # Mark the original record as FAILED_REJECTED so we don't refine it again894 record.status = "FAILED_REJECTED"895 db.commit()896 except Exception as e:897 logger.error(f"Error during Phase F Refinement: {e}")898 db.rollback()899 finally:900 db.close()901 902 # Short sleep before the next overarching iteration903 logger.info("Cycle complete. Sleeping before next iteration...")904 await asyncio.sleep(5)905 906 except Exception as e:907 logger.exception("Critical error in miner_loop. Sleeping before retry...")908 await asyncio.sleep(10)909 910if __name__ == "__main__":911 try:912 MINER_RUNNING = True913 asyncio.run(miner_loop())914 except KeyboardInterrupt:915 MINER_RUNNING = False916 logger.info("Miner gracefully shut down by user.")917 