Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| baseline.py β Baseline inference script for IndicatorsEnv (OpenEnv Hackathon) | |
| Uses the OpenAI API client to run a language model against all 3 IndicatorsEnv tasks. | |
| The model receives a structured prompt of technical indicators and must output a | |
| JSON prediction in the format: {"direction": "Bullish"|"Bearish"|"Neutral", "conviction": float} | |
| Usage: | |
| # Against local environment server: | |
| python baseline.py --env_url http://localhost:7860 | |
| # Against Hugging Face Space: | |
| python baseline.py --env_url https://bawsi99-indicators-env.hf.space | |
| Environment variables: | |
| OPENAI_API_KEY : Your OpenAI API key (required) | |
| OPENAI_BASE_URL : Optional custom base URL (e.g. for local models or HF Inference API) | |
| OPENAI_MODEL : Model to use (default: gpt-4o-mini) | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import logging | |
| import os | |
| import re | |
| import sys | |
| from typing import Any, Dict, List, Optional, Tuple | |
| import httpx | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") | |
| logger = logging.getLogger(__name__) | |
| # βββ OpenAI Client Setup ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _get_openai_client(): | |
| """Initialize the OpenAI client from environment variables.""" | |
| try: | |
| from openai import OpenAI | |
| except ImportError: | |
| logger.error("openai package not installed. Run: pip install openai") | |
| sys.exit(1) | |
| api_key = os.environ.get("OPENAI_API_KEY") | |
| if not api_key: | |
| logger.error("OPENAI_API_KEY environment variable not set.") | |
| sys.exit(1) | |
| base_url = os.environ.get("OPENAI_BASE_URL", None) | |
| client = OpenAI(api_key=api_key, base_url=base_url) | |
| return client | |
| # βββ Prompt Builder βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _build_prompt(observation: Dict[str, Any]) -> str: | |
| """Convert an IndicatorsEnv observation into a formatted model prompt.""" | |
| ind = observation.get("indicators", {}) | |
| ma = ind.get("moving_averages", {}) | |
| rsi = ind.get("rsi", {}) | |
| mac = ind.get("macd", {}) | |
| adx = ind.get("adx", {}) | |
| vlt = ind.get("volatility", {}) | |
| bb = ind.get("bollinger_bands", {}) | |
| vol = ind.get("enhanced_volume", {}) | |
| t = observation.get("term", "MEDIUM") | |
| prompt = f"""You are a quantitative analyst evaluating NSE (Indian) stocks. | |
| Stock: {observation.get('symbol')} | Date: {observation.get('date')} | Price: {observation.get('current_price')} | |
| Prediction Term: {t} | |
| --- Technical Indicators --- | |
| RSI(14): {rsi.get('rsi_14')} | RSI Signal: {rsi.get('rsi_signal')} | |
| MACD Line: {mac.get('macd_line')} | Signal: {mac.get('signal_line')} | Histogram: {mac.get('histogram')} | |
| ADX: {adx.get('adx')} | +DI: {adx.get('plus_di')} | -DI: {adx.get('minus_di')} | Trend: {adx.get('trend_strength')} | |
| SMA20: {ma.get('sma_20')} | SMA50: {ma.get('sma_50')} | SMA200: {ma.get('sma_200')} | |
| EMA20: {ma.get('ema_20')} | Cross: {ma.get('golden_cross')} | |
| BB%: {bb.get('percent_b')} | BB Width: {bb.get('bandwidth')} | Squeeze: {bb.get('squeeze')} | |
| ATR(14): {vlt.get('atr_14')} | Volatility Regime: {vlt.get('volatility_regime')} | |
| VWAP: {vol.get('vwap')} | MFI: {vol.get('mfi')} | MFI Status: {vol.get('mfi_status')} | |
| CMF: {vol.get('cmf')} | A/D Trend: {vol.get('ad_line_trend')} | |
| Analyze these indicators and predict the {t.lower()}-term price direction. | |
| Respond with ONLY valid JSON (no markdown, no explanation): | |
| {{"direction": "Bullish" | "Bearish" | "Neutral", "conviction": <float 0.0-1.0>}}""" | |
| return prompt | |
| # βββ Prediction Logic βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _parse_prediction(text: str) -> Tuple[str, float]: | |
| """Extract direction and conviction from model output.""" | |
| text = re.sub(r"```(?:json)?", "", text).strip().rstrip("`").strip() | |
| try: | |
| match = re.search(r"\{[^}]+\}", text, re.DOTALL) | |
| if match: | |
| obj = json.loads(match.group(0)) | |
| direction = str(obj.get("direction", "Neutral")).strip().capitalize() | |
| conviction = float(obj.get("conviction", 0.5)) | |
| if direction not in ("Bullish", "Bearish", "Neutral"): | |
| direction = "Neutral" | |
| conviction = max(0.0, min(1.0, conviction)) | |
| return direction, conviction | |
| except Exception: | |
| pass | |
| lower = text.lower() | |
| if "bullish" in lower: | |
| return "Bullish", 0.6 | |
| if "bearish" in lower: | |
| return "Bearish", 0.6 | |
| return "Neutral", 0.4 | |
| def _run_llm(client, model: str, prompt: str) -> Tuple[str, float]: | |
| """Call the LLM and parse the prediction.""" | |
| try: | |
| response = client.chat.completions.create( | |
| model=model, | |
| messages=[ | |
| {"role": "system", "content": "You are a quantitative analyst. Always respond with valid JSON only."}, | |
| {"role": "user", "content": prompt}, | |
| ], | |
| max_tokens=64, | |
| temperature=0.1, | |
| ) | |
| text = response.choices[0].message.content or "" | |
| return _parse_prediction(text) | |
| except Exception as e: | |
| logger.warning(f"[LLM] Call failed: {e}") | |
| return "Neutral", 0.5 | |
| # βββ Environment Client βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _env_reset(env_url: str, term: str) -> Optional[Dict]: | |
| """Call /reset on the environment.""" | |
| try: | |
| r = httpx.post(f"{env_url}/reset", params={"term": term}, timeout=30) | |
| r.raise_for_status() | |
| return r.json() | |
| except Exception as e: | |
| logger.error(f"[Env] /reset failed: {e}") | |
| return None | |
| def _env_step(env_url: str, session_id: str, direction: str, conviction: float) -> Optional[Dict]: | |
| """Call /step on the environment.""" | |
| try: | |
| r = httpx.post( | |
| f"{env_url}/step", | |
| params={"session_id": session_id}, | |
| json={"direction": direction, "conviction": conviction}, | |
| timeout=30, | |
| ) | |
| r.raise_for_status() | |
| return r.json() | |
| except Exception as e: | |
| logger.error(f"[Env] /step failed: {e}") | |
| return None | |
| def _env_grade(env_url: str, task_id: str, episode_results: List[Dict]) -> Optional[Dict]: | |
| """Call /grader on the environment.""" | |
| try: | |
| r = httpx.post( | |
| f"{env_url}/grader", | |
| json={"task_id": task_id, "episode_results": episode_results}, | |
| timeout=30, | |
| ) | |
| r.raise_for_status() | |
| return r.json() | |
| except Exception as e: | |
| logger.error(f"[Env] /grader failed: {e}") | |
| return None | |
| # βββ Main Baseline Runner βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def run_baseline(env_url: str, n_episodes: int, model: str) -> None: | |
| client = _get_openai_client() | |
| # Get tasks from the environment | |
| try: | |
| tasks_resp = httpx.get(f"{env_url}/tasks", timeout=15) | |
| tasks_resp.raise_for_status() | |
| tasks = tasks_resp.json()["tasks"] | |
| except Exception as e: | |
| logger.error(f"[Env] /tasks failed: {e}") | |
| sys.exit(1) | |
| logger.info(f"[Baseline] Running {model} on {len(tasks)} tasks Γ {n_episodes} episodes each...") | |
| print(f"\n{'='*65}") | |
| print(f"BASELINE SCORES β IndicatorsEnv | Model: {model}") | |
| print(f"{'='*65}") | |
| all_scores = [] | |
| for task in tasks: | |
| task_id = task["id"] | |
| term = task["term"] | |
| difficulty = task["difficulty"] | |
| logger.info(f"[{task_id}] Starting {n_episodes} episodes (term={term})...") | |
| episode_results = [] | |
| total_reward = 0.0 | |
| for ep in range(n_episodes): | |
| # 1. Reset | |
| reset_data = _env_reset(env_url, term=term) | |
| if reset_data is None: | |
| continue | |
| obs = reset_data["observation"] | |
| session_id = reset_data["info"]["session_id"] | |
| # 2. Build prompt and get LLM prediction | |
| prompt = _build_prompt(obs) | |
| direction, conviction = _run_llm(client, model, prompt) | |
| # 3. Step | |
| step_data = _env_step(env_url, session_id, direction, conviction) | |
| if step_data is None: | |
| continue | |
| reward = step_data.get("reward", 0.0) | |
| gt = step_data.get("info", {}).get("ground_truth", "") | |
| total_reward += reward | |
| episode_results.append({ | |
| "ground_truth": gt, | |
| "predicted": direction, | |
| "conviction": conviction, | |
| }) | |
| if (ep + 1) % 5 == 0: | |
| logger.info(f"[{task_id}] {ep+1}/{n_episodes} done | mean_reward={total_reward/(ep+1):.3f}") | |
| # 4. Grade | |
| grader_data = _env_grade(env_url, task_id, episode_results) | |
| score = grader_data["score"] if grader_data else 0.0 | |
| all_scores.append(score) | |
| print(f"\nTask: {task['name']}") | |
| print(f" Difficulty: {difficulty} | Episodes: {len(episode_results)}") | |
| print(f" Mean Reward: {total_reward/max(1,len(episode_results)):.4f}") | |
| print(f" Grader Score: {score:.4f}") | |
| overall = sum(all_scores) / len(all_scores) if all_scores else 0.0 | |
| print(f"\n{'='*65}") | |
| print(f"OVERALL MEAN SCORE: {overall:.4f}") | |
| print(f"{'='*65}\n") | |
| # Save results | |
| out = { | |
| "model": model, | |
| "env_url": env_url, | |
| "n_episodes": n_episodes, | |
| "task_scores": dict(zip([t["id"] for t in tasks], all_scores)), | |
| "overall_mean": overall, | |
| } | |
| with open("baseline_results.json", "w") as f: | |
| json.dump(out, f, indent=2) | |
| logger.info("Results saved to baseline_results.json") | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="IndicatorsEnv Baseline Inference Script") | |
| parser.add_argument("--env_url", default="http://localhost:7860", help="URL of the IndicatorsEnv server") | |
| parser.add_argument("--n_episodes", type=int, default=10, help="Episodes per task") | |
| parser.add_argument("--model", default=os.environ.get("OPENAI_MODEL", "gpt-4o-mini"), help="OpenAI model to use") | |
| args = parser.parse_args() | |
| run_baseline(args.env_url, args.n_episodes, args.model) | |