Add advanced indicators: MTF analysis, ADX, StochRSI, OBV, CMF, pivot points
- config.py: Add HTF_TIMEFRAME (1h) and HTF_CANDLE_LIMIT (50) - data_fetcher.py: Fetch both 5m and 1h candles per symbol - indicators.py: Add ADX, StochRSI, OBV+slope, CMF to 5m indicators; new functions for HTF indicators, pivot points, and their summaries - main.py: Wire up HTF data flow (1h indicators + pivots → LLM summary) - llm_analyzer.py: Rewrite prompt with MTF filter (1h trend alignment), require 2+ confirmations for BUY, confidence scoring guide Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2e45db006f
commit
1abfdefecd
@ -32,6 +32,8 @@ SYMBOL_NAMES = {
|
||||
# Candle settings
|
||||
CANDLE_TIMEFRAME = "5m"
|
||||
CANDLE_LIMIT = 100
|
||||
HTF_TIMEFRAME = "1h"
|
||||
HTF_CANDLE_LIMIT = 50
|
||||
|
||||
# Trading parameters
|
||||
MIN_ORDER_USDT = 5
|
||||
|
||||
@ -169,17 +169,20 @@ def fetch_active_orders() -> list[dict]:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def fetch_all_market_data(symbols: list[str] | None = None) -> dict:
|
||||
"""Fetch tickers + candles for all symbols. Returns dict keyed by symbol."""
|
||||
"""Fetch tickers + candles (5m and 1h) for all symbols. Returns dict keyed by symbol."""
|
||||
symbols = symbols or config.TOP_15_SYMBOLS
|
||||
tickers = fetch_tickers(symbols)
|
||||
data = {}
|
||||
for sym in symbols:
|
||||
try:
|
||||
candles = fetch_candles(sym)
|
||||
candles_htf = fetch_candles(sym, timeframe=config.HTF_TIMEFRAME, limit=config.HTF_CANDLE_LIMIT)
|
||||
data[sym] = {
|
||||
"ticker": tickers.get(sym, {}),
|
||||
"candles": candles,
|
||||
"candles_htf": candles_htf,
|
||||
}
|
||||
time.sleep(0.15) # Avoid Bitfinex rate limits
|
||||
except Exception as e:
|
||||
logger.warning("Failed to fetch data for %s: %s", sym, e)
|
||||
return data
|
||||
|
||||
121
indicators.py
121
indicators.py
@ -15,7 +15,7 @@ def calculate_indicators(candles_df: pd.DataFrame) -> pd.DataFrame:
|
||||
Returns the DataFrame with additional indicator columns.
|
||||
"""
|
||||
df = candles_df.copy()
|
||||
if df.empty or len(df) < 26:
|
||||
if df.empty or len(df) < 30:
|
||||
logger.warning("Not enough candle data to compute indicators (got %d rows)", len(df))
|
||||
return df
|
||||
|
||||
@ -54,9 +54,73 @@ def calculate_indicators(candles_df: pd.DataFrame) -> pd.DataFrame:
|
||||
# Volume moving average (20)
|
||||
df["vol_ma20"] = volume.rolling(window=20).mean()
|
||||
|
||||
# ADX(14) — trend strength
|
||||
df["adx"] = ta.trend.adx(high, low, close, window=14)
|
||||
|
||||
# Stochastic RSI(14, 14, 3, 3)
|
||||
df["stoch_rsi_k"] = ta.momentum.stochrsi_k(close, window=14, smooth1=3, smooth2=3)
|
||||
df["stoch_rsi_d"] = ta.momentum.stochrsi_d(close, window=14, smooth1=3, smooth2=3)
|
||||
|
||||
# OBV (On-Balance Volume) + slope
|
||||
df["obv"] = ta.volume.on_balance_volume(close, volume)
|
||||
df["obv_slope"] = df["obv"].diff(5)
|
||||
|
||||
# CMF (Chaikin Money Flow, 20-period)
|
||||
df["cmf"] = ta.volume.chaikin_money_flow(high, low, close, volume, window=20)
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def calculate_htf_indicators(candles_df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Calculate trend-context indicators on higher timeframe (1h) candles."""
|
||||
df = candles_df.copy()
|
||||
if df.empty or len(df) < 30:
|
||||
return df
|
||||
|
||||
close = df["close"]
|
||||
high = df["high"]
|
||||
low = df["low"]
|
||||
|
||||
df["ema9"] = ta.trend.ema_indicator(close, window=9)
|
||||
df["ema21"] = ta.trend.ema_indicator(close, window=21)
|
||||
df["adx"] = ta.trend.adx(high, low, close, window=14)
|
||||
df["rsi"] = ta.momentum.rsi(close, window=14)
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def calculate_pivot_points(htf_df: pd.DataFrame) -> dict | None:
|
||||
"""Calculate classic pivot points from previous day's price action using 1h candles."""
|
||||
if htf_df.empty or len(htf_df) < 24:
|
||||
return None
|
||||
|
||||
df = htf_df.copy()
|
||||
df["date"] = df["timestamp"].dt.date
|
||||
|
||||
dates = sorted(df["date"].unique())
|
||||
if len(dates) < 2:
|
||||
return None
|
||||
|
||||
prev_day = df[df["date"] == dates[-2]]
|
||||
if prev_day.empty:
|
||||
return None
|
||||
|
||||
h = prev_day["high"].max()
|
||||
l = prev_day["low"].min()
|
||||
c = prev_day["close"].iloc[-1]
|
||||
|
||||
pivot = (h + l + c) / 3
|
||||
return {
|
||||
"pivot": pivot,
|
||||
"r1": 2 * pivot - l,
|
||||
"r2": pivot + (h - l),
|
||||
"r3": h + 2 * (pivot - l),
|
||||
"s1": 2 * pivot - h,
|
||||
"s2": pivot - (h - l),
|
||||
"s3": l - 2 * (h - pivot),
|
||||
}
|
||||
|
||||
|
||||
def summarize_indicators(df: pd.DataFrame, symbol: str) -> str:
|
||||
"""Produce a concise text summary of the latest indicator values for LLM consumption."""
|
||||
if df.empty or len(df) < 2:
|
||||
@ -89,6 +153,61 @@ def summarize_indicators(df: pd.DataFrame, symbol: str) -> str:
|
||||
lines.append(f"ATR(14): {last['atr']:.6g}")
|
||||
if pd.notna(last.get("vwap")):
|
||||
lines.append(f"VWAP: {last['vwap']:.6g}")
|
||||
if pd.notna(last.get("adx")):
|
||||
adx_label = "趨勢" if last["adx"] > 25 else "盤整" if last["adx"] < 20 else "弱"
|
||||
lines.append(f"ADX(14): {last['adx']:.1f} ({adx_label})")
|
||||
if pd.notna(last.get("stoch_rsi_k")):
|
||||
lines.append(f"StochRSI: K={last['stoch_rsi_k']:.2f} D={last['stoch_rsi_d']:.2f}")
|
||||
if pd.notna(last.get("obv_slope")):
|
||||
obv_dir = "流入" if last["obv_slope"] > 0 else "流出"
|
||||
lines.append(f"OBV趨勢: {obv_dir} (slope={last['obv_slope']:.0f})")
|
||||
if pd.notna(last.get("cmf")):
|
||||
cmf_label = "買壓" if last["cmf"] > 0.05 else "賣壓" if last["cmf"] < -0.05 else "中性"
|
||||
lines.append(f"CMF(20): {last['cmf']:.3f} ({cmf_label})")
|
||||
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def summarize_htf(htf_by_symbol: dict[str, pd.DataFrame]) -> str:
|
||||
"""Produce a compact 1h trend context summary for LLM."""
|
||||
lines = ["## 1小時趨勢背景"]
|
||||
for sym in sorted(htf_by_symbol):
|
||||
df = htf_by_symbol[sym]
|
||||
if df.empty or len(df) < 2:
|
||||
continue
|
||||
last = df.iloc[-1]
|
||||
name = config.SYMBOL_NAMES.get(sym, sym)
|
||||
|
||||
trend = "多頭" if last.get("ema9", 0) > last.get("ema21", 0) else "空頭"
|
||||
adx_val = last.get("adx", 0)
|
||||
strength = "強趨勢" if adx_val > 25 else "盤整" if adx_val < 20 else "弱趨勢"
|
||||
rsi_1h = last.get("rsi", 50)
|
||||
|
||||
lines.append(f"- {name}: {trend} ({strength}, ADX={adx_val:.0f}, RSI={rsi_1h:.0f})")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def summarize_pivots(pivots_by_symbol: dict[str, dict], current_prices: dict) -> str:
|
||||
"""Summarize pivot point levels relative to current price."""
|
||||
lines = ["## 關鍵支撐/阻力 (日線樞紐)"]
|
||||
for sym in sorted(pivots_by_symbol):
|
||||
p = pivots_by_symbol[sym]
|
||||
if not p:
|
||||
continue
|
||||
name = config.SYMBOL_NAMES.get(sym, sym)
|
||||
price = current_prices.get(sym, 0)
|
||||
if price <= 0:
|
||||
continue
|
||||
|
||||
supports = [("S3", p["s3"]), ("S2", p["s2"]), ("S1", p["s1"]), ("P", p["pivot"])]
|
||||
resistances = [("P", p["pivot"]), ("R1", p["r1"]), ("R2", p["r2"]), ("R3", p["r3"])]
|
||||
|
||||
nearest_sup = max([(n, v) for n, v in supports if v < price], key=lambda x: x[1], default=None)
|
||||
nearest_res = min([(n, v) for n, v in resistances if v > price], key=lambda x: x[1], default=None)
|
||||
|
||||
sup_str = f"{nearest_sup[0]}={nearest_sup[1]:.6g}" if nearest_sup else "N/A"
|
||||
res_str = f"{nearest_res[0]}={nearest_res[1]:.6g}" if nearest_res else "N/A"
|
||||
lines.append(f"- {name}: 支撐 {sup_str} | 阻力 {res_str}")
|
||||
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
@ -14,24 +14,41 @@ def analyze_market(indicator_summary: str, account_status: str) -> list[dict]:
|
||||
## 帳戶狀態
|
||||
{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 過高時降低倉位(波動風控)
|
||||
|
||||
### 最高優先:多時間框架過濾(現貨,只做多)
|
||||
- 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)
|
||||
1. **超買回落**: StochRSI K > 0.8 且 K 下穿 D + RSI > 65
|
||||
2. **阻力拒絕**: 價格接近樞紐阻力位(R1/R2)+ OBV 流出
|
||||
3. **趨勢反轉**: MACD 死叉 + EMA9 下穿 EMA21 + CMF < 0
|
||||
|
||||
### 過濾條件(必須全部滿足)
|
||||
- 成交量需高於 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
|
||||
@ -40,8 +57,8 @@ def analyze_market(indicator_summary: str, account_status: str) -> list[dict]:
|
||||
"symbol": "tBTCUST",
|
||||
"action": "BUY" | "SELL" | "HOLD",
|
||||
"confidence": 0.0-1.0,
|
||||
"reason": "簡短理由",
|
||||
"suggested_amount_pct": 0.10-0.20
|
||||
"reason": "簡短理由(含觸發的指標)",
|
||||
"suggested_amount_pct": 0.05-0.20
|
||||
}}
|
||||
]
|
||||
```
|
||||
|
||||
17
main.py
17
main.py
@ -179,7 +179,7 @@ def run_cycle():
|
||||
slack_notifier.send_error_alert(f"Market data fetch failed: {e}")
|
||||
return
|
||||
|
||||
# 4. Calculate indicators
|
||||
# 4. Calculate indicators (5m)
|
||||
indicators_by_symbol = {}
|
||||
current_prices = {}
|
||||
for sym, md in market_data.items():
|
||||
@ -191,6 +191,15 @@ def run_cycle():
|
||||
if ticker:
|
||||
current_prices[sym] = ticker.get("last_price", 0)
|
||||
|
||||
# 4b. Calculate HTF indicators (1h)
|
||||
htf_by_symbol = {}
|
||||
pivots_by_symbol = {}
|
||||
for sym, md in market_data.items():
|
||||
candles_htf = md.get("candles_htf")
|
||||
if candles_htf is not None and not candles_htf.empty:
|
||||
htf_by_symbol[sym] = indicators.calculate_htf_indicators(candles_htf)
|
||||
pivots_by_symbol[sym] = indicators.calculate_pivot_points(candles_htf)
|
||||
|
||||
# 5. Cache data (persisted for next crontab run)
|
||||
try:
|
||||
data_fetcher.cache_market_data(market_data, indicators_by_symbol)
|
||||
@ -301,8 +310,12 @@ def run_cycle():
|
||||
logger.info("Take profit executed: %s, P&L: %.2f USDT (%.2f%%)",
|
||||
sym, realized_pnl or 0, realized_pnl_pct or 0)
|
||||
|
||||
# 7. Build indicator summary for LLM
|
||||
# 7. Build indicator summary for LLM (5m + 1h context + pivots)
|
||||
indicator_summary = indicators.summarize_all(indicators_by_symbol)
|
||||
if htf_by_symbol:
|
||||
indicator_summary += "\n" + indicators.summarize_htf(htf_by_symbol)
|
||||
if pivots_by_symbol:
|
||||
indicator_summary += "\n" + indicators.summarize_pivots(pivots_by_symbol, current_prices)
|
||||
|
||||
# 8. Build account status string for LLM
|
||||
account_str = _build_account_string(port, current_prices)
|
||||
|
||||
@ -1,15 +1,9 @@
|
||||
{
|
||||
"tADAUST": {
|
||||
"order_id": 233273720461,
|
||||
"stop_price": 0.2626,
|
||||
"entry_price": 0.2707080505480357,
|
||||
"amount": 201.94477684
|
||||
},
|
||||
"tDOGE:UST": {
|
||||
"order_id": 233282701621,
|
||||
"stop_price": 0.09394531,
|
||||
"stop_price": 0.093946,
|
||||
"entry_price": 0.09685083602369626,
|
||||
"amount": 144.10790954030196
|
||||
"amount": 144.10790954
|
||||
},
|
||||
"tSUIUST": {
|
||||
"order_id": 233265250698,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user