mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-27 23:47:46 +00:00
feat: EURUSD Trading-Verbesserungen implementiert (Phase 1)
Neue Module für quantitatives EURUSD-Trading: 1. Hurst Exponent Regime Detection (eurusd_regime.py) - Erkennt Marktregime: MEAN_REVERSION, NEUTRAL, TRENDING - R/S-Analyse für 1min EURUSD-Daten optimiert - Trading-Empfehlungen pro Regime 2. BM25 Memory-System (eurusd_memory.py) - Speichert vergangene Trades mit Situation/Ergebnis - Findet ähnliche Setups via BM25-Ähnlichkeit - Persistente JSON-Speicherung - Historische Win-Rate Analyse 3. Volatility-Adjusted Position Sizing (eurusd_risk.py) - ATR-basierte Volatilitätsmessung - Positionsgröße nach Volatilitäts-Percentile (0.4x-1.5x) - Regime-Adjustierung (MEAN_REVERSION/TRENDING/NEUTRAL) - Korrelations-Adjustierung für Forex-Paare 4. Multi-Provider LLM Fallback (eurusd_llm.py) - Automatische Fallback-Kette bei API-Ausfällen - Provider: Qwen3.5 → DeepSeek → Gemini → Ollama - Provider-Statistiken für Monitoring - JSON-Modus für strukturierte Outputs Daten-Pipeline verbessert: - 1-Minuten-Daten korrekt in Qlib integriert - Prompts von 15min auf 1min aktualisiert - generate.py für 1min EURUSD-Daten angepasst Alle Module einzeln und im Integrationstest bestanden.
This commit is contained in:
@@ -0,0 +1,467 @@
|
||||
"""
|
||||
BM25 Memory-System für EURUSD Trading-Setups
|
||||
|
||||
Inspiriert von: TradingAgents/tradingagents/agents/utils/memory.py
|
||||
|
||||
Vorteile gegenüber Vector-DBs:
|
||||
- Keine API-Kosten (offline-fähig)
|
||||
- Keine Token-Limits
|
||||
- Lexikalische Ähnlichkeit (präzise für Trading-Setups)
|
||||
- Schnell und einfach zu implementieren
|
||||
"""
|
||||
|
||||
import json
|
||||
import pickle
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
from rank_bm25 import BM25Okapi
|
||||
|
||||
|
||||
def tokenize(text: str) -> List[str]:
|
||||
"""
|
||||
Tokenisiert Text für BM25-Verarbeitung.
|
||||
|
||||
- Entfernt Sonderzeichen
|
||||
- Konvertiert zu Kleinbuchstaben
|
||||
- Split auf Wörter und Zahlen
|
||||
|
||||
Parameters
|
||||
----------
|
||||
text : str
|
||||
Eingabetext (Trading-Situation, Setup-Beschreibung)
|
||||
|
||||
Returns
|
||||
-------
|
||||
List[str]
|
||||
Liste von Tokens
|
||||
"""
|
||||
# Konvertiere zu Kleinbuchstaben
|
||||
text = text.lower()
|
||||
|
||||
# Extrahiere Wörter und Zahlen (inkl. Dezimalzahlen wie 1.0850)
|
||||
tokens = re.findall(r'\b\w+(?:\.\d+)?\b', text)
|
||||
|
||||
# Filtere sehr kurze Tokens (< 2 Zeichen)
|
||||
tokens = [t for t in tokens if len(t) >= 2]
|
||||
|
||||
return tokens
|
||||
|
||||
|
||||
class EURUSDTradeMemory:
|
||||
"""
|
||||
BM25-basiertes Memory-System für vergangene EURUSD-Trading-Setups.
|
||||
|
||||
Speichert vergangene Trades mit:
|
||||
- Marktsituation (Features, Regime, Indikatoren)
|
||||
- Entscheidung (LONG/SHORT/NEUTRAL, Leverage, SL, TP)
|
||||
- Ergebnis (PnL, Win/Loss)
|
||||
- Reflection (Lessons Learned)
|
||||
|
||||
Bei neuer Situation: Findet ähnliche vergangene Setups und gibt
|
||||
historische Win-Rate und durchschnittliche Rendite zurück.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
memory_file : Path
|
||||
Pfad zur persistenten Speicherdatei (JSON)
|
||||
memories : List[dict]
|
||||
Liste aller gespeicherten Trades
|
||||
bm25 : BM25Okapi
|
||||
BM25-Index für schnelle Ähnlichkeitssuche
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> memory = EURUSDTradeMemory()
|
||||
>>> memory.add_trade(
|
||||
... situation="EURUSD 1.0850, RSI=28, Hurst=0.52 (MEAN_REVERSION), EZB hawkish",
|
||||
... decision={"action": "LONG", "leverage": 20, "sl_pips": 25, "tp_pips": 15},
|
||||
... outcome=0.023, # +2.3% Gewinn
|
||||
... reflection="RSI < 30 in Mean-Reversion Regime war erfolgreich"
|
||||
... )
|
||||
>>> similar = memory.get_similar_setups("EURUSD 1.0820, RSI=25, Hurst=0.48")
|
||||
>>> print(f"Historische Win-Rate: {similar['historical_win_rate']:.1%}")
|
||||
"""
|
||||
|
||||
def __init__(self, memory_file: str = "git_ignore_folder/eurusd_trade_memory.json"):
|
||||
"""
|
||||
Initialisiert das Memory-System.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
memory_file : str
|
||||
Pfad zur JSON-Datei für persistente Speicherung
|
||||
"""
|
||||
self.memory_file = Path(memory_file)
|
||||
self.memories: List[dict] = []
|
||||
self.bm25: Optional[BM25Okapi] = None
|
||||
self.tokenized_memories: List[List[str]] = []
|
||||
|
||||
# Lade existierende Memories von Datei
|
||||
if self.memory_file.exists():
|
||||
self.load()
|
||||
|
||||
def add_trade(
|
||||
self,
|
||||
situation: str,
|
||||
decision: dict,
|
||||
outcome: float,
|
||||
reflection: Optional[str] = None
|
||||
) -> None:
|
||||
"""
|
||||
Speichert einen vergangenen Trade im Memory.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
situation : str
|
||||
Beschreibung der Marktsituation zum Zeitpunkt des Trades.
|
||||
Beispiel: "EURUSD 1.0850, RSI=28, Hurst=0.52 (MEAN_REVERSION),
|
||||
London Session, EZB hawkish, DXY downtrend"
|
||||
decision : dict
|
||||
Trade-Entscheidung mit Details.
|
||||
Beispiel: {"action": "LONG", "leverage": 20, "sl_pips": 25, "tp_pips": 15}
|
||||
outcome : float
|
||||
Ergebnis des Trades als Dezimalzahl.
|
||||
Beispiel: 0.023 = +2.3% Gewinn, -0.015 = -1.5% Verlust
|
||||
reflection : str, optional
|
||||
Lessons Learned nach dem Trade (vom Reflection-System generiert).
|
||||
"""
|
||||
trade_record = {
|
||||
"id": len(self.memories) + 1,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"situation": situation,
|
||||
"decision": decision,
|
||||
"outcome": outcome,
|
||||
"reflection": reflection or "",
|
||||
"tokens": tokenize(situation)
|
||||
}
|
||||
|
||||
self.memories.append(trade_record)
|
||||
self.tokenized_memories.append(trade_record["tokens"])
|
||||
|
||||
# Rebuild BM25 Index
|
||||
self._rebuild_bm25()
|
||||
|
||||
# Speichere auf Festplatte
|
||||
self.save()
|
||||
|
||||
def add_trades_batch(self, trades: List[dict]) -> None:
|
||||
"""
|
||||
Fügt mehrere Trades auf einmal hinzu (effizienter als einzelne add_trade Aufrufe).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
trades : List[dict]
|
||||
Liste von Trade-Records mit Keys: situation, decision, outcome, reflection
|
||||
"""
|
||||
for trade in trades:
|
||||
trade_record = {
|
||||
"id": len(self.memories) + len(trades),
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"situation": trade["situation"],
|
||||
"decision": trade["decision"],
|
||||
"outcome": trade["outcome"],
|
||||
"reflection": trade.get("reflection", ""),
|
||||
"tokens": tokenize(trade["situation"])
|
||||
}
|
||||
self.memories.append(trade_record)
|
||||
self.tokenized_memories.append(trade_record["tokens"])
|
||||
|
||||
self._rebuild_bm25()
|
||||
self.save()
|
||||
|
||||
def get_similar_setups(
|
||||
self,
|
||||
current_situation: str,
|
||||
n: int = 5,
|
||||
min_similarity: float = 0.0
|
||||
) -> dict:
|
||||
"""
|
||||
Findet ähnliche vergangene Trading-Setups.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
current_situation : str
|
||||
Aktuelle Marktsituation (gleiche Formatierung wie bei add_trade)
|
||||
n : int, default 5
|
||||
Anzahl der zurückzugebenden ähnlichen Setups
|
||||
min_similarity : float, default 0.0
|
||||
Minimale BM25-Ähnlichkeit für Treffer
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Ähnliche Setups mit Statistiken:
|
||||
- similar_setups: Liste der Top-N ähnlichen Trades
|
||||
- historical_win_rate: Win-Rate der ähnlichen Setups
|
||||
- historical_avg_return: Durchschnittliche Rendite
|
||||
- best_setup: Bestes historisches Setup
|
||||
- recommendation: Handlungsempfehlung basierend auf History
|
||||
"""
|
||||
if len(self.memories) == 0:
|
||||
return {
|
||||
"similar_setups": [],
|
||||
"historical_win_rate": 0.0,
|
||||
"historical_avg_return": 0.0,
|
||||
"message": "Keine historischen Trades gespeichert"
|
||||
}
|
||||
|
||||
# Tokenisiere aktuelle Situation
|
||||
query_tokens = tokenize(current_situation)
|
||||
|
||||
# Berechne BM25-Ähnlichkeiten
|
||||
scores = self.bm25.get_scores(query_tokens)
|
||||
|
||||
# Finde Top-N Treffer
|
||||
top_indices = np.argsort(scores)[::-1][:n]
|
||||
|
||||
# Filtere nach min_similarity
|
||||
filtered_indices = [
|
||||
i for i in top_indices
|
||||
if scores[i] >= min_similarity
|
||||
]
|
||||
|
||||
if len(filtered_indices) == 0:
|
||||
return {
|
||||
"similar_setups": [],
|
||||
"historical_win_rate": 0.0,
|
||||
"historical_avg_return": 0.0,
|
||||
"message": f"Keine ähnlichen Setups gefunden (min_similarity={min_similarity})"
|
||||
}
|
||||
|
||||
# Sammle ähnliche Setups
|
||||
similar_setups = []
|
||||
outcomes = []
|
||||
|
||||
for idx in filtered_indices:
|
||||
memory = self.memories[idx]
|
||||
similar_setups.append({
|
||||
"id": memory["id"],
|
||||
"situation": memory["situation"],
|
||||
"decision": memory["decision"],
|
||||
"outcome": memory["outcome"],
|
||||
"reflection": memory["reflection"],
|
||||
"similarity_score": float(scores[idx]),
|
||||
"timestamp": memory["timestamp"]
|
||||
})
|
||||
outcomes.append(memory["outcome"])
|
||||
|
||||
# Berechne Statistiken
|
||||
outcomes_array = np.array(outcomes)
|
||||
win_rate = np.mean(outcomes_array > 0)
|
||||
avg_return = np.mean(outcomes_array)
|
||||
std_return = np.std(outcomes_array) if len(outcomes) > 1 else 0.0
|
||||
|
||||
# Finde bestes Setup
|
||||
best_idx = np.argmax(outcomes_array)
|
||||
best_setup = similar_setups[best_idx]
|
||||
|
||||
# Generiere Empfehlung
|
||||
if win_rate > 0.7 and len(filtered_indices) >= 3:
|
||||
recommendation = "STRONG_SIGNAL"
|
||||
rec_text = f"Starke Historie: {win_rate:.0%} Win-Rate in {len(filtered_indices)} ähnlichen Situationen"
|
||||
elif win_rate > 0.55:
|
||||
recommendation = "MODERATE_SIGNAL"
|
||||
rec_text = f"Moderate Historie: {win_rate:.0%} Win-Rate"
|
||||
elif win_rate < 0.4 and len(filtered_indices) >= 3:
|
||||
recommendation = "AVOID"
|
||||
rec_text = f"Schwache Historie: Nur {win_rate:.0%} Win-Rate - Setup vermeiden!"
|
||||
else:
|
||||
recommendation = "NEUTRAL"
|
||||
rec_text = f"Neutrale Historie: {win_rate:.0%} Win-Rate, zu wenig Daten für klare Empfehlung"
|
||||
|
||||
return {
|
||||
"similar_setups": similar_setups,
|
||||
"historical_win_rate": float(win_rate),
|
||||
"historical_avg_return": float(avg_return),
|
||||
"historical_std_return": float(std_return),
|
||||
"n_similar_trades": len(filtered_indices),
|
||||
"best_setup": best_setup,
|
||||
"recommendation": recommendation,
|
||||
"recommendation_text": rec_text
|
||||
}
|
||||
|
||||
def get_memory_stats(self) -> dict:
|
||||
"""
|
||||
Gibt Statistiken über das gespeicherte Memory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Memory-Statistiken:
|
||||
- total_trades: Gesamtanzahl Trades
|
||||
- win_rate: Gesamte Win-Rate
|
||||
- avg_return: Durchschnittliche Rendite
|
||||
- best_trade: Bester Trade
|
||||
- worst_trade: Schlechtester Trade
|
||||
- recent_performance: Performance der letzten 10 Trades
|
||||
"""
|
||||
if len(self.memories) == 0:
|
||||
return {"message": "Keine Trades gespeichert"}
|
||||
|
||||
outcomes = [m["outcome"] for m in self.memories]
|
||||
outcomes_array = np.array(outcomes)
|
||||
|
||||
# Recent Performance (letzte 10 Trades)
|
||||
recent_outcomes = outcomes_array[-10:] if len(outcomes) > 10 else outcomes_array
|
||||
|
||||
return {
|
||||
"total_trades": len(self.memories),
|
||||
"win_rate": float(np.mean(outcomes_array > 0)),
|
||||
"avg_return": float(np.mean(outcomes_array)),
|
||||
"std_return": float(np.std(outcomes_array)),
|
||||
"sharpe_ratio": float(np.mean(outcomes_array) / np.std(outcomes_array)) if np.std(outcomes_array) > 0 else 0.0,
|
||||
"best_trade": {
|
||||
"id": self.memories[np.argmax(outcomes_array)]["id"],
|
||||
"outcome": float(np.max(outcomes_array)),
|
||||
"situation": self.memories[np.argmax(outcomes_array)]["situation"]
|
||||
},
|
||||
"worst_trade": {
|
||||
"id": self.memories[np.argmin(outcomes_array)]["id"],
|
||||
"outcome": float(np.min(outcomes_array)),
|
||||
"situation": self.memories[np.argmin(outcomes_array)]["situation"]
|
||||
},
|
||||
"recent_performance": {
|
||||
"n_trades": len(recent_outcomes),
|
||||
"win_rate": float(np.mean(recent_outcomes > 0)),
|
||||
"avg_return": float(np.mean(recent_outcomes))
|
||||
}
|
||||
}
|
||||
|
||||
def _rebuild_bm25(self) -> None:
|
||||
"""
|
||||
Baut den BM25-Index neu auf (nach Hinzufügen neuer Trades).
|
||||
"""
|
||||
if len(self.tokenized_memories) > 0:
|
||||
self.bm25 = BM25Okapi(self.tokenized_memories)
|
||||
|
||||
def save(self) -> None:
|
||||
"""
|
||||
Speichert das Memory persistent auf die Festplatte.
|
||||
"""
|
||||
# Erstelle Verzeichnis falls nicht existent
|
||||
self.memory_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Speichere als JSON (ohne BM25-Index, der wird beim Laden neu gebaut)
|
||||
save_data = []
|
||||
for memory in self.memories:
|
||||
save_entry = {k: v for k, v in memory.items() if k != "tokens"}
|
||||
save_data.append(save_entry)
|
||||
|
||||
with open(self.memory_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(save_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def load(self) -> None:
|
||||
"""
|
||||
Lädt das Memory von der Festplatte.
|
||||
"""
|
||||
try:
|
||||
with open(self.memory_file, 'r', encoding='utf-8') as f:
|
||||
save_data = json.load(f)
|
||||
|
||||
self.memories = []
|
||||
self.tokenized_memories = []
|
||||
|
||||
for entry in save_data:
|
||||
entry["tokens"] = tokenize(entry["situation"])
|
||||
self.memories.append(entry)
|
||||
self.tokenized_memories.append(entry["tokens"])
|
||||
|
||||
self._rebuild_bm25()
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Fehler beim Laden des Memory: {e}")
|
||||
self.memories = []
|
||||
self.tokenized_memories = []
|
||||
|
||||
def clear(self) -> None:
|
||||
"""
|
||||
Löscht das gesamte Memory.
|
||||
"""
|
||||
self.memories = []
|
||||
self.tokenized_memories = []
|
||||
self.bm25 = None
|
||||
|
||||
if self.memory_file.exists():
|
||||
self.memory_file.unlink()
|
||||
|
||||
|
||||
# Test-Funktion für lokale Validierung
|
||||
if __name__ == "__main__":
|
||||
print("=== BM25 Memory Test ===\n")
|
||||
|
||||
# Erstelle Test-Memory
|
||||
memory = EURUSDTradeMemory(memory_file="git_ignore_folder/test_trade_memory.json")
|
||||
|
||||
# Füge Beispiel-Trades hinzu
|
||||
test_trades = [
|
||||
{
|
||||
"situation": "EURUSD 1.0850, RSI=28, Hurst=0.52 (MEAN_REVERSION), London Session, EZB hawkish, DXY downtrend",
|
||||
"decision": {"action": "LONG", "leverage": 20, "sl_pips": 25, "tp_pips": 15},
|
||||
"outcome": 0.023,
|
||||
"reflection": "RSI < 30 in Mean-Reversion Regime war erfolgreich"
|
||||
},
|
||||
{
|
||||
"situation": "EURUSD 1.0920, RSI=72, Hurst=0.58 (NEUTRAL), NY Session, Fed dovish, DXY weak",
|
||||
"decision": {"action": "SHORT", "leverage": 15, "sl_pips": 30, "tp_pips": 20},
|
||||
"outcome": 0.015,
|
||||
"reflection": "RSI > 70 mit Mean-Reversion funktioniert gut"
|
||||
},
|
||||
{
|
||||
"situation": "EURUSD 1.0780, RSI=25, Hurst=0.48 (MEAN_REVERSION), Asian Session, low volatility",
|
||||
"decision": {"action": "LONG", "leverage": 10, "sl_pips": 20, "tp_pips": 12},
|
||||
"outcome": -0.012,
|
||||
"reflection": "Asian Session zu wenig Volumen für Mean-Reversion"
|
||||
},
|
||||
{
|
||||
"situation": "EURUSD 1.0950, RSI=65, Hurst=0.72 (TRENDING), London-NY Overlap, strong momentum",
|
||||
"decision": {"action": "LONG", "leverage": 25, "sl_pips": 20, "tp_pips": 35},
|
||||
"outcome": 0.035,
|
||||
"reflection": "Trending Regime mit Momentum war sehr profitabel"
|
||||
},
|
||||
{
|
||||
"situation": "EURUSD 1.0880, RSI=45, Hurst=0.61 (NEUTRAL), no clear direction, choppy market",
|
||||
"decision": {"action": "NEUTRAL", "leverage": 0, "sl_pips": 0, "tp_pips": 0},
|
||||
"outcome": 0.0,
|
||||
"reflection": "Abwarten war die beste Entscheidung in choppy Market"
|
||||
},
|
||||
]
|
||||
|
||||
memory.add_trades_batch(test_trades)
|
||||
print(f"✅ {len(test_trades)} Trades zum Memory hinzugefügt\n")
|
||||
|
||||
# Teste Ähnlichkeitssuche
|
||||
print("=== Test 1: Ähnliche Setups finden ===")
|
||||
query = "EURUSD 1.0840, RSI=26, Hurst=0.50, MEAN_REVERSION, EZB hawkish"
|
||||
similar = memory.get_similar_setups(query, n=3)
|
||||
|
||||
print(f"Query: {query}")
|
||||
print(f"Gefundene ähnliche Setups: {similar.get('n_similar_trades', 0)}")
|
||||
print(f"Historische Win-Rate: {similar.get('historical_win_rate', 0):.1%}")
|
||||
print(f"Durchschnittliche Rendite: {similar.get('historical_avg_return', 0):.2%}")
|
||||
print(f"Empfehlung: {similar.get('recommendation', 'N/A')} - {similar.get('recommendation_text', '')}")
|
||||
|
||||
# Teste Memory-Statistiken
|
||||
print("\n=== Test 2: Memory Statistiken ===")
|
||||
stats = memory.get_memory_stats()
|
||||
print(f"Gesamte Trades: {stats.get('total_trades', 0)}")
|
||||
print(f"Gesamte Win-Rate: {stats.get('win_rate', 0):.1%}")
|
||||
print(f"Durchschnittliche Rendite: {stats.get('avg_return', 0):.2%}")
|
||||
print(f"Sharpe Ratio: {stats.get('sharpe_ratio', 0):.2f}")
|
||||
print(f"Bester Trade: {stats.get('best_trade', {}).get('outcome', 0):.2%}")
|
||||
print(f"Schlechtester Trade: {stats.get('worst_trade', {}).get('outcome', 0):.2%}")
|
||||
|
||||
# Teste Persistenz
|
||||
print("\n=== Test 3: Persistenz ===")
|
||||
memory2 = EURUSDTradeMemory(memory_file="git_ignore_folder/test_trade_memory.json")
|
||||
print(f"Memory nach Neuladen: {len(memory2.memories)} Trades")
|
||||
|
||||
# Cleanup
|
||||
import os
|
||||
if os.path.exists("git_ignore_folder/test_trade_memory.json"):
|
||||
os.remove("git_ignore_folder/test_trade_memory.json")
|
||||
|
||||
print("\n✅ BM25 Memory Implementierung ist funktionsfähig!")
|
||||
Reference in New Issue
Block a user