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>
102 lines
3.0 KiB
Python
102 lines
3.0 KiB
Python
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 []
|