diff --git a/rdagent/scenarios/qlib/fx_validator/__init__.py b/rdagent/scenarios/qlib/fx_validator/__init__.py new file mode 100644 index 00000000..799b20e5 --- /dev/null +++ b/rdagent/scenarios/qlib/fx_validator/__init__.py @@ -0,0 +1,3 @@ +from .fx_graph import validate_factor, create_fx_validator + +__all__ = ["validate_factor", "create_fx_validator"] diff --git a/rdagent/scenarios/qlib/fx_validator/agents/__init__.py b/rdagent/scenarios/qlib/fx_validator/agents/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rdagent/scenarios/qlib/fx_validator/agents/analysts/macro_analyst.py b/rdagent/scenarios/qlib/fx_validator/agents/analysts/macro_analyst.py new file mode 100644 index 00000000..5179b826 --- /dev/null +++ b/rdagent/scenarios/qlib/fx_validator/agents/analysts/macro_analyst.py @@ -0,0 +1,73 @@ +""" +FX Macro Analyst — analysiert makroökonomische FX-Faktoren +statt Aktien-Fundamentals +""" +import yfinance as yf +import pandas as pd +from datetime import datetime, timedelta + + +def get_fx_macro_data() -> str: + """Holt DXY, EUR/USD, Volatilität als Makro-Kontext""" + try: + end = datetime.now() + start = end - timedelta(days=5) + + eurusd = yf.download("EURUSD=X", start=start, end=end, interval="1h", progress=False) + dxy = yf.download("DX-Y.NYB", start=start, end=end, interval="1h", progress=False) + + eurusd_last = eurusd['Close'].iloc[-1] if not eurusd.empty else "N/A" + eurusd_change = ((eurusd['Close'].iloc[-1] / eurusd['Close'].iloc[-24]) - 1) * 100 if len(eurusd) > 24 else "N/A" + dxy_last = dxy['Close'].iloc[-1] if not dxy.empty else "N/A" + + # Realized volatility (24h) + if len(eurusd) > 1: + returns = eurusd['Close'].pct_change().dropna() + vol_24h = returns.tail(24).std() * (24**0.5) * 100 + else: + vol_24h = "N/A" + + return f""" +EURUSD Current: {eurusd_last:.5f if isinstance(eurusd_last, float) else eurusd_last} +EURUSD 24h Change: {eurusd_change:.3f}% if isinstance(eurusd_change, float) else eurusd_change} +DXY (Dollar Index): {dxy_last:.2f if isinstance(dxy_last, float) else dxy_last} +Realized Volatility 24h: {vol_24h:.4f}% if isinstance(vol_24h, float) else vol_24h} +""" + except Exception as e: + return f"Macro data unavailable: {e}" + + +def create_macro_analyst(llm): + def macro_analyst_node(state): + factor_report = state.get("factor_report", "") + current_date = state.get("trade_date", "") + + macro_data = get_fx_macro_data() + + prompt = f"""You are an FX Macro Analyst specialized in EURUSD trading. + +Current Date: {current_date} + +Live Macro Data: +{macro_data} + +Factor Report from Predix RD-Agent: +{factor_report} + +Analyze the macro environment and its impact on the proposed factor: +1. DXY trend and correlation with EURUSD +2. Current volatility regime (low/normal/high) and what it means for the factor +3. Any macro risks that could invalidate the factor signal +4. Recommended position sizing given current volatility + +End with a markdown table of key macro indicators and their signal implications. +""" + response = llm.invoke(prompt) + report = response.content if hasattr(response, 'content') else str(response) + + return { + "messages": [response], + "macro_report": report, + } + + return macro_analyst_node diff --git a/rdagent/scenarios/qlib/fx_validator/agents/analysts/session_analyst.py b/rdagent/scenarios/qlib/fx_validator/agents/analysts/session_analyst.py new file mode 100644 index 00000000..1fbc6a35 --- /dev/null +++ b/rdagent/scenarios/qlib/fx_validator/agents/analysts/session_analyst.py @@ -0,0 +1,72 @@ +""" +Session Analyst — analysiert welche FX Session gerade aktiv ist +und gibt Momentum vs Mean-Reversion Empfehlung +""" +from datetime import datetime, timezone +import pandas as pd + + +def create_session_analyst(llm): + def session_analyst_node(state): + factor_report = state.get("factor_report", "") + current_date = state.get("trade_date", datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M")) + + # Aktuelle UTC Stunde bestimmen + try: + dt = datetime.fromisoformat(current_date.replace("Z", "+00:00")) + hour_utc = dt.hour + except Exception: + hour_utc = datetime.now(timezone.utc).hour + + # Session bestimmen + if 0 <= hour_utc < 8: + session = "Asian" + regime = "Mean Reversion" + session_note = "Low volume, ranging market. Mean reversion factors perform better. Avoid momentum strategies." + elif 8 <= hour_utc < 13: + session = "London" + regime = "Momentum/Trending" + session_note = "High volume, trending market. Momentum factors perform better. London open breakouts common." + elif 13 <= hour_utc < 16: + session = "London-NY Overlap" + regime = "Strong Momentum" + session_note = "Highest volume of the day. Strong directional moves. Best session for momentum factors." + elif 16 <= hour_utc < 21: + session = "NY" + regime = "Momentum/Reversal" + session_note = "Moderate volume. Watch for reversals after London close at 16:00." + else: + session = "After Hours" + regime = "Low Liquidity" + session_note = "Very low volume. Spreads widen. Avoid trading." + + prompt = f"""You are an FX Session Analyst specialized in EURUSD intraday dynamics. + +Current UTC time: {current_date} +Active Session: {session} +Expected Regime: {regime} +Session Notes: {session_note} + +Factor Report from Predix RD-Agent: +{factor_report} + +Analyze whether the proposed factor is suitable for the current session regime. +Provide a detailed report covering: +1. Session characteristics and expected price behavior +2. Whether the factor aligns with the current session regime +3. Recommended adjustments if needed (e.g., apply only during London hours) +4. Risk considerations for current session + +End with a markdown table summarizing your findings. +""" + response = llm.invoke(prompt) + report = response.content if hasattr(response, 'content') else str(response) + + return { + "messages": [response], + "session_report": report, + "current_session": session, + "current_regime": regime, + } + + return session_analyst_node diff --git a/rdagent/scenarios/qlib/fx_validator/agents/researchers/bear_researcher.py b/rdagent/scenarios/qlib/fx_validator/agents/researchers/bear_researcher.py new file mode 100644 index 00000000..9bba56f7 --- /dev/null +++ b/rdagent/scenarios/qlib/fx_validator/agents/researchers/bear_researcher.py @@ -0,0 +1,46 @@ +""" +Bear FX Researcher — argumentiert GEGEN den vorgeschlagenen Faktor +""" + + +def create_fx_bear_researcher(llm): + def bear_node(state): + debate_state = state.get("fx_debate_state", {}) + history = debate_state.get("history", "") + current_response = debate_state.get("current_response", "") + + factor_report = state.get("factor_report", "") + session_report = state.get("session_report", "") + macro_report = state.get("macro_report", "") + + prompt = f"""You are a Bear FX Analyst arguing AGAINST using this trading factor on EURUSD. + +Factor Analysis: {factor_report} +Session Analysis: {session_report} +Macro Analysis: {macro_report} +Debate History: {history} +Bull's Last Argument: {current_response} + +Build a critical case AGAINST this factor: +1. Overfitting risk — does IC hold out-of-sample? +2. Spread cost impact — does 1.5 bps erode the edge? +3. Session limitations — does the factor fail in Asian session? +4. Macro regime risk — does the factor break in high-volatility environments? +5. Counter the bull's specific claims with data + +Be specific and critical. Challenge every assumption. +""" + response = llm.invoke(prompt) + argument = f"Bear Analyst: {response.content if hasattr(response, 'content') else str(response)}" + + new_debate_state = { + "history": history + "\n" + argument, + "bear_history": debate_state.get("bear_history", "") + "\n" + argument, + "bull_history": debate_state.get("bull_history", ""), + "current_response": argument, + "count": debate_state.get("count", 0) + 1, + } + + return {"fx_debate_state": new_debate_state} + + return bear_node diff --git a/rdagent/scenarios/qlib/fx_validator/agents/researchers/bull_researcher.py b/rdagent/scenarios/qlib/fx_validator/agents/researchers/bull_researcher.py new file mode 100644 index 00000000..1301b828 --- /dev/null +++ b/rdagent/scenarios/qlib/fx_validator/agents/researchers/bull_researcher.py @@ -0,0 +1,45 @@ +""" +Bull FX Researcher — argumentiert FÜR den vorgeschlagenen Faktor +""" + + +def create_fx_bull_researcher(llm): + def bull_node(state): + debate_state = state.get("fx_debate_state", {}) + history = debate_state.get("history", "") + current_response = debate_state.get("current_response", "") + + factor_report = state.get("factor_report", "") + session_report = state.get("session_report", "") + macro_report = state.get("macro_report", "") + + prompt = f"""You are a Bull FX Analyst advocating FOR using this trading factor on EURUSD. + +Factor Analysis: {factor_report} +Session Analysis: {session_report} +Macro Analysis: {macro_report} +Debate History: {history} +Bear's Last Argument: {current_response} + +Build a strong evidence-based case FOR this factor: +1. Why the IC and ARR justify using this factor +2. How session timing makes this factor especially effective +3. Why spread costs are manageable given the signal strength +4. Counter the bear's specific concerns with data + +Be specific, use numbers from the reports. Engage directly with bear arguments. +""" + response = llm.invoke(prompt) + argument = f"Bull Analyst: {response.content if hasattr(response, 'content') else str(response)}" + + new_debate_state = { + "history": history + "\n" + argument, + "bull_history": debate_state.get("bull_history", "") + "\n" + argument, + "bear_history": debate_state.get("bear_history", ""), + "current_response": argument, + "count": debate_state.get("count", 0) + 1, + } + + return {"fx_debate_state": new_debate_state} + + return bull_node diff --git a/rdagent/scenarios/qlib/fx_validator/agents/trader/fx_trader.py b/rdagent/scenarios/qlib/fx_validator/agents/trader/fx_trader.py new file mode 100644 index 00000000..2111f4c9 --- /dev/null +++ b/rdagent/scenarios/qlib/fx_validator/agents/trader/fx_trader.py @@ -0,0 +1,70 @@ +""" +FX Trader — trifft finale BUY/HOLD/SELL Entscheidung +basierend auf allen Analyst- und Researcher-Reports +""" + + +def create_fx_trader(llm): + def trader_node(state): + factor_report = state.get("factor_report", "") + session_report = state.get("session_report", "") + macro_report = state.get("macro_report", "") + debate_state = state.get("fx_debate_state", {}) + debate_history = debate_state.get("history", "") + risk_report = state.get("risk_report", "") + + prompt = f"""You are an FX Trading Decision Agent for EURUSD 15min intraday trading. + +You have received reports from your team: + +FACTOR ANALYSIS (Predix RD-Agent): +{factor_report} + +SESSION ANALYSIS: +{session_report} + +MACRO ANALYSIS: +{macro_report} + +BULL vs BEAR DEBATE: +{debate_history} + +RISK ASSESSMENT: +{risk_report} + +Based on all available information, make a final trading decision: +- APPROVE: Factor is valid, deploy it in the live loop +- REJECT: Factor has critical flaws, do not deploy +- CONDITIONAL: Deploy only under specific conditions (specify which) + +Consider: +1. IC > 0.02 threshold +2. ARR > 9.62% target +3. Spread costs vs signal strength +4. Session-specific validity +5. Macro regime alignment + +End your response with exactly one of: +FINAL DECISION: **APPROVE** +FINAL DECISION: **REJECT** +FINAL DECISION: **CONDITIONAL: [conditions]** +""" + response = llm.invoke(prompt) + content = response.content if hasattr(response, 'content') else str(response) + + # Decision extrahieren + decision = "UNKNOWN" + if "FINAL DECISION: **APPROVE**" in content: + decision = "APPROVE" + elif "FINAL DECISION: **REJECT**" in content: + decision = "REJECT" + elif "FINAL DECISION: **CONDITIONAL" in content: + decision = "CONDITIONAL" + + return { + "messages": [response], + "trader_decision": content, + "final_decision": decision, + } + + return trader_node diff --git a/rdagent/scenarios/qlib/fx_validator/config.py b/rdagent/scenarios/qlib/fx_validator/config.py new file mode 100644 index 00000000..e07d4c74 --- /dev/null +++ b/rdagent/scenarios/qlib/fx_validator/config.py @@ -0,0 +1,27 @@ +""" +FX Validator Configuration +Angepasst für EURUSD 15min intraday trading +""" +import os + +FX_CONFIG = { + "instrument": "EURUSD=X", + "frequency": "15min", + "llm_provider": "openai", + "backend_url": os.getenv("OPENAI_API_BASE", "http://localhost:8081/v1"), + "api_key": os.getenv("OPENAI_API_KEY", "local"), + "deep_think_llm": os.getenv("CHAT_MODEL", "openai/qwen3.5-35b"), + "quick_think_llm": os.getenv("CHAT_MODEL", "openai/qwen3.5-35b"), + "max_debate_rounds": 2, + "max_risk_discuss_rounds": 1, + # FX-spezifisch + "spread_bps": 1.5, + "target_arr": 9.62, + "max_drawdown": 20.0, + "sessions": { + "asian": ("00:00", "08:00"), + "london": ("08:00", "16:00"), + "ny": ("13:00", "21:00"), + "overlap": ("13:00", "16:00"), + }, +} diff --git a/rdagent/scenarios/qlib/fx_validator/fx_graph.py b/rdagent/scenarios/qlib/fx_validator/fx_graph.py new file mode 100644 index 00000000..a5dbff81 --- /dev/null +++ b/rdagent/scenarios/qlib/fx_validator/fx_graph.py @@ -0,0 +1,115 @@ +""" +FX Validator Graph — Multi-Agent Validierung für Predix Faktoren +Inspiriert von TradingAgents, angepasst für EURUSD 15min +""" +from typing import TypedDict, Optional +from langgraph.graph import StateGraph, END +from langchain_openai import ChatOpenAI +import os + +from .agents.analysts.session_analyst import create_session_analyst +from .agents.analysts.macro_analyst import create_macro_analyst +from .agents.researchers.bull_researcher import create_fx_bull_researcher +from .agents.researchers.bear_researcher import create_fx_bear_researcher +from .agents.trader.fx_trader import create_fx_trader +from .config import FX_CONFIG + + +class FXValidatorState(TypedDict): + factor_report: str + trade_date: str + session_report: str + macro_report: str + fx_debate_state: dict + risk_report: str + trader_decision: str + final_decision: str + messages: list + + +def create_fx_validator(config: dict = None): + cfg = config or FX_CONFIG + + llm = ChatOpenAI( + model=cfg["deep_think_llm"].replace("openai/", ""), + base_url=cfg["backend_url"], + api_key=cfg["api_key"], + temperature=0.5, + ) + + # Agenten erstellen + session_analyst = create_session_analyst(llm) + macro_analyst = create_macro_analyst(llm) + bull_researcher = create_fx_bull_researcher(llm) + bear_researcher = create_fx_bear_researcher(llm) + fx_trader = create_fx_trader(llm) + + # Debate Loop + def should_continue_debate(state): + count = state.get("fx_debate_state", {}).get("count", 0) + max_rounds = cfg.get("max_debate_rounds", 2) * 2 + if count >= max_rounds: + return "trader" + return "bear" if count % 2 == 0 else "bull" + + # Graph bauen + graph = StateGraph(FXValidatorState) + + graph.add_node("session_analyst", session_analyst) + graph.add_node("macro_analyst", macro_analyst) + graph.add_node("bull", bull_researcher) + graph.add_node("bear", bear_researcher) + graph.add_node("trader", fx_trader) + + graph.set_entry_point("session_analyst") + graph.add_edge("session_analyst", "macro_analyst") + graph.add_edge("macro_analyst", "bull") + + graph.add_conditional_edges( + "bull", + should_continue_debate, + {"bear": "bear", "bull": "bull", "trader": "trader"} + ) + graph.add_conditional_edges( + "bear", + should_continue_debate, + {"bear": "bear", "bull": "bull", "trader": "trader"} + ) + + graph.add_edge("trader", END) + + return graph.compile() + + +def validate_factor(factor_report: str, trade_date: str = None) -> dict: + """ + Hauptfunktion — validiert einen Predix-Faktor durch Multi-Agent Debatte + + Args: + factor_report: Der Faktor-Report von Predix RD-Agent + trade_date: Datum/Zeit in ISO Format (default: jetzt) + + Returns: + dict mit final_decision (APPROVE/REJECT/CONDITIONAL) und Reports + """ + from datetime import datetime, timezone + + if trade_date is None: + trade_date = datetime.now(timezone.utc).isoformat() + + validator = create_fx_validator() + + initial_state = { + "factor_report": factor_report, + "trade_date": trade_date, + "session_report": "", + "macro_report": "", + "fx_debate_state": {"history": "", "bull_history": "", "bear_history": "", "current_response": "", "count": 0}, + "risk_report": "", + "trader_decision": "", + "final_decision": "", + "messages": [], + } + + result = validator.invoke(initial_state) + return result