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
|
#!/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)})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
3
main.py
3
main.py
@ -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
|
||||||
|
|||||||
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_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
|
||||||
|
|||||||
@ -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 ---
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user