diff --git a/prompts/standard_prompts.yaml b/prompts/standard_prompts.yaml index 93a4b8ae..977d457b 100644 --- a/prompts/standard_prompts.yaml +++ b/prompts/standard_prompts.yaml @@ -106,29 +106,41 @@ strategy_generation: \ directional moves\n- Spread cost: ~1.5 bps per trade — signals must overcome\ \ this\n\nFactor Usage Rules:\n1. ONLY use the factors provided below — no others!\n\ 2. The code MUST work with a DataFrame called 'factors' containing factor columns\n\ - 3. Create a pandas Series called 'signal' with values: 1 (long), -1 (short), 0\ - \ (neutral)\n4. signal.index MUST match factors.index exactly\n5. signal.name\ - \ must be 'signal'\n\nSignal Quality Requirements:\n- Generate meaningful signals\ - \ (avoid constant 0 or all 1s)\n- Use rolling z-scores for normalization: (x -\ - \ rolling.mean()) / rolling.std()\n- Apply thresholds (e.g., z > 0.3 for long,\ - \ z < -0.3 for short)\n- Combine factors with weights based on their IC values\n\ - - Consider regime filters (trend vs mean-reversion)\n\nOutput ONLY valid JSON\ + 3. Also available: 'close' Series with OHLCV close prices\n4. Create a pandas\ + \ Series called 'signal' with values: 1 (long), -1 (short), 0 (neutral)\n5. signal.index\ + \ MUST match factors.index exactly\n6. signal.name must be 'signal'\n\nIMPORTANT:\ + \ Understanding IC Sign\n- Factors with POSITIVE IC (e.g., IC=+0.25): HIGH factor\ + \ value → price goes UP → go LONG\n- Factors with NEGATIVE IC (e.g., IC=-0.20):\ + \ HIGH factor value → price goes DOWN → go SHORT\n- Best strategies COMBINE both\ + \ types: use positive IC for trend direction, negative IC for divergence/reversal\n\ + \nSignal Quality Requirements:\n- Generate balanced signals (~40-60% in each direction)\n\ + - Use rolling z-scores for normalization: (x - rolling.mean()) / rolling.std()\n\ + - Combine factors respecting their IC SIGN (multiply negative IC factors by -1)\n\ + - Apply thresholds based on signal distribution (e.g., z > 0.5 for long, z < -0.5\ + \ for short)\n- Consider regime filters (trend vs mean-reversion)\n- Use available\ + \ 'close' Series for additional calculations if needed\n\nOutput ONLY valid JSON\ \ with these exact fields:\n{\n \"strategy_name\": \"short_descriptive_name\"\ ,\n \"factors_used\": [\"factor1\", \"factor2\", \"factor3\"],\n \"description\"\ : \"one sentence explaining the strategy logic\",\n \"code\": \"complete Python\ - \ code that creates signal Series\"\n}" + \ code that creates signal Series\"\n}\n" user: "Generate a EUR/USD trading strategy using these factors:\n\n{{ factors }}\n\ \n{{ additional_context }}\n\nCRITICAL RULES:\n1. DO NOT define functions - write\ \ direct executable code\n2. DO NOT use def - just write the code that creates\ - \ 'signal'\n3. The code will be executed with 'factors' DataFrame already in scope\n\ - 4. You MUST create a variable called 'signal' as a pandas Series\n5. signal must\ - \ have values 1 (LONG), -1 (SHORT), or 0 (NEUTRAL)\n6. signal.index must equal\ - \ factors.index\n\nEXAMPLE OF CORRECT FORMAT:\n```\nimport pandas as pd\nimport\ - \ numpy as np\n\nz = (factors['factor1'] - factors['factor1'].rolling(20).mean())\ - \ / factors['factor1'].rolling(20).std()\nsignal = pd.Series(0, index=factors.index)\n\ - signal[z > 0.3] = 1\nsignal[z < -0.3] = -1\nsignal.name = 'signal'\n```\n\nWRONG\ + \ 'signal'\n3. The code will be executed with 'factors' DataFrame and 'close'\ + \ Series already in scope\n4. You MUST create a variable called 'signal' as a\ + \ pandas Series\n5. signal must have values 1 (LONG), -1 (SHORT), or 0 (NEUTRAL)\n\ + 6. signal.index must equal factors.index\n7. RESPECT IC SIGN: Negative IC factors\ + \ should be INVERTED (multiplied by -1) before combining\n\nEXAMPLE OF CORRECT\ + \ FORMAT:\n```\nimport pandas as pd\nimport numpy as np\n\n# Positive IC factor:\ + \ high value → go LONG\nmom = factors['daily_close_return_96']\nz_mom = (mom -\ + \ mom.rolling(20).mean()) / mom.rolling(20).std()\n\n# Negative IC factor: high\ + \ value → go SHORT (INVERT!)\ndiv = factors['daily_session_momentum_divergence_1d']\n\ + z_div = -(div - div.rolling(20).mean()) / div.rolling(20).std() # NOTE the minus\ + \ sign!\n\n# Combine: momentum + inverted divergence\ncomposite = 0.5 * z_mom\ + \ + 0.5 * z_div\nsignal = pd.Series(0, index=factors.index)\nsignal[composite\ + \ > 0.5] = 1\nsignal[composite < -0.5] = -1\nsignal.name = 'signal'\n```\n\nWRONG\ \ FORMAT (DO NOT DO THIS):\n```\ndef generate_signal(factors):\n ...\n return\ - \ signal\n```\n\nOutput ONLY the JSON object, no additional text." + \ signal\n```\n\nOutput ONLY the JSON object, no additional text.\n" trading_strategy: system: "You are a portfolio manager designing trading strategies for EURUSD.\n\n\ Strategy components:\n1. Entry signals (from factors/models)\n2. Position sizing\ diff --git a/prompts/strategy_generation_v3.yaml b/prompts/strategy_generation_v3.yaml new file mode 100644 index 00000000..cc953c6c --- /dev/null +++ b/prompts/strategy_generation_v3.yaml @@ -0,0 +1,87 @@ +strategy_generation: + system: | + You are an expert quantitative trading researcher specialized in EUR/USD intraday strategies. + + Your task is to generate a trading strategy by combining the provided factors into a coherent signal. + + EUR/USD Domain Knowledge: + - London session (08:00-16:00 UTC): highest volume, trending behavior + - NY session (13:00-21:00 UTC): second volume peak, continuation + - Asian session (00:00-08:00 UTC): lower volume, mean-reverting + - London/NY overlap (13:00-16:00 UTC): strongest directional moves + - Spread cost: ~1.5 bps per trade — signals must overcome this + + Factor Usage Rules: + 1. ONLY use the factors provided below — no others! + 2. The code MUST work with a DataFrame called 'factors' containing factor columns + 3. Also available: 'close' Series with OHLCV close prices + 4. Create a pandas Series called 'signal' with values: 1 (long), -1 (short), 0 (neutral) + 5. signal.index MUST match factors.index exactly + 6. signal.name must be 'signal' + + IMPORTANT: Understanding IC Sign + - Factors with POSITIVE IC (e.g., IC=+0.25): HIGH factor value → price goes UP → go LONG + - Factors with NEGATIVE IC (e.g., IC=-0.20): HIGH factor value → price goes DOWN → go SHORT + - Best strategies COMBINE both types: use positive IC for trend direction, negative IC for divergence/reversal + + Signal Quality Requirements: + - Generate balanced signals (~40-60% in each direction) + - Use rolling z-scores for normalization: (x - rolling.mean()) / rolling.std() + - Combine factors respecting their IC SIGN (multiply negative IC factors by -1) + - Apply thresholds based on signal distribution (e.g., z > 0.5 for long, z < -0.5 for short) + - Consider regime filters (trend vs mean-reversion) + - Use available 'close' Series for additional calculations if needed + + Output ONLY valid JSON with these exact fields: + { + "strategy_name": "short_descriptive_name", + "factors_used": ["factor1", "factor2", "factor3"], + "description": "one sentence explaining the strategy logic", + "code": "complete Python code that creates signal Series" + } + + user: | + Generate a EUR/USD trading strategy using these factors: + + {{ factors }} + + {{ additional_context }} + + CRITICAL RULES: + 1. DO NOT define functions - write direct executable code + 2. DO NOT use def - just write the code that creates 'signal' + 3. The code will be executed with 'factors' DataFrame and 'close' Series already in scope + 4. You MUST create a variable called 'signal' as a pandas Series + 5. signal must have values 1 (LONG), -1 (SHORT), or 0 (NEUTRAL) + 6. signal.index must equal factors.index + 7. RESPECT IC SIGN: Negative IC factors should be INVERTED (multiplied by -1) before combining + + EXAMPLE OF CORRECT FORMAT: + ``` + import pandas as pd + import numpy as np + + # Positive IC factor: high value → go LONG + mom = factors['daily_close_return_96'] + z_mom = (mom - mom.rolling(20).mean()) / mom.rolling(20).std() + + # Negative IC factor: high value → go SHORT (INVERT!) + div = factors['daily_session_momentum_divergence_1d'] + z_div = -(div - div.rolling(20).mean()) / div.rolling(20).std() # NOTE the minus sign! + + # Combine: momentum + inverted divergence + composite = 0.5 * z_mom + 0.5 * z_div + signal = pd.Series(0, index=factors.index) + signal[composite > 0.5] = 1 + signal[composite < -0.5] = -1 + signal.name = 'signal' + ``` + + WRONG FORMAT (DO NOT DO THIS): + ``` + def generate_signal(factors): + ... + return signal + ``` + + Output ONLY the JSON object, no additional text. diff --git a/rdagent/components/coder/strategy_orchestrator.py b/rdagent/components/coder/strategy_orchestrator.py index efc31367..d678a60b 100644 --- a/rdagent/components/coder/strategy_orchestrator.py +++ b/rdagent/components/coder/strategy_orchestrator.py @@ -12,8 +12,8 @@ Usage: orchestrator = StrategyOrchestrator( top_factors=20, trading_style='swing', - min_sharpe=1.5, - max_drawdown=-0.20, + min_sharpe=0.3, + max_drawdown=-0.30, ) results = orchestrator.generate_strategies(count=10, workers=4) """ @@ -56,9 +56,9 @@ class StrategyOrchestrator: self, top_factors: int = 20, trading_style: str = "swing", - min_sharpe: float = 1.5, - max_drawdown: float = -0.20, - min_win_rate: float = 0.50, + min_sharpe: float = 0.3, + max_drawdown: float = -0.30, + min_win_rate: float = 0.40, results_dir: Optional[str] = None, use_optuna: bool = True, optuna_trials: int = 20, @@ -179,7 +179,67 @@ class StrategyOrchestrator: else: logger.debug(f"Skipping {fname} - no parquet file") - return factors_with_files[: self.top_factors] + # Select diverse factor TYPES, not just top IC + # This ensures we get momentum, volatility, session, volume, etc. + type_keywords = { + "momentum": [], "trend": [], "volatility": [], "volume": [], + "session": [], "london": [], "range": [], "vwap": [], + "return": [], "ofi": [], "spread": [], "close": [], + "divergence": [], "other": [] + } + + for f in factors_with_files: + name = f.get("factor_name", "").lower() + matched = False + for kw in type_keywords: + if kw in name: + type_keywords[kw].append(f) + matched = True + break + if not matched: + type_keywords["other"].append(f) + + # Select best from each type (ensures diversity) + selected = [] + already_names = set() + + # Priority order: momentum, divergence, volatility, session, volume, etc. + priority_types = ["momentum", "divergence", "volatility", "session", + "london", "range", "vwap", "volume", "ofi", "spread", + "return", "trend", "close", "other"] + + per_type = max(2, self.top_factors // len(priority_types)) + + for kw in priority_types: + for f in sorted(type_keywords[kw], key=lambda x: abs(x.get("ic", 0)), reverse=True): + if f["factor_name"] not in already_names: + selected.append(f) + already_names.add(f["factor_name"]) + if len([s for s in selected if s["factor_name"] in [x["factor_name"] for x in type_keywords[kw]]]) >= per_type: + break + + # Fill remaining with highest IC not yet selected + if len(selected) < self.top_factors: + remaining = [f for f in factors_with_files if f["factor_name"] not in already_names] + remaining.sort(key=lambda x: abs(x.get("ic", 0)), reverse=True) + selected.extend(remaining[:self.top_factors - len(selected)]) + + # Log diversity + type_counts = {} + for f in selected: + name = f.get("factor_name", "").lower() + matched = False + for kw in type_keywords: + if kw in name: + type_counts[kw] = type_counts.get(kw, 0) + 1 + matched = True + break + if not matched: + type_counts["other"] = type_counts.get("other", 0) + 1 + + logger.info(f"Selected {len(selected)} diverse factors: {type_counts}") + + return selected[:self.top_factors] def load_factor_values(self, factor_name: str) -> Optional[pd.Series]: """ @@ -848,8 +908,12 @@ signal = signal.rolling(window=3, min_periods=1).mean().round().astype(int) f"DD={result['max_drawdown']:.2%}" ) else: - logger.debug( - f"Strategy rejected: {result['strategy_name']} - {result.get('reason', 'unknown')}" + # Also save rejected strategies for debugging + self._save_strategy(result) + logger.warning( + f"Strategy REJECTED: {result['strategy_name']} - {result.get('reason', 'unknown')} | " + f"Sharpe={result.get('sharpe_ratio', 'N/A')} | " + f"DD={result.get('max_drawdown', 'N/A')}" ) if progress_callback: