feat: FX Multi-Agent Validator (TradingAgents-inspired) - Session/Macro/Bull-Bear/Trader

This commit is contained in:
TPTBusiness
2026-03-22 21:57:03 +01:00
parent b7c1e4db8e
commit cddfc53ab0
9 changed files with 451 additions and 0 deletions
@@ -0,0 +1,3 @@
from .fx_graph import validate_factor, create_fx_validator
__all__ = ["validate_factor", "create_fx_validator"]
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"),
},
}
@@ -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