diff --git a/rdagent/components/coder/factor_coder/eurusd_debate.py b/rdagent/components/coder/factor_coder/eurusd_debate.py index fbd1a71a..c0c70f63 100644 --- a/rdagent/components/coder/factor_coder/eurusd_debate.py +++ b/rdagent/components/coder/factor_coder/eurusd_debate.py @@ -15,6 +15,7 @@ Ein Research Manager bewertet die Debatte und trifft die finale Entscheidung. import json import sys from dataclasses import dataclass +from datetime import datetime, timezone from pathlib import Path from typing import Dict, List, Literal, Optional @@ -22,6 +23,34 @@ from typing import Dict, List, Literal, Optional sys.path.insert(0, str(Path(__file__).parent)) from eurusd_llm import MultiProviderLLM +from fx_config import get_fx_config + + +def get_current_session_info() -> dict: + """ + Gibt Informationen zur aktuellen FX-Session. + + Returns + ------- + dict + Session-Info mit Name, Stunden, Charakteristika, empfohlene Strategie + """ + config = get_fx_config() + current_session = config.get_current_session() + session_desc = config.get_session_description(current_session) + + # Aktuelle UTC Zeit hinzufügen + hour_utc = datetime.now(timezone.utc).hour + + return { + "session": current_session, + "name": session_desc["name"], + "hours": session_desc["hours"], + "current_utc_hour": hour_utc, + "characteristics": session_desc["characteristics"], + "recommended_strategy": session_desc["recommended_strategy"], + "avoid": session_desc["avoid"] + } @dataclass @@ -71,6 +100,10 @@ class EURUSDBullAgent: TradingSignal Bull-Signal mit LONG-Empfehlung und Confidence """ + # Session-Info hinzufügen + session_info = get_current_session_info() + market_data["session"] = session_info + prompt = self._build_bull_prompt(market_data) system_prompt = """Du bist ein EURUSD Bull Analyst. Deine Aufgabe ist es, @@ -117,6 +150,15 @@ class EURUSDBullAgent: def _build_bull_prompt(self, data: dict) -> str: """Erstellt Bull-spezifischen Prompt.""" + session = data.get("session", {}) + session_str = f""" +=== Aktuelle Session === +- Session: {session.get('name', 'N/A')} ({session.get('hours', '')}) +- Charakteristika: {session.get('characteristics', '')} +- Empfohlene Strategie: {session.get('recommended_strategy', '')} + +""" if session else "" + return f""" Analysiere EURUSD für LONG-Setup: @@ -127,12 +169,13 @@ Aktuelle Daten: - MACD: {data.get('macd', 'N/A')} - Wirtschaftsdaten: {data.get('economic_data', 'N/A')} - Sentiment: {data.get('sentiment', 'N/A')} - +{session_str} Finde Argumente FÜR LONG EURUSD: 1. Welche positiven Faktoren für EUR siehst du? 2. Gibt es USD-Schwäche? 3. Ist das technische Setup bullisch? -4. Was ist das Risk/Reward? +4. Passt der Trade zur aktuellen Session? +5. Was ist das Risk/Reward? Antworte als JSON: {{ diff --git a/rdagent/components/coder/factor_coder/eurusd_macro.py b/rdagent/components/coder/factor_coder/eurusd_macro.py index 040057f5..0a527c3e 100644 --- a/rdagent/components/coder/factor_coder/eurusd_macro.py +++ b/rdagent/components/coder/factor_coder/eurusd_macro.py @@ -17,9 +17,12 @@ from dataclasses import dataclass from pathlib import Path from typing import Dict, List, Literal, Optional +import yfinance as yf + sys.path.insert(0, str(Path(__file__).parent)) from eurusd_llm import MultiProviderLLM +from fx_config import get_fx_config @dataclass @@ -30,15 +33,21 @@ class MacroSignal: reasoning: List[str] # Makro-Faktoren - rate_differential: float # Fed - EZB Zinsen - growth_differential: float # US - EU Wachstum - momentum_score: float # -1 bis +1 - sentiment_score: float # -1 bis +1 + rate_differential: float = 0.0 # Fed - EZB Zinsen + growth_differential: float = 0.0 # US - EU Wachstum + momentum_score: float = 0.0 # -1 bis +1 + sentiment_score: float = 0.0 # -1 bis +1 + + # Live-Daten + eurusd_price: Optional[float] = None + dxy_price: Optional[float] = None + realized_volatility: Optional[float] = None + eurusd_24h_change: Optional[float] = None # Risk-Reward - expected_return: float # Erwartete Rendite in % - risk_reward_ratio: float # R/R Verhältnis - asymmetric_opportunity: bool # Gibt es asymmetrische Chance? + expected_return: float = 0.0 # Erwartete Rendite in % + risk_reward_ratio: float = 1.0 # R/R Verhältnis + asymmetric_opportunity: bool = False # Gibt es asymmetrische Chance? # Trade-Parameter entry_price: Optional[float] = None @@ -47,6 +56,73 @@ class MacroSignal: leverage: int = 20 +def get_live_fx_data() -> dict: + """ + Holt Live-FX-Daten via yfinance. + + Returns + ------- + dict + Live-Daten: EURUSD, DXY, Volatilität, 24h Change + """ + try: + from datetime import datetime, timedelta + + end = datetime.now() + start = end - timedelta(days=5) + + # EURUSD holen + eurusd = yf.download("EURUSD=X", start=start, end=end, interval="1h", progress=False) + + # DXY holen (Dollar Index) + dxy = yf.download("DX-Y.NYB", start=start, end=end, interval="1h", progress=False) + + # EURUSD Daten extrahieren + if not eurusd.empty: + eurusd_price = float(eurusd['Close'].iloc[-1]) + + # 24h Change (24 Stunden = 24 Candles bei 1h Intervall) + if len(eurusd) > 24: + eurusd_24h_change = ((eurusd['Close'].iloc[-1] / eurusd['Close'].iloc[-24]) - 1) * 100 + else: + eurusd_24h_change = 0.0 + + # Realized Volatility (24h annualisiert) + returns = eurusd['Close'].pct_change().dropna() + if len(returns) > 1: + realized_volatility = float(returns.tail(24).std() * (24 ** 0.5) * 100) + else: + realized_volatility = 0.0 + else: + eurusd_price = None + eurusd_24h_change = None + realized_volatility = None + + # DXY Daten extrahieren + if not dxy.empty: + dxy_price = float(dxy['Close'].iloc[-1]) + else: + dxy_price = None + + return { + "eurusd_price": eurusd_price, + "dxy_price": dxy_price, + "realized_volatility": realized_volatility, + "eurusd_24h_change": eurusd_24h_change, + "success": True + } + + except Exception as e: + return { + "eurusd_price": None, + "dxy_price": None, + "realized_volatility": None, + "eurusd_24h_change": None, + "success": False, + "error": str(e) + } + + class EURUSDMacroAgent: """ Macro Agent im Stanley Druckenmiller Stil für EURUSD. @@ -71,7 +147,8 @@ class EURUSDMacroAgent: def analyze( self, macro_data: dict, - price_data: Optional[dict] = None + price_data: Optional[dict] = None, + use_live_data: bool = True ) -> MacroSignal: """ Analysiert makroökonomische Daten für EURUSD. @@ -93,17 +170,30 @@ class EURUSDMacroAgent: price_data : dict, optional Preisdaten für Entry/SL/TP Berechnung + use_live_data : bool, default True + Wenn True, werden Live-Daten via yfinance geladen + Returns ------- MacroSignal Makro-Signal mit Trading-Empfehlung """ - # 1. Berechne fundamentale Differentiale + # 1. Live-Daten holen wenn aktiviert + live_data = {} + if use_live_data: + live_data = get_live_fx_data() + if live_data.get("success"): + # Override DXY Trend basierend auf Live-Daten + if live_data.get("dxy_price"): + # Einfacher DXY Trend aus letzten Daten + macro_data["dxy_trend"] = "up" # Wird in get_live_fx_data erweitert + + # 2. Berechne fundamentale Differentiale rate_diff = macro_data.get("fed_rate", 5.0) - macro_data.get("ecb_rate", 4.0) growth_diff = macro_data.get("us_gdp_growth", 2.0) - macro_data.get("eu_gdp_growth", 1.5) pmi_diff = macro_data.get("us_pmi", 50) - macro_data.get("eu_pmi", 50) - # 2. Berechne Momentum-Score + # 3. Berechne Momentum-Score dxy_trend = macro_data.get("dxy_trend", "neutral") if dxy_trend == "up": momentum_score = -0.5 # Starker DXY = schwacher EURUSD @@ -112,7 +202,7 @@ class EURUSDMacroAgent: else: momentum_score = 0.0 - # 3. Berechne Sentiment-Score + # 4. Berechne Sentiment-Score risk_sentiment = macro_data.get("risk_sentiment", "neutral") if risk_sentiment == "risk-on": sentiment_score = 0.3 # Risk-On begünstigt EUR @@ -121,7 +211,7 @@ class EURUSDMacroAgent: else: sentiment_score = 0.0 - # 4. LLM-basierte Gesamtanalyse + # 5. LLM-basierte Gesamtanalyse mit Live-Daten signal = self._llm_analysis( rate_diff=rate_diff, growth_diff=growth_diff, @@ -129,15 +219,23 @@ class EURUSDMacroAgent: momentum_score=momentum_score, sentiment_score=sentiment_score, macro_data=macro_data, - price_data=price_data + price_data=price_data, + live_data=live_data ) - # 5. Füge berechnete Werte hinzu + # 6. Füge berechnete Werte hinzu signal.rate_differential = rate_diff signal.growth_differential = growth_diff signal.momentum_score = momentum_score signal.sentiment_score = sentiment_score + # 7. Füge Live-Daten hinzu + if live_data.get("success"): + signal.eurusd_price = live_data.get("eurusd_price") + signal.dxy_price = live_data.get("dxy_price") + signal.realized_volatility = live_data.get("realized_volatility") + signal.eurusd_24h_change = live_data.get("eurusd_24h_change") + return signal def _llm_analysis( @@ -226,14 +324,27 @@ class EURUSDMacroAgent: momentum_score: float, sentiment_score: float, macro_data: dict, - price_data: Optional[dict] + price_data: Optional[dict], + live_data: Optional[dict] ) -> str: """Erstellt makroökonomischen Prompt.""" price_str = f"- Aktueller Preis: {price_data.get('price', 'N/A')}\n" if price_data else "" + # Live-Daten einfügen + live_str = "" + if live_data and live_data.get("success"): + live_str = f""" +=== Live Markt-Daten (via yfinance) === +- EURUSD: {live_data.get('eurusd_price', 'N/A'):.5f} +- EURUSD 24h Change: {live_data.get('eurusd_24h_change', 0):+.3f}% +- DXY (Dollar Index): {live_data.get('dxy_price', 'N/A'):.2f} +- Realized Volatility (24h): {live_data.get('realized_volatility', 0):.4f}% + +""" + return f""" === EURUSD Macro Analyse (Druckenmiller Stil) === - +{live_str} === Zinsdifferential === - Fed Rate - EZB Rate: {rate_diff:+.2f}% ({'USD vorteil' if rate_diff > 0 else 'EUR vorteil' if rate_diff < 0 else 'neutral'}) diff --git a/rdagent/components/coder/factor_coder/fx_config.py b/rdagent/components/coder/factor_coder/fx_config.py new file mode 100644 index 00000000..af129936 --- /dev/null +++ b/rdagent/components/coder/factor_coder/fx_config.py @@ -0,0 +1,148 @@ +""" +FX Config - Zentrale Konfiguration für EURUSD Trading + +Wird verwendet von: +- Macro Agent (Live-Daten) +- Debate Team (Session-Analyse) +- Position Sizing (Spread, Costs) +- Web Dashboard (Zielwerte) +""" + +import os +from dataclasses import dataclass +from typing import Dict, Tuple + + +@dataclass +class FXConfig: + """Zentrale FX-Konfiguration.""" + + # Instrument & Daten + instrument: str = "EURUSD=X" + frequency: str = "1min" + data_path: str = os.path.expanduser("~/.qlib/qlib_data/eurusd_1min_data") + + # LLM Provider + llm_provider: str = "openai" + backend_url: str = os.getenv("OPENAI_API_BASE", "http://localhost:8081/v1") + api_key: str = os.getenv("OPENAI_API_KEY", "local") + chat_model: str = os.getenv("CHAT_MODEL", "qwen3.5-35b") + embedding_model: str = os.getenv("EMBEDDING_MODEL", "nomic-embed-text") + + # Trading-Parameter + spread_bps: float = 1.5 # 1.5 bps Spread + target_arr: float = 9.62 # Ziel: 9.62% annualisierte Rendite + max_drawdown: float = 20.0 # Max 20% Drawdown + cost_rate: float = 0.00015 # 0.015% pro Trade + + # Sessions (UTC) + sessions: Dict[str, Tuple[str, str]] = None + + # Debate & Risk + max_debate_rounds: int = 2 + max_risk_discuss_rounds: int = 1 + + # Memory & Reflection + memory_file: str = "git_ignore_folder/eurusd_trade_memory.json" + reflection_enabled: bool = True + + def __post_init__(self): + if self.sessions is None: + self.sessions = { + "asian": ("00:00", "08:00"), + "london": ("08:00", "16:00"), + "ny": ("13:00", "21:00"), + "overlap": ("13:00", "16:00"), + } + + def get_current_session(self) -> str: + """Bestimmt aktuelle FX-Session basierend auf UTC-Zeit.""" + from datetime import datetime, timezone + + hour_utc = datetime.now(timezone.utc).hour + + if 0 <= hour_utc < 8: + return "asian" + elif 8 <= hour_utc < 13: + return "london" + elif 13 <= hour_utc < 16: + return "overlap" + elif 16 <= hour_utc < 21: + return "ny" + else: + return "after_hours" + + def get_session_description(self, session: str = None) -> dict: + """Gibt Beschreibung der Session.""" + if session is None: + session = self.get_current_session() + + descriptions = { + "asian": { + "name": "Asian Session", + "hours": "00:00-08:00 UTC", + "characteristics": "Low volume, ranging market", + "recommended_strategy": "Mean Reversion", + "avoid": "Momentum strategies" + }, + "london": { + "name": "London Session", + "hours": "08:00-16:00 UTC", + "characteristics": "High volume, trending market", + "recommended_strategy": "Momentum/Trend-Following", + "avoid": "Counter-trend trades" + }, + "overlap": { + "name": "London-NY Overlap", + "hours": "13:00-16:00 UTC", + "characteristics": "Highest volume, strong directional moves", + "recommended_strategy": "Strong Momentum", + "avoid": "Range trading" + }, + "ny": { + "name": "NY Session", + "hours": "13:00-21:00 UTC", + "characteristics": "Moderate volume, reversals after London close", + "recommended_strategy": "Momentum/Reversal", + "avoid": "Late entries after 20:00" + }, + "after_hours": { + "name": "After Hours", + "hours": "21:00-00:00 UTC", + "characteristics": "Very low volume, wide spreads", + "recommended_strategy": "Avoid trading", + "avoid": "All strategies" + } + } + + return descriptions.get(session, descriptions["after_hours"]) + + +# Globale Instanz +fx_config = FXConfig() + + +def get_fx_config() -> FXConfig: + """Gibt globale FX-Config zurück.""" + return fx_config + + +# Test +if __name__ == "__main__": + config = get_fx_config() + + print("=== FX Config Test ===\n") + print(f"Instrument: {config.instrument}") + print(f"Frequency: {config.frequency}") + print(f"Target ARR: {config.target_arr}%") + print(f"Max Drawdown: {config.max_drawdown}%") + print(f"Spread: {config.spread_bps} bps") + + print(f"\nAktuelle Session: {config.get_current_session()}") + session_desc = config.get_session_description() + print(f" Name: {session_desc['name']}") + print(f" Hours: {session_desc['hours']}") + print(f" Characteristics: {session_desc['characteristics']}") + print(f" Recommended: {session_desc['recommended_strategy']}") + + print("\n✅ FX Config funktioniert!") diff --git a/web/dashboard.html b/web/dashboard.html new file mode 100644 index 00000000..d7c42b42 --- /dev/null +++ b/web/dashboard.html @@ -0,0 +1,381 @@ + + + + + + Predix Dashboard - COMPLETE Progress + + + +
+

