""" BM25 Memory-System für 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) 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 # nosec 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"⚠️ Error loading 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!")