import json import logging import os import re import shutil import subprocess logger = logging.getLogger(__name__) def analyze_market(indicator_summary: str, account_status: str) -> list[dict]: """Call Claude CLI to analyze market data and return trade signals.""" prompt = f"""你是一位專業的加密貨幣短線交易分析師。 ## 帳戶狀態 {account_status} ## 目前市場指標摘要 {indicator_summary} 請根據以下策略分析每個幣種並給出交易建議: ## 交易策略 ### 最高優先:多時間框架過濾(現貨,只做多) - 1小時趨勢為多頭(EMA9 > EMA21)時,才考慮 BUY - 1小時為空頭時,只持有或平倉(SELL),不開新倉 - 1小時 ADX < 20(盤整)時,降低信心但不禁止進場 - 1小時 ADX > 25 且方向一致 = 高信心交易 ### 進場訊號 (BUY) — 需至少2個確認 1. **超賣反彈**: StochRSI K < 0.2 且 K 上穿 D + RSI < 40 2. **均值回歸**: 價格觸及 BB 下軌 + RSI < 40 + CMF > 0(有買壓) 3. **趨勢啟動**: MACD 金叉 + EMA9 上穿 EMA21 + ADX > 20(有趨勢) 4. **支撐反彈**: 價格接近樞紐支撐位(S1/S2)+ OBV 流入 + RSI < 45 ### 出場訊號 (SELL) — 需至少2個確認,不輕易賣出 ⚠️ 現貨多單應讓利潤奔跑,除非有明確反轉訊號,否則傾向持有。 1. **強烈超買反轉**: StochRSI K > 0.8 且 K 下穿 D + RSI > 70 + MACD histogram 轉負 2. **阻力拒絕+量縮**: 價格接近樞紐阻力位(R1/R2)+ OBV 流出 + CMF < 0 3. **趨勢反轉確認**: MACD 死叉 + EMA9 下穿 EMA21 + 1h 趨勢也轉空 - 若 1h 趨勢仍為多頭,即使 5m 出現賣出訊號,也應降低信心或 HOLD ### 輔助條件(加減分,非硬性過濾) - 成交量高於 MA20 → 信心 +0.1;遠低於 MA20 → 信心 -0.1 - 5分鐘 ADX > 15 → 正常;ADX < 15 → 信心 -0.1(但不禁止進場) - OBV 方向與交易一致 → 信心 +0.1;相反 → 信心 -0.1 - ATR 過高時降低 suggested_amount_pct(波動風控) - 這些條件用於調整信心分數,不應單獨阻擋進場 ### 信心分數指引 - 0.8+: 多時間框架對齊 + 3個以上確認指標 - 0.6-0.8: 多時間框架對齊 + 2個確認 - 0.4-0.6: 單一時間框架訊號,降低倉位 - < 0.4: 不建議交易,回傳 HOLD 請以 JSON 格式回傳,每個幣種一個物件: ```json [ {{ "symbol": "tBTCUST", "action": "BUY" | "SELL" | "HOLD", "confidence": 0.0-1.0, "reason": "簡短理由(含觸發的指標)", "suggested_amount_pct": 0.05-0.20 }} ] ``` 只回傳 JSON,不要其他文字。""" claude_bin = shutil.which("claude") or os.path.expanduser("~/.local/bin/claude") try: result = subprocess.run( [claude_bin, "-p", prompt, "--output-format", "json", "--no-session-persistence"], capture_output=True, text=True, timeout=120, ) if result.returncode != 0: logger.error("Claude CLI failed (rc=%d): %s", result.returncode, result.stderr) return [] return _parse_llm_response(result.stdout) except subprocess.TimeoutExpired: logger.error("Claude CLI timed out") return [] except FileNotFoundError: logger.error("Claude CLI not found — is 'claude' installed and in PATH?") return [] except Exception as e: logger.error("LLM analysis error: %s", e) return [] def _parse_llm_response(raw: str) -> list[dict]: """Extract the JSON array from Claude's response.""" # First try: the output-format json wraps response in {"result": "..."} try: wrapper = json.loads(raw) if isinstance(wrapper, dict) and "result" in wrapper: raw = wrapper["result"] except (json.JSONDecodeError, TypeError): pass # Try direct parse if isinstance(raw, list): return raw try: parsed = json.loads(raw) if isinstance(parsed, list): return parsed except (json.JSONDecodeError, TypeError): pass # Try extracting JSON array from markdown code block or mixed text match = re.search(r'\[[\s\S]*\]', raw) if match: try: return json.loads(match.group()) except json.JSONDecodeError: pass logger.warning("Could not parse LLM response as JSON array") return []