mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-27 23:47:46 +00:00
feat: FX Multi-Agent Validator (TradingAgents-inspired) - Session/Macro/Bull-Bear/Trader
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user