- Add fetch_total_deposits() with hourly local cache (deposit_cache.json) - Use deposit total as capital base for accurate total return calculation - Add --no-session-persistence to claude CLI subprocess calls - Show both total return (deposit-based) and change rate (cost-based) in reports - Update portfolio summary with Total Return line Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
121 lines
4.0 KiB
Python
121 lines
4.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}
|
||
|
||
## 目前市場指標摘要
|
||
{indicator_summary}
|
||
|
||
請根據以下策略分析每個幣種並給出交易建議:
|
||
|
||
## 交易策略
|
||
|
||
### 最高優先:多時間框架過濾(現貨,只做多)
|
||
- 1小時趨勢為多頭(EMA9 > EMA21)時,才考慮 BUY
|
||
- 1小時為空頭時,只持有或平倉(SELL),不開新倉
|
||
- 1小時 ADX < 20(盤整)時,避免進場
|
||
- 1小時 ADX > 25 且方向一致 = 高信心交易
|
||
|
||
### 進場訊號 (BUY) — 需至少2個確認
|
||
1. **超賣反彈**: StochRSI K < 0.2 且 K 上穿 D + RSI < 40
|
||
2. **均值回歸**: 價格觸及 BB 下軌 + RSI < 40 + CMF > 0(有買壓)
|
||
3. **趨勢啟動**: MACD 金叉 + EMA9 上穿 EMA21 + ADX > 20(有趨勢)
|
||
4. **支撐反彈**: 價格接近樞紐支撐位(S1/S2)+ OBV 流入 + RSI < 45
|
||
|
||
### 出場訊號 (SELL) — 需至少2個確認,不輕易賣出
|
||
⚠️ 現貨多單應讓利潤奔跑,除非有明確反轉訊號,否則傾向持有。
|
||
1. **強烈超買反轉**: StochRSI K > 0.8 且 K 下穿 D + RSI > 70 + MACD histogram 轉負
|
||
2. **阻力拒絕+量縮**: 價格接近樞紐阻力位(R1/R2)+ OBV 流出 + CMF < 0
|
||
3. **趨勢反轉確認**: MACD 死叉 + EMA9 下穿 EMA21 + 1h 趨勢也轉空
|
||
- 若 1h 趨勢仍為多頭,即使 5m 出現賣出訊號,也應降低信心或 HOLD
|
||
|
||
### 過濾條件(必須全部滿足)
|
||
- 成交量需高於 20 期平均(確認動能)
|
||
- 5分鐘 ADX > 15(排除極度盤整)
|
||
- OBV 方向需與交易方向一致(量價確認)
|
||
- ATR 過高時降低 suggested_amount_pct(波動風控)
|
||
|
||
### 信心分數指引
|
||
- 0.8+: 多時間框架對齊 + 3個以上確認指標
|
||
- 0.6-0.8: 多時間框架對齊 + 2個確認
|
||
- 0.4-0.6: 單一時間框架訊號,降低倉位
|
||
- < 0.4: 不建議交易,回傳 HOLD
|
||
|
||
請以 JSON 格式回傳,每個幣種一個物件:
|
||
```json
|
||
[
|
||
{{
|
||
"symbol": "tBTCUST",
|
||
"action": "BUY" | "SELL" | "HOLD",
|
||
"confidence": 0.0-1.0,
|
||
"reason": "簡短理由(含觸發的指標)",
|
||
"suggested_amount_pct": 0.05-0.20
|
||
}}
|
||
]
|
||
```
|
||
只回傳 JSON,不要其他文字。"""
|
||
|
||
try:
|
||
result = subprocess.run(
|
||
["claude", "-p", prompt, "--output-format", "json", "--no-session-persistence"],
|
||
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 []
|