Add 5-tier LLM SELL profit thresholds, allow loss sells, fix PnL sign display

- Replace 3-tier LLM SELL thresholds with 5-tier descending confidence
  (1-2%→0.7, 2-3%→0.6, 3-5%→0.5, ≥5%→0.4), min profit 1%
- Allow LLM SELL on losing positions (trust LLM signal for cut-loss)
- Fix +-1.90% sign bug: use pnl_pct instead of pnl amount for sign

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kroutony 2026-03-14 13:49:14 +00:00
parent e2ec3cd920
commit 7f03d479c7
3 changed files with 46 additions and 6 deletions

View File

@ -44,8 +44,22 @@ MIN_ORDER_AMOUNT = {
"tLINK:UST": 0.2, "tXRPUST": 4.0, "tSHIB:UST": 141010.0, "tLINK:UST": 0.2, "tXRPUST": 4.0, "tSHIB:UST": 141010.0,
"tDOGE:UST": 22.0, "tATOUST": 0.02, "tXLMUST": 4.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 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_POSITION_PCT = 0.30 # Max 30% of portfolio per coin
MAX_TOTAL_EXPOSURE_PCT = 1.00 # No cap — use full balance MAX_TOTAL_EXPOSURE_PCT = 1.00 # No cap — use full balance

View File

@ -101,7 +101,7 @@ def adjust_size_by_volatility(amount_usdt: float, atr: float, price: float) -> f
return amount_usdt 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.""" """Run all risk checks. Returns (signal, "") on success or (None, reason) on rejection."""
action = signal.get("action", "HOLD") action = signal.get("action", "HOLD")
if 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: if pos.get("amount", 0) <= 0:
logger.info("No position to sell for %s", symbol) logger.info("No position to sell for %s", symbol)
return None, "無持倉,跳過" 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: 一律全部賣出 # SELL: 一律全部賣出
pos_value = pos.get("value_usdt", 0) pos_value = pos.get("value_usdt", 0)
if pos_value <= 0: if pos_value <= 0:

View File

@ -71,8 +71,10 @@ def send_cycle_report(
amount_usdt = tp.get("amount_usdt", 0) amount_usdt = tp.get("amount_usdt", 0)
pnl_str = "" pnl_str = ""
if tp.get("realized_pnl") is not None: if tp.get("realized_pnl") is not None:
sign = "+" if tp["realized_pnl"] >= 0 else "" pnl_val = tp["realized_pnl"]
pnl_str = f",收益 {sign}{tp['realized_pnl']:.2f} USDT ({sign}{tp['realized_pnl_pct']:.2f}%)" 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(f" 🔴 SELL {name}{pnl},平倉 {amount_usdt:.1f} USDT{pnl_str}")
lines.append("") lines.append("")
@ -116,8 +118,10 @@ def send_cycle_report(
if action == "BUY" and t.get("stop_price"): if action == "BUY" and t.get("stop_price"):
extra = f" — 止損掛 {t['stop_price']:.6g}" extra = f" — 止損掛 {t['stop_price']:.6g}"
elif action == "SELL" and t.get("realized_pnl") is not None: elif action == "SELL" and t.get("realized_pnl") is not None:
sign = "+" if t["realized_pnl"] >= 0 else "" pnl = t["realized_pnl"]
extra = f" — 收益 {sign}{t['realized_pnl']:.2f} USDT ({sign}{t['realized_pnl_pct']:.2f}%)" 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 "" tag = " [止損觸發]" if t.get("is_stop_loss") else ""
lines.append(f" {emoji} {name}{detail}{extra}{tag}") lines.append(f" {emoji} {name}{detail}{extra}{tag}")
lines.append("") lines.append("")