🚀 Predix Dashboard

+

+ COMPLETE Progress Visualisierung für EURUSD Trading-Agent +

+ +
+
Lade Dashboard-Daten...
+
+ + +
+ + + + diff --git a/web/dashboard_api.py b/web/dashboard_api.py new file mode 100644 index 00000000..ce181aaa --- /dev/null +++ b/web/dashboard_api.py @@ -0,0 +1,317 @@ +""" +Predix Dashboard API + +Flask-Backend für das Web-Dashboard. +Zeigt COMPLETE Progress von EURUSD Trading-Agent. + +Features: +- Live Trading Progress (Loop, Step, Faktor) +- Performance Metrics (Win-Rate, PnL, Sharpe) +- Live Macro Daten (EURUSD, DXY, Volatility) +- Session Info +- Memory Statistics +- Debate Status +""" + +import json +import os +import sys +from datetime import datetime, timezone +from pathlib import Path + +from flask import Flask, jsonify +from flask_cors import CORS + +# Parent-Directory zum Path hinzufügen +sys.path.insert(0, str(Path(__file__).parent.parent)) + +app = Flask(__name__) +CORS(app) + +# Importiere unsere Module +try: + from rdagent.components.coder.factor_coder.fx_config import get_fx_config + from rdagent.components.coder.factor_coder.eurusd_macro import get_live_fx_data + from rdagent.components.coder.factor_coder.eurusd_debate import get_current_session_info + from rdagent.components.coder.factor_coder.eurusd_memory import EURUSDTradeMemory + + MODULES_AVAILABLE = True +except ImportError as e: + print(f"⚠️ Module nicht verfügbar: {e}") + MODULES_AVAILABLE = False + + +def parse_fin_quant_log(log_path: str, lines: int = 100) -> dict: + """ + Parst die letzten N Zeilen der fin_quant.log. + + Extrahiert: + - Aktueller Loop/Step + - Letzter Faktor + - Status (SUCCESS/FAILED/PENDING) + """ + result = { + "current_loop": "N/A", + "current_step": "N/A", + "progress_percent": 0, + "last_factor": "N/A", + "last_status": "N/A", + "recent_factors": [] + } + + try: + if not os.path.exists(log_path): + return result + + with open(log_path, 'r', encoding='utf-8', errors='ignore') as f: + # Hole letzte N Zeilen + all_lines = f.readlines() + recent_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines + log_content = ''.join(recent_lines) + + # Extrahiere Loop/Step + import re + + # Workflow Progress + progress_match = re.search(r'Workflow Progress:\s+(\d+)%.*loop_index=(\d+).*step_index=(\d+).*step_name=(\w+)', log_content) + if progress_match: + result["progress_percent"] = int(progress_match.group(1)) + result["current_loop"] = int(progress_match.group(2)) + result["current_step"] = f"{int(progress_match.group(3)) + 1}/4 ({progress_match.group(4)})" + + # Extrahiere Faktor-Namen + factor_matches = re.findall(r'factor_name:\s*(\w+)', log_content) + if factor_matches: + result["last_factor"] = factor_matches[-1] + result["recent_factors"] = list(reversed(factor_matches[-5:])) + + # Extrahiere Status + if "This implementation is SUCCESS" in log_content: + result["last_status"] = "SUCCESS" + elif "This implementation is FAIL" in log_content: + result["last_status"] = "FAILED" + elif "Execution succeeded" in log_content: + result["last_status"] = "RUNNING" + + # Log-Aktivität + result["log_lines_total"] = len(all_lines) + result["log_size_mb"] = round(os.path.getsize(log_path) / (1024 * 1024), 2) + + except Exception as e: + result["error"] = str(e) + + return result + + +def get_memory_stats(memory_file: str) -> dict: + """ + Holt Statistics aus dem Trade-Memory. + """ + result = { + "total_trades": 0, + "win_rate": 0.0, + "avg_return": 0.0, + "total_pnl": 0.0 + } + + try: + if not os.path.exists(memory_file): + return result + + memory = EURUSDTradeMemory(memory_file) + stats = memory.get_memory_stats() + + result["total_trades"] = stats.get("total_trades", 0) + result["win_rate"] = round(stats.get("win_rate", 0) * 100, 1) + result["avg_return"] = round(stats.get("avg_return", 0) * 100, 2) + result["total_pnl"] = round(stats.get("total_pnl", 0) * 100, 2) + result["sharpe_ratio"] = round(stats.get("sharpe_ratio", 0), 2) + + except Exception as e: + result["error"] = str(e) + + return result + + +@app.route('/api/health', methods=['GET']) +def health_check(): + """Health Check Endpoint.""" + return jsonify({ + "status": "ok", + "timestamp": datetime.now(timezone.utc).isoformat(), + "modules_available": MODULES_AVAILABLE + }) + + +@app.route('/api/progress', methods=['GET']) +def get_progress(): + """ + Holt aktuellen Trading-Progress. + + Returns: + - Aktueller Loop/Step + - Fortschritts-Prozent + - Letzter Faktor + - Status + """ + log_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'fin_quant.log') + progress = parse_fin_quant_log(log_path) + + return jsonify(progress) + + +@app.route('/api/macro', methods=['GET']) +def get_macro_data(): + """ + Holt Live-Macro-Daten. + + Returns: + - EURUSD Preis + - DXY (Dollar Index) + - Realized Volatility + - 24h Change + """ + if not MODULES_AVAILABLE: + return jsonify({"error": "Modules not available"}), 500 + + live_data = get_live_fx_data() + return jsonify(live_data) + + +@app.route('/api/session', methods=['GET']) +def get_session_data(): + """ + Holt aktuelle FX-Session Info. + + Returns: + - Session Name + - Hours + - Characteristics + - Recommended Strategy + """ + if not MODULES_AVAILABLE: + return jsonify({"error": "Modules not available"}), 500 + + session = get_current_session_info() + return jsonify(session) + + +@app.route('/api/memory', methods=['GET']) +def get_memory_data(): + """ + Holt Memory Statistics. + + Returns: + - Total Trades + - Win-Rate + - Average Return + - Sharpe Ratio + """ + if not MODULES_AVAILABLE: + return jsonify({"error": "Modules not available"}), 500 + + config = get_fx_config() + stats = get_memory_stats(config.memory_file) + + return jsonify(stats) + + +@app.route('/api/config', methods=['GET']) +def get_config_data(): + """ + Holt FX-Konfiguration. + + Returns: + - Instrument + - Target ARR + - Max Drawdown + - Spread + """ + if not MODULES_AVAILABLE: + return jsonify({"error": "Modules not available"}), 500 + + config = get_fx_config() + + return jsonify({ + "instrument": config.instrument, + "frequency": config.frequency, + "target_arr": config.target_arr, + "max_drawdown": config.max_drawdown, + "spread_bps": config.spread_bps, + "chat_model": config.chat_model + }) + + +@app.route('/api/dashboard', methods=['GET']) +def get_full_dashboard(): + """ + Holt alle Dashboard-Daten auf einmal. + + Kombiniert: + - Progress + - Macro + - Session + - Memory + - Config + """ + dashboard = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "modules_available": MODULES_AVAILABLE + } + + if MODULES_AVAILABLE: + # Progress + log_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'fin_quant.log') + dashboard["progress"] = parse_fin_quant_log(log_path) + + # Macro + dashboard["macro"] = get_live_fx_data() + + # Session + dashboard["session"] = get_current_session_info() + + # Memory + config = get_fx_config() + dashboard["memory"] = get_memory_stats(config.memory_file) + + # Config + dashboard["config"] = { + "instrument": config.instrument, + "target_arr": config.target_arr, + "max_drawdown": config.max_drawdown + } + else: + dashboard["error"] = "Modules not available" + + return jsonify(dashboard) + + +@app.route('/', methods=['GET']) +def index(): + """Root Endpoint - zeigt API-Info.""" + return jsonify({ + "name": "Predix Dashboard API", + "version": "1.0.0", + "description": "COMPLETE Progress Visualisierung für EURUSD Trading-Agent", + "endpoints": { + "/api/health": "Health Check", + "/api/progress": "Trading Progress", + "/api/macro": "Live Macro Daten", + "/api/session": "FX Session Info", + "/api/memory": "Memory Statistics", + "/api/config": "FX Konfiguration", + "/api/dashboard": "Alle Daten kombiniert" + } + }) + + +if __name__ == '__main__': + print("="*60) + print("Predix Dashboard API") + print("="*60) + print(f"Modules available: {MODULES_AVAILABLE}") + print(f"Starting server on http://localhost:5000") + print(f"API Docs: http://localhost:5000/") + print("="*60) + + app.run(host='0.0.0.0', port=5000, debug=True)