mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-27 23:47:46 +00:00
feat: Diverse factor selection + improved prompt v3
Factor Selection: - Select by TYPE (momentum, divergence, volatility, session, etc.) - Ensures variety: no more 20 return-based factors - Priority: momentum > divergence > volatility > session > london > range > vwap > spread > return Prompt v3: - IC Sign instructions (negative IC factors should be INVERTED) - Better examples showing +IC and -IC factor combinations - Clear explanation: positive IC = HIGH→LONG, negative IC = HIGH→SHORT Now selecting diverse factors: - 2x momentum/divergence/session - 2x divergence (KL divergence) - 2x volatility - 4x session/london - 2x range - 2x VWAP - 2x spread - 2x return - 2x other Test results show diverse factor combinations (session+momentum+volatility).
This commit is contained in:
@@ -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\
|
||||
|
||||
@@ -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.
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user