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:
kroutony 2026-03-14 03:26:59 +00:00
parent 2e45db006f
commit 1abfdefecd
6 changed files with 175 additions and 27 deletions

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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. **過濾條件**:
### 最高優先:多時間框架過濾(現貨,只做多)
- 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 期平均確認動能
- ATR 過高時降低倉位波動風控
- 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
View File

@ -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)

View File

@ -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,