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:
parent
a9981cb881
commit
36225df832
@ -1,7 +1,10 @@
|
||||
#!/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 shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@ -9,6 +12,7 @@ sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
import slack_notifier
|
||||
|
||||
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
LOG_FILES = [
|
||||
"trading.log",
|
||||
@ -25,14 +29,13 @@ def collect_recent_errors() -> list[str]:
|
||||
errors = []
|
||||
|
||||
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):
|
||||
continue
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
if "[ERROR]" not in line:
|
||||
continue
|
||||
# Only compare lines that start with a timestamp
|
||||
if not line[:4].isdigit():
|
||||
continue
|
||||
if line[:16] >= since_str:
|
||||
@ -41,16 +44,90 @@ def collect_recent_errors() -> list[str]:
|
||||
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():
|
||||
errors = collect_recent_errors()
|
||||
if not errors:
|
||||
return
|
||||
|
||||
# Deduplicate and limit
|
||||
unique = list(dict.fromkeys(errors))
|
||||
display = unique[:20]
|
||||
remaining = len(unique) - len(display)
|
||||
|
||||
# Try auto-fix
|
||||
fix_result = attempt_auto_fix(unique)
|
||||
|
||||
# Build Slack message
|
||||
lines = [
|
||||
"<!channel>",
|
||||
f"⚠️ *API 錯誤警報* (最近 {LOOKBACK_MINUTES} 分鐘,共 {len(unique)} 筆)",
|
||||
@ -61,6 +138,20 @@ def main():
|
||||
if remaining > 0:
|
||||
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)})
|
||||
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -66,9 +68,11 @@ def analyze_market(indicator_summary: str, account_status: str) -> list[dict]:
|
||||
```
|
||||
只回傳 JSON,不要其他文字。"""
|
||||
|
||||
claude_bin = shutil.which("claude") or os.path.expanduser("~/.local/bin/claude")
|
||||
|
||||
try:
|
||||
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,
|
||||
text=True,
|
||||
timeout=120,
|
||||
|
||||
3
main.py
3
main.py
@ -338,8 +338,10 @@ def run_cycle():
|
||||
|
||||
# 9. Call LLM for analysis
|
||||
signals = []
|
||||
llm_ok = False
|
||||
try:
|
||||
signals = llm_analyzer.analyze_market(indicator_summary, account_str)
|
||||
llm_ok = True
|
||||
logger.info("LLM returned %d signals", len(signals))
|
||||
except Exception as e:
|
||||
logger.error("LLM analysis failed: %s", e)
|
||||
@ -473,6 +475,7 @@ def run_cycle():
|
||||
rejected=rejected,
|
||||
tp_closed=tp_closed,
|
||||
portfolio_summary=portfolio_summary,
|
||||
llm_ok=llm_ok,
|
||||
)
|
||||
|
||||
# 12. Save final state
|
||||
|
||||
2
setup.sh
2
setup.sh
@ -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_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_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
|
||||
if crontab -l 2>/dev/null | grep -q "bifitnex-trading"; then
|
||||
|
||||
@ -58,6 +58,7 @@ def send_cycle_report(
|
||||
rejected: list[dict],
|
||||
tp_closed: list[dict],
|
||||
portfolio_summary: str,
|
||||
llm_ok: bool = True,
|
||||
):
|
||||
"""Send a unified cycle report combining analysis, execution, and portfolio status."""
|
||||
lines = [f"📊 *Trading Cycle Report #{cycle_number}*\n"]
|
||||
@ -99,6 +100,9 @@ def send_cycle_report(
|
||||
if holds:
|
||||
lines.append(f" ⏸️ 其餘 {len(holds)} 幣種 HOLD")
|
||||
if not buys and not sells:
|
||||
if not llm_ok:
|
||||
lines.append(" ⚠️ LLM 分析失敗,本次跳過。")
|
||||
else:
|
||||
lines.append(" All symbols → HOLD, no action this cycle.")
|
||||
lines.append("")
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user