bifitnex-trading/llm_analyzer.py
kroutony 972d66ab1b Initial commit: LLM-driven crypto trading bot
Includes: Bitfinex API integration, technical indicators,
LLM signal generation, risk management, Slack notifications.

Recent fixes:
- SELL orders use position value instead of total balance
- SELL signals always close full position
- Failed orders added to rejected list for Slack reporting
- Position/exposure limits auto-cap to remaining room
- BUY order minimum raised to 10% of portfolio

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 03:25:18 +00:00

102 lines
3.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
import logging
import re
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}
## 目前市場指標摘要5分鐘K線最新值
{indicator_summary}
請根據以下策略分析每個幣種並給出交易建議:
## 交易策略
1. **趨勢確認**: EMA(9) > EMA(21) 為多頭,反之為空頭
2. **進場訊號 (BUY)**:
- RSI < 35 且 MACD histogram 由負轉正(超賣反彈)
- 價格觸及 Bollinger 下軌且 RSI < 40均值回歸
- MACD 金叉 + EMA(9) 上穿 EMA(21)(趨勢啟動)
3. **進場訊號 (SELL)**:
- RSI > 70 且 MACD histogram 由正轉負(超買回落)
- 價格觸及 Bollinger 上軌且 RSI > 65
- MACD 死叉 + EMA(9) 下穿 EMA(21)
4. **過濾條件**:
- 成交量需高於 20 期平均(確認動能)
- ATR 過高時降低倉位(波動風控)
請以 JSON 格式回傳,每個幣種一個物件:
```json
[
{{
"symbol": "tBTCUST",
"action": "BUY" | "SELL" | "HOLD",
"confidence": 0.0-1.0,
"reason": "簡短理由",
"suggested_amount_pct": 0.10-0.20
}}
]
```
只回傳 JSON不要其他文字。"""
try:
result = subprocess.run(
["claude", "-p", prompt, "--output-format", "json"],
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 []