diff --git a/config.py b/config.py index c6d16de..51f7777 100644 --- a/config.py +++ b/config.py @@ -44,8 +44,22 @@ MIN_ORDER_AMOUNT = { "tLINK:UST": 0.2, "tXRPUST": 4.0, "tSHIB:UST": 141010.0, "tDOGE:UST": 22.0, "tATOUST": 0.02, "tXLMUST": 4.0, } -STOP_LOSS_PCT = 0.03 +# ATR-based dynamic stop-loss +ATR_SL_MULTIPLIER = 3.0 # stop = entry - ATR × multiplier +ATR_SL_MIN_PCT = 0.03 # 下限:不低於 3%(低波動期等同現行) +ATR_SL_MAX_PCT = 0.08 # 上限:不超過 8% +STOP_LOSS_PCT = 0.08 # 安全網用上限值(防止交易所 stop 失敗時過早觸發) TAKE_PROFIT_PCT = 0.15 + +# LLM SELL profit-tiered thresholds (5 層遞減) +LLM_SELL_MIN_PROFIT_PCT = 0.01 # < 1% 不賣 +LLM_SELL_TIERS = [ + # (獲利上限, 最低信心) + (0.02, 0.7), # 1-2% 需信心 ≥ 0.7 + (0.03, 0.6), # 2-3% 需信心 ≥ 0.6 + (0.05, 0.5), # 3-5% 需信心 ≥ 0.5 + (None, 0.4), # ≥ 5% 需信心 ≥ 0.4 +] MAX_POSITION_PCT = 0.30 # Max 30% of portfolio per coin MAX_TOTAL_EXPOSURE_PCT = 1.00 # No cap — use full balance diff --git a/risk_manager.py b/risk_manager.py index 6523db5..6184456 100644 --- a/risk_manager.py +++ b/risk_manager.py @@ -101,7 +101,7 @@ def adjust_size_by_volatility(amount_usdt: float, atr: float, price: float) -> f return amount_usdt -def validate_trade(signal: dict, portfolio: dict, indicators_df=None) -> tuple[dict | None, str]: +def validate_trade(signal: dict, portfolio: dict, indicators_df=None, current_prices: dict | None = None) -> tuple[dict | None, str]: """Run all risk checks. Returns (signal, "") on success or (None, reason) on rejection.""" action = signal.get("action", "HOLD") if action == "HOLD": @@ -170,6 +170,28 @@ def validate_trade(signal: dict, portfolio: dict, indicators_df=None) -> tuple[d if pos.get("amount", 0) <= 0: logger.info("No position to sell for %s", symbol) return None, "無持倉,跳過" + + # Profit-tiered LLM SELL filter (止盈/止損由 risk_manager 和交易所處理,這裡只管 LLM 信號) + entry = pos.get("entry_price", 0) + current = (current_prices or {}).get(symbol, 0) + if entry > 0 and current > 0: + pnl_pct = (current - entry) / entry + confidence = signal.get("confidence", 0) + if 0 <= pnl_pct < config.LLM_SELL_MIN_PROFIT_PCT: + return None, f"獲利不足 {pnl_pct:+.2%},不執行 LLM SELL" + if pnl_pct < 0: + logger.info("LLM SELL %s: 虧損中 (P&L %+.2f%%),信任 LLM 信號", symbol, pnl_pct * 100) + else: + required_conf = config.LLM_SELL_TIERS[-1][1] # default to loosest + for upper, min_conf in config.LLM_SELL_TIERS: + if upper is None or pnl_pct < upper: + required_conf = min_conf + break + if confidence < required_conf: + return None, f"獲利 {pnl_pct:+.2%} 需信心 ≥ {required_conf},目前 {confidence}" + logger.info("LLM SELL %s: 通過 (P&L %+.2f%%, conf %.2f ≥ %.2f)", + symbol, pnl_pct * 100, confidence, required_conf) + # SELL: 一律全部賣出 pos_value = pos.get("value_usdt", 0) if pos_value <= 0: diff --git a/slack_notifier.py b/slack_notifier.py index 114cc08..39d4a79 100644 --- a/slack_notifier.py +++ b/slack_notifier.py @@ -71,8 +71,10 @@ def send_cycle_report( amount_usdt = tp.get("amount_usdt", 0) pnl_str = "" if tp.get("realized_pnl") is not None: - sign = "+" if tp["realized_pnl"] >= 0 else "" - pnl_str = f",收益 {sign}{tp['realized_pnl']:.2f} USDT ({sign}{tp['realized_pnl_pct']:.2f}%)" + pnl_val = tp["realized_pnl"] + pnl_pct_val = tp["realized_pnl_pct"] + sign = "+" if pnl_pct_val > 0 else "" + pnl_str = f",收益 {sign}{pnl_val:.2f} USDT ({sign}{pnl_pct_val:.2f}%)" lines.append(f" 🔴 SELL {name} — {pnl},平倉 {amount_usdt:.1f} USDT{pnl_str}") lines.append("") @@ -116,8 +118,10 @@ def send_cycle_report( if action == "BUY" and t.get("stop_price"): extra = f" — 止損掛 {t['stop_price']:.6g}" elif action == "SELL" and t.get("realized_pnl") is not None: - sign = "+" if t["realized_pnl"] >= 0 else "" - extra = f" — 收益 {sign}{t['realized_pnl']:.2f} USDT ({sign}{t['realized_pnl_pct']:.2f}%)" + pnl = t["realized_pnl"] + pnl_pct = t["realized_pnl_pct"] + sign = "+" if pnl_pct > 0 else "" + extra = f" — 收益 {sign}{pnl:.2f} USDT ({sign}{pnl_pct:.2f}%)" tag = " [止損觸發]" if t.get("is_stop_loss") else "" lines.append(f" {emoji} {name} — {detail}{extra}{tag}") lines.append("")