Add Claude auto-fix to check_errors.py, distinguish LLM failure in Slack reports

- check_errors.py: on errors, call Claude CLI to diagnose and attempt auto-fix,
  include fix report in Slack alert
- slack_notifier.py: show "LLM 分析失敗" when LLM fails instead of "All HOLD"
- main.py: track llm_ok flag and pass to Slack reporter
- setup.sh: restore ~/.local/bin in crontab PATH for claude CLI
- llm_analyzer.py: use shutil.which for robust claude binary lookup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kroutony 2026-03-16 03:08:06 +00:00
parent a9981cb881
commit 36225df832
5 changed files with 109 additions and 7 deletions

View File

@ -1,7 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Check API errors in the last hour and send Slack alert if any found.""" """Check API errors in the last hour, attempt auto-fix via Claude, and report to Slack."""
import json
import os import os
import shutil
import subprocess
import sys import sys
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -9,6 +12,7 @@ sys.path.insert(0, os.path.dirname(__file__))
import slack_notifier import slack_notifier
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
LOG_FILES = [ LOG_FILES = [
"trading.log", "trading.log",
@ -25,14 +29,13 @@ def collect_recent_errors() -> list[str]:
errors = [] errors = []
for logfile in LOG_FILES: for logfile in LOG_FILES:
path = os.path.join(os.path.dirname(__file__), logfile) path = os.path.join(PROJECT_DIR, logfile)
if not os.path.exists(path): if not os.path.exists(path):
continue continue
with open(path) as f: with open(path) as f:
for line in f: for line in f:
if "[ERROR]" not in line: if "[ERROR]" not in line:
continue continue
# Only compare lines that start with a timestamp
if not line[:4].isdigit(): if not line[:4].isdigit():
continue continue
if line[:16] >= since_str: if line[:16] >= since_str:
@ -41,16 +44,90 @@ def collect_recent_errors() -> list[str]:
return errors return errors
def attempt_auto_fix(errors: list[str]) -> str | None:
"""Ask Claude to diagnose and fix the errors. Returns a summary or None on failure."""
error_text = "\n".join(errors[:20])
prompt = f"""你是 bifitnex-trading 專案的維運工程師。
專案目錄: {PROJECT_DIR}
以下是最近一小時的錯誤 log
```
{error_text}
```
1. 診斷錯誤的根因
2. 如果是程式碼問題直接修復編輯檔案
3. 如果是外部問題API 暫時不可用網路問題等不需要改程式碼只需說明
最後用以下 JSON 格式回覆
```json
{{
"diagnosis": "錯誤根因簡述",
"fixed": true/false,
"changes": "修改了什麼(沒改就寫 null",
"suggestion": "如需人工介入的建議(不需要就寫 null"
}}
```
只回傳 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", "--allowedTools", "Read,Edit,Glob,Grep"],
capture_output=True,
text=True,
timeout=180,
cwd=PROJECT_DIR,
)
if result.returncode != 0:
return None
return _parse_fix_response(result.stdout)
except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
return None
def _parse_fix_response(raw: str) -> dict | None:
"""Parse Claude's JSON response."""
try:
wrapper = json.loads(raw)
text = wrapper.get("result", raw) if isinstance(wrapper, dict) else raw
except json.JSONDecodeError:
text = raw
# Try to extract JSON from the text
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# Try finding JSON block in text
start = text.find("{")
end = text.rfind("}") + 1
if start >= 0 and end > start:
try:
return json.loads(text[start:end])
except json.JSONDecodeError:
pass
return None
def main(): def main():
errors = collect_recent_errors() errors = collect_recent_errors()
if not errors: if not errors:
return return
# Deduplicate and limit
unique = list(dict.fromkeys(errors)) unique = list(dict.fromkeys(errors))
display = unique[:20] display = unique[:20]
remaining = len(unique) - len(display) remaining = len(unique) - len(display)
# Try auto-fix
fix_result = attempt_auto_fix(unique)
# Build Slack message
lines = [ lines = [
"<!channel>", "<!channel>",
f"⚠️ *API 錯誤警報* (最近 {LOOKBACK_MINUTES} 分鐘,共 {len(unique)} 筆)", f"⚠️ *API 錯誤警報* (最近 {LOOKBACK_MINUTES} 分鐘,共 {len(unique)} 筆)",
@ -61,6 +138,20 @@ def main():
if remaining > 0: if remaining > 0:
lines.append(f"\n...另有 {remaining} 筆錯誤") lines.append(f"\n...另有 {remaining} 筆錯誤")
# Append fix report
lines.append("")
if fix_result:
lines.append("🔧 *自動修復報告:*")
lines.append(f" 診斷:{fix_result.get('diagnosis', '未知')}")
if fix_result.get("fixed"):
lines.append(f" ✅ 已修復:{fix_result.get('changes', '-')}")
else:
lines.append(" ❌ 未自動修復")
if fix_result.get("suggestion"):
lines.append(f" 💡 建議:{fix_result.get('suggestion')}")
else:
lines.append("🔧 *自動修復:* Claude 分析失敗,請人工檢查")
slack_notifier._send({"text": "\n".join(lines)}) slack_notifier._send({"text": "\n".join(lines)})

View File

@ -1,6 +1,8 @@
import json import json
import logging import logging
import os
import re import re
import shutil
import subprocess import subprocess
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -66,9 +68,11 @@ def analyze_market(indicator_summary: str, account_status: str) -> list[dict]:
``` ```
只回傳 JSON不要其他文字""" 只回傳 JSON不要其他文字"""
claude_bin = shutil.which("claude") or os.path.expanduser("~/.local/bin/claude")
try: try:
result = subprocess.run( result = subprocess.run(
["claude", "-p", prompt, "--output-format", "json", "--no-session-persistence"], [claude_bin, "-p", prompt, "--output-format", "json", "--no-session-persistence"],
capture_output=True, capture_output=True,
text=True, text=True,
timeout=120, timeout=120,

View File

@ -338,8 +338,10 @@ def run_cycle():
# 9. Call LLM for analysis # 9. Call LLM for analysis
signals = [] signals = []
llm_ok = False
try: try:
signals = llm_analyzer.analyze_market(indicator_summary, account_str) signals = llm_analyzer.analyze_market(indicator_summary, account_str)
llm_ok = True
logger.info("LLM returned %d signals", len(signals)) logger.info("LLM returned %d signals", len(signals))
except Exception as e: except Exception as e:
logger.error("LLM analysis failed: %s", e) logger.error("LLM analysis failed: %s", e)
@ -473,6 +475,7 @@ def run_cycle():
rejected=rejected, rejected=rejected,
tp_closed=tp_closed, tp_closed=tp_closed,
portfolio_summary=portfolio_summary, portfolio_summary=portfolio_summary,
llm_ok=llm_ok,
) )
# 12. Save final state # 12. Save final state

View File

@ -126,7 +126,7 @@ VENV_PYTHON="$PROJECT_DIR/.venv/bin/python"
CRON_MAIN="*/5 * * * * sleep 30 && cd $PROJECT_DIR && $VENV_PYTHON main.py >> cron.log 2>&1" CRON_MAIN="*/5 * * * * sleep 30 && cd $PROJECT_DIR && $VENV_PYTHON main.py >> cron.log 2>&1"
CRON_SYNC="2,32 * * * * cd $PROJECT_DIR && $VENV_PYTHON sync_cost_basis.py >> sync_cost_basis_cron.log 2>&1" CRON_SYNC="2,32 * * * * cd $PROJECT_DIR && $VENV_PYTHON sync_cost_basis.py >> sync_cost_basis_cron.log 2>&1"
CRON_CHECK="7 * * * * cd $PROJECT_DIR && $VENV_PYTHON check_errors.py 2>&1" CRON_CHECK="7 * * * * cd $PROJECT_DIR && $VENV_PYTHON check_errors.py 2>&1"
CRON_ENV="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" CRON_ENV="PATH=$HOME/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# 檢查是否已有此專案的 crontab # 檢查是否已有此專案的 crontab
if crontab -l 2>/dev/null | grep -q "bifitnex-trading"; then if crontab -l 2>/dev/null | grep -q "bifitnex-trading"; then

View File

@ -58,6 +58,7 @@ def send_cycle_report(
rejected: list[dict], rejected: list[dict],
tp_closed: list[dict], tp_closed: list[dict],
portfolio_summary: str, portfolio_summary: str,
llm_ok: bool = True,
): ):
"""Send a unified cycle report combining analysis, execution, and portfolio status.""" """Send a unified cycle report combining analysis, execution, and portfolio status."""
lines = [f"📊 *Trading Cycle Report #{cycle_number}*\n"] lines = [f"📊 *Trading Cycle Report #{cycle_number}*\n"]
@ -99,7 +100,10 @@ def send_cycle_report(
if holds: if holds:
lines.append(f" ⏸️ 其餘 {len(holds)} 幣種 HOLD") lines.append(f" ⏸️ 其餘 {len(holds)} 幣種 HOLD")
if not buys and not sells: if not buys and not sells:
lines.append(" All symbols → HOLD, no action this cycle.") if not llm_ok:
lines.append(" ⚠️ LLM 分析失敗,本次跳過。")
else:
lines.append(" All symbols → HOLD, no action this cycle.")
lines.append("") lines.append("")
# --- Executed trades --- # --- Executed trades ---