| |
| """ |
| AI Agent Core Module — Natural Language → Strategy Config |
| Supports OpenAI-compatible APIs and Gemini API. Falls back to template-based parsing when no API key is available. |
| """ |
|
|
| import json |
| import os |
| import re |
| from app.llm_client import chat_completion, get_usage_stats |
|
|
| |
| DEFAULT_STRATEGY_CONFIG = { |
| "ticker": "TSLA", |
| "interval": "1d", |
| "strategy_mode": "dynamic", |
| "stop_loss_pct": 0.015, |
| "profit_target_pct": 0.030, |
| "trailing_stop_mode": "atr", |
| "trailing_stop_atr_mult": 2.0, |
| "rsi_threshold_buy": 65.0, |
| "risk_per_trade_pct": 0.01, |
| "max_position_size_pct": 0.50, |
| "position_sizing_mode": "atr", |
| "commission_per_share": 0.005, |
| "slippage_rate": 0.0003 |
| } |
|
|
| |
| TICKER_ALIASES = { |
| "tesla": "TSLA", "tsla": "TSLA", |
| "nvidia": "NVDA", "nvda": "NVDA", |
| "apple": "AAPL", "aapl": "AAPL", |
| "microsoft": "MSFT", "msft": "MSFT", |
| "amd": "AMD", |
| "google": "GOOGL", "googl": "GOOGL", "alphabet": "GOOGL", |
| "amazon": "AMZN", "amzn": "AMZN", |
| "meta": "META", "facebook": "META", |
| "spy": "SPY", "qqq": "QQQ", |
| } |
|
|
| STRATEGY_ALIASES = { |
| "donchian": "dynamic", "breakout": "breakout", "突破": "breakout", |
| "ema": "ema_cross", "均线": "ema_cross", "金叉": "ema_cross", |
| "pattern": "patterns", "形态": "patterns", "k线": "patterns", |
| "consensus": "consensus", "共振": "consensus", |
| "dynamic": "dynamic", "动态": "dynamic", "自适应": "dynamic", |
| "mean reversion": "dynamic", "均值回归": "dynamic", |
| "trend": "dynamic", "趋势": "dynamic", |
| } |
|
|
| INTERVAL_ALIASES = { |
| "1分钟": "1m", "1min": "1m", "1minute": "1m", "1m": "1m", |
| "5分钟": "5m", "5min": "5m", "5minute": "5m", "5m": "5m", |
| "15分钟": "15m", "15min": "15m", "15m": "15m", |
| "30分钟": "30m", "30min": "30m", "30m": "30m", |
| "1小时": "1h", "1hour": "1h", "1h": "1h", "hourly": "1h", |
| "日线": "1d", "daily": "1d", "1d": "1d", "1day": "1d", |
| "day": "1d", "天": "1d", |
| } |
|
|
| |
| BACKEND_TOOLS = [ |
| { |
| "name": "fetch_market_data", |
| "description": "Fetch historical OHLCV market data for a given ticker and time range", |
| "parameters": {"ticker": "str", "period": "str", "interval": "str"} |
| }, |
| { |
| "name": "compute_indicators", |
| "description": "Compute technical indicators (EMA, RSI, ATR, MACD, ADX, Bollinger Bands, etc.) on market data", |
| "parameters": {"ticker": "str", "interval": "str"} |
| }, |
| { |
| "name": "run_backtest", |
| "description": "Run a backtest simulation with given strategy and risk parameters", |
| "parameters": {"strategy_config": "dict"} |
| }, |
| { |
| "name": "generate_risk_report", |
| "description": "Generate AI risk analysis report from backtest results", |
| "parameters": {"backtest_id": "str"} |
| } |
| ] |
|
|
| EXAMPLE_PROMPTS = [ |
| "Backtest TSLA with Donchian breakout strategy over the past year using daily bars", |
| "Test NVDA with EMA crossover strategy on 5-minute bars, use 1.5x ATR trailing stop", |
| "Run a mean reversion backtest on SPY with RSI oversold at 10 and Bollinger Bands", |
| "Compare dynamic routing vs consensus strategy on AAPL daily data", |
| "用日线级别回测 MSFT 的动态路由策略,ATR 止损倍数设为 2.5" |
| ] |
|
|
| SYSTEM_PROMPT = """You are the AI Strategy Parser engine for Quant.ai. |
| Your task is to analyze the user's trading strategy description in natural language and extract a precise, valid strategy configuration in JSON format. |
| |
| You must output a JSON object containing the following keys (you can leave out keys to use defaults, but you must ensure any provided keys have valid types and ranges): |
| - ticker (string, uppercase, e.g., "TSLA") |
| - interval (string, e.g., "1m", "5m", "15m", "30m", "1h", "1d") |
| - strategy_mode (string, choice of: "dynamic", "ema_cross", "breakout", "patterns", "consensus") |
| - stop_loss_pct (float, range 0.001 to 0.20, e.g., 0.015) |
| - profit_target_pct (float, range 0.001 to 0.50, e.g., 0.030) |
| - trailing_stop_mode (string, choice of: "atr", "fixed", "none") |
| - trailing_stop_atr_mult (float, range 0.5 to 5.0, e.g., 2.0) |
| - rsi_threshold_buy (float, range 10.0 to 90.0, e.g., 65.0) |
| - risk_per_trade_pct (float, range 0.001 to 0.10, e.g., 0.01) |
| - max_position_size_pct (float, range 0.05 to 1.00, e.g., 0.50) |
| - position_sizing_mode (string, choice of: "atr", "fixed") |
| - commission_per_share (float, range 0.0 to 0.10, e.g., 0.005) |
| - slippage_rate (float, range 0.0 to 0.01, e.g., 0.0003) |
| |
| If the user wants a SMA or EMA cross strategy, set strategy_mode to "ema_cross". |
| If the user wants candle patterns, breakout, consensus, etc., set strategy_mode to "patterns", "breakout", "consensus" or "dynamic". |
| If the user's description is vague, merge your extractions with the default config: |
| {default_config} |
| |
| CRITICAL: Return ONLY a valid JSON block enclosed in ```json ... ``` code blocks. Do not add conversational text around it when generating strategy_config. |
| """ |
|
|
| CHAT_SYSTEM_PROMPT = """You are Quant.ai's Quantitative Research Assistant. |
| You help users design, refine, and backtest quantitative trading strategies. |
| When the user describes a trading strategy or asks questions: |
| 1. Provide a professional, helpful, and natural language explanation/response. |
| 2. If the user's message contains a trading strategy or a change to an existing strategy, you MUST ALSO generate a JSON strategy configuration block inside ```json ... ```. |
| The JSON config must match the following format and merge with the current config: |
| {default_config} |
| |
| Valid fields: |
| - ticker (string, uppercase) |
| - interval ("1m", "5m", "15m", "30m", "1h", "1d") |
| - strategy_mode ("dynamic", "ema_cross", "breakout", "patterns", "consensus") |
| - stop_loss_pct (float, 0.001-0.20) |
| - profit_target_pct (float, 0.001-0.50) |
| - trailing_stop_mode ("atr", "fixed", "none") |
| - trailing_stop_atr_mult (float, 0.5-5.0) |
| - rsi_threshold_buy (float, 10-90) |
| - risk_per_trade_pct (float, 0.001-0.10) |
| - max_position_size_pct (float, 0.05-1.00) |
| - position_sizing_mode ("atr", "fixed") |
| - commission_per_share (float) |
| - slippage_rate (float) |
| |
| Response format: |
| Write your professional suggestions or chat response, followed by the ```json ... ``` block containing the extracted strategy_config (only if a strategy description is present). |
| """ |
|
|
| def parse_research_prompt(prompt: str, use_llm: bool = True) -> dict: |
| """ |
| Parse a natural language research prompt into a strategy_config JSON. |
| First tries LLM parsing if use_llm is True and keys exist, otherwise falls back to template. |
| """ |
| if use_llm: |
| openai_key = os.environ.get("OPENAI_API_KEY") |
| gemini_key = os.environ.get("GEMINI_API_KEY") |
| if openai_key or gemini_key: |
| res = parse_with_llm(prompt) |
| if res: |
| return res |
|
|
| |
| prompt_lower = prompt.lower().strip() |
| config = DEFAULT_STRATEGY_CONFIG.copy() |
| parsed_parts = [] |
| |
| |
| ticker_found = False |
| for alias, ticker in TICKER_ALIASES.items(): |
| if alias in prompt_lower: |
| config["ticker"] = ticker |
| parsed_parts.append(f"Ticker: {ticker}") |
| ticker_found = True |
| break |
| |
| if not ticker_found: |
| ticker_match = re.findall(r'\b([A-Z]{1,5})\b', prompt) |
| if ticker_match: |
| config["ticker"] = ticker_match[0] |
| parsed_parts.append(f"Ticker: {ticker_match[0]}") |
| |
| |
| for alias, mode in STRATEGY_ALIASES.items(): |
| if alias in prompt_lower: |
| config["strategy_mode"] = mode |
| parsed_parts.append(f"Strategy: {mode}") |
| break |
| |
| |
| for alias, interval in INTERVAL_ALIASES.items(): |
| if alias in prompt_lower: |
| config["interval"] = interval |
| parsed_parts.append(f"Interval: {interval}") |
| break |
| |
| |
| atr_match = re.search(r'atr.*?(\d+\.?\d*)', prompt_lower) |
| if atr_match: |
| atr_val = float(atr_match.group(1)) |
| if 0.5 <= atr_val <= 5.0: |
| config["trailing_stop_atr_mult"] = atr_val |
| parsed_parts.append(f"ATR Multiplier: {atr_val}") |
| |
| |
| rsi_match = re.search(r'rsi.*?(\d+)', prompt_lower) |
| if rsi_match: |
| rsi_val = float(rsi_match.group(1)) |
| if 5 <= rsi_val <= 90: |
| config["rsi_threshold_buy"] = rsi_val |
| parsed_parts.append(f"RSI Threshold: {rsi_val}") |
| |
| |
| risk_match = re.search(r'risk.*?(\d+\.?\d*)%', prompt_lower) |
| if risk_match: |
| risk_val = float(risk_match.group(1)) / 100 |
| if 0.001 <= risk_val <= 0.05: |
| config["risk_per_trade_pct"] = risk_val |
| parsed_parts.append(f"Risk Per Trade: {risk_val*100}%") |
| |
| execution_plan = [ |
| {"step": 1, "tool": "fetch_market_data", "desc": f"Fetch {config['interval']} OHLCV data for {config['ticker']}"}, |
| {"step": 2, "tool": "compute_indicators", "desc": "Calculate technical indicators (EMA, RSI, ATR, MACD, ADX, Bollinger Bands, Donchian)"}, |
| {"step": 3, "tool": "run_backtest", "desc": f"Run {config['strategy_mode']} strategy backtest with ATR={config['trailing_stop_atr_mult']}x trailing stop"}, |
| {"step": 4, "tool": "generate_risk_report", "desc": "Analyze results: drawdown, regime performance, parameter sensitivity, overfitting risk"} |
| ] |
| |
| parsed_intent = f"Backtest {config['ticker']} using {config['strategy_mode']} strategy on {config['interval']} bars" |
| if parsed_parts: |
| parsed_intent += f" (Parsed: {', '.join(parsed_parts)})" |
| |
| return { |
| "strategy_config": config, |
| "execution_plan": execution_plan, |
| "parsed_intent": parsed_intent, |
| "source": "template", |
| "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, "cost_usd": 0.0} |
| } |
|
|
|
|
| def parse_with_llm(prompt: str, history: list = None) -> dict: |
| """ |
| Call LLM to parse a natural language prompt into a strategy configuration. |
| """ |
| sys_prompt = SYSTEM_PROMPT.format(default_config=json.dumps(DEFAULT_STRATEGY_CONFIG, indent=2)) |
| messages = [ |
| {"role": "system", "content": sys_prompt} |
| ] |
| if history: |
| for h in history: |
| messages.append(h) |
| messages.append({"role": "user", "content": prompt}) |
| |
| res = chat_completion(messages) |
| if not res or not res.get("available"): |
| return None |
| |
| content = res["content"] |
| |
| try: |
| config_data = json.loads(content) |
| except Exception: |
| match = re.search(r'\{.*\}', content, re.DOTALL) |
| if match: |
| try: |
| config_data = json.loads(match.group(0)) |
| except Exception: |
| return None |
| else: |
| return None |
| |
| final_config = DEFAULT_STRATEGY_CONFIG.copy() |
| |
| for k, v in config_data.items(): |
| if k in DEFAULT_STRATEGY_CONFIG: |
| try: |
| if k == "ticker": |
| final_config[k] = str(v).upper() |
| elif k == "interval" and str(v) in ["1m", "5m", "15m", "30m", "1h", "1d"]: |
| final_config[k] = str(v) |
| elif k == "strategy_mode" and str(v) in ["dynamic", "ema_cross", "breakout", "patterns", "consensus"]: |
| final_config[k] = str(v) |
| elif k == "stop_loss_pct": |
| final_config[k] = max(0.001, min(0.20, float(v))) |
| elif k == "profit_target_pct": |
| final_config[k] = max(0.001, min(0.50, float(v))) |
| elif k == "trailing_stop_mode" and str(v) in ["atr", "fixed", "none"]: |
| final_config[k] = str(v) |
| elif k == "trailing_stop_atr_mult": |
| final_config[k] = max(0.5, min(5.0, float(v))) |
| elif k == "rsi_threshold_buy": |
| final_config[k] = max(10.0, min(90.0, float(v))) |
| elif k == "risk_per_trade_pct": |
| final_config[k] = max(0.001, min(0.10, float(v))) |
| elif k == "max_position_size_pct": |
| final_config[k] = max(0.05, min(1.00, float(v))) |
| elif k == "position_sizing_mode" and str(v) in ["atr", "fixed"]: |
| final_config[k] = str(v) |
| elif k == "commission_per_share": |
| final_config[k] = max(0.0, min(0.10, float(v))) |
| elif k == "slippage_rate": |
| final_config[k] = max(0.0, min(0.01, float(v))) |
| except Exception: |
| pass |
| |
| execution_plan = [ |
| {"step": 1, "tool": "fetch_market_data", "desc": f"Fetch {final_config['interval']} OHLCV data for {final_config['ticker']}"}, |
| {"step": 2, "tool": "compute_indicators", "desc": "Calculate technical indicators (EMA, RSI, ATR, MACD, ADX, Bollinger Bands, Donchian)"}, |
| {"step": 3, "tool": "run_backtest", "desc": f"Run {final_config['strategy_mode']} strategy backtest with ATR={final_config['trailing_stop_atr_mult']}x trailing stop"}, |
| {"step": 4, "tool": "generate_risk_report", "desc": "Analyze results: drawdown, regime performance, parameter sensitivity, overfitting risk"} |
| ] |
| |
| parsed_intent = f"Backtest {final_config['ticker']} using {final_config['strategy_mode']} strategy on {final_config['interval']} bars" |
| |
| return { |
| "strategy_config": final_config, |
| "execution_plan": execution_plan, |
| "parsed_intent": parsed_intent, |
| "source": "llm", |
| "usage": res["usage"] |
| } |
|
|
|
|
| def get_chat_response(prompt: str, history: list = None) -> dict: |
| """ |
| Generate chatbot strategy recommendations and natural language explanations. |
| """ |
| sys_prompt = CHAT_SYSTEM_PROMPT.format(default_config=json.dumps(DEFAULT_STRATEGY_CONFIG, indent=2)) |
| messages = [ |
| {"role": "system", "content": sys_prompt} |
| ] |
| if history: |
| for h in history: |
| messages.append(h) |
| messages.append({"role": "user", "content": prompt}) |
| |
| res = chat_completion(messages) |
| |
| |
| if not res or not res.get("available"): |
| parsed = parse_research_prompt(prompt, use_llm=False) |
| msg = f"你好!我是 Quant.ai 策略研究助手。由于目前未检测到可用的大模型 API 密钥(OPENAI_API_KEY 或 GEMINI_API_KEY),我已使用系统规则模板将您的策略解析为:{parsed['parsed_intent']}。您可以在控制面板中查看详细参数并点击执行回测。" |
| return { |
| "message": msg, |
| "strategy_config": parsed["strategy_config"], |
| "execution_plan": parsed["execution_plan"], |
| "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, "cost_usd": 0.0} |
| } |
| |
| content = res["content"] |
| strategy_config = None |
| clean_msg = content |
| |
| |
| match = re.search(r'```json\s*(\{.*?\})\s*```', content, re.DOTALL) |
| if not match: |
| match = re.search(r'(\{.*?\})', content, re.DOTALL) |
| |
| if match: |
| try: |
| config_data = json.loads(match.group(1)) |
| strategy_config = DEFAULT_STRATEGY_CONFIG.copy() |
| for k, v in config_data.items(): |
| if k in DEFAULT_STRATEGY_CONFIG: |
| try: |
| if k == "ticker": |
| strategy_config[k] = str(v).upper() |
| elif k == "interval" and str(v) in ["1m", "5m", "15m", "30m", "1h", "1d"]: |
| strategy_config[k] = str(v) |
| elif k == "strategy_mode" and str(v) in ["dynamic", "ema_cross", "breakout", "patterns", "consensus"]: |
| strategy_config[k] = str(v) |
| elif k == "stop_loss_pct": |
| strategy_config[k] = max(0.001, min(0.20, float(v))) |
| elif k == "profit_target_pct": |
| strategy_config[k] = max(0.001, min(0.50, float(v))) |
| elif k == "trailing_stop_mode" and str(v) in ["atr", "fixed", "none"]: |
| strategy_config[k] = str(v) |
| elif k == "trailing_stop_atr_mult": |
| strategy_config[k] = max(0.5, min(5.0, float(v))) |
| elif k == "rsi_threshold_buy": |
| strategy_config[k] = max(10.0, min(90.0, float(v))) |
| elif k == "risk_per_trade_pct": |
| strategy_config[k] = max(0.001, min(0.10, float(v))) |
| elif k == "max_position_size_pct": |
| strategy_config[k] = max(0.05, min(1.00, float(v))) |
| elif k == "position_sizing_mode" and str(v) in ["atr", "fixed"]: |
| strategy_config[k] = str(v) |
| elif k == "commission_per_share": |
| strategy_config[k] = max(0.0, min(0.10, float(v))) |
| elif k == "slippage_rate": |
| strategy_config[k] = max(0.0, min(0.01, float(v))) |
| except Exception: |
| pass |
| |
| clean_msg = content.replace(match.group(0), "").strip() |
| if not clean_msg: |
| clean_msg = f"我已为您生成了策略配置:{strategy_config['ticker']} ({strategy_config['strategy_mode']})。" |
| except Exception: |
| pass |
| |
| if not strategy_config: |
| strategy_config = DEFAULT_STRATEGY_CONFIG.copy() |
| |
| execution_plan = [ |
| {"step": 1, "tool": "fetch_market_data", "desc": f"Fetch {strategy_config['interval']} OHLCV data for {strategy_config['ticker']}"}, |
| {"step": 2, "tool": "compute_indicators", "desc": "Calculate technical indicators (EMA, RSI, ATR, MACD, ADX, Bollinger Bands, Donchian)"}, |
| {"step": 3, "tool": "run_backtest", "desc": f"Run {strategy_config['strategy_mode']} strategy backtest with ATR={strategy_config['trailing_stop_atr_mult']}x trailing stop"}, |
| {"step": 4, "tool": "generate_risk_report", "desc": "Analyze results: drawdown, regime performance, parameter sensitivity, overfitting risk"} |
| ] |
| |
| return { |
| "message": clean_msg, |
| "strategy_config": strategy_config, |
| "execution_plan": execution_plan, |
| "usage": res["usage"] |
| } |
|
|
|
|
| def get_example_prompts() -> list: |
| """Return example research prompts for the frontend""" |
| return EXAMPLE_PROMPTS |
|
|
|
|
| def get_backend_tools() -> list: |
| """Return available backend tools for documentation""" |
| return BACKEND_TOOLS |
|
|