feat: add Fear & Greed filter to entry logic
- core/fng.py: F&G API wrapper with 1h cache (alternative.me) - FNG_MIN_ENTRY=41 (env-configurable), blocks entry below threshold - core/strategy.py: call is_entry_allowed() before volume/regime checks - daemon/runner.py: log F&G status on every scan cycle - core/notify.py: include F&G value in buy/signal/status notifications - core/trader.py: pass current F&G value to notify_buy Backtest evidence (1y / 18 tickers / 1h candles): - No filter: 820 trades, 32.7% WR, avg +0.012%, KRW +95k - F&G >= 41: 372 trades, 39.5% WR, avg +0.462%, KRW +1.72M - Blocked 452 trades (avg -0.372%, saved ~1.68M KRW loss) Also add: - backtest_db.py: Oracle DB storage for backtest runs/results/trades - fng_1y_backtest.py, fng_adaptive_backtest.py, fng_sim_comparison.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,16 +31,27 @@ def _send(text: str) -> None:
|
||||
def notify_buy(
|
||||
ticker: str, price: float, amount: float, invested_krw: int,
|
||||
max_budget: int = 0, per_position: int = 0,
|
||||
fng: int = 0,
|
||||
) -> None:
|
||||
budget_line = (
|
||||
f"운용예산: {max_budget:,}원 (포지션당 {per_position:,}원)\n"
|
||||
if max_budget else ""
|
||||
)
|
||||
fng_label = (
|
||||
"극탐욕" if fng >= 76 else
|
||||
"탐욕" if fng >= 56 else
|
||||
"중립" if fng >= 46 else
|
||||
"약공포" if fng >= 41 else
|
||||
"공포" if fng >= 26 else
|
||||
"극공포"
|
||||
) if fng else ""
|
||||
fng_line = f"F&G: {fng} ({fng_label})\n" if fng else ""
|
||||
_send(
|
||||
f"📈 <b>[매수]</b> {ticker}\n"
|
||||
f"가격: {price:,.2f}원\n"
|
||||
f"수량: {amount:.8f}\n"
|
||||
f"투자금: {invested_krw:,.2f}원\n"
|
||||
f"{fng_line}"
|
||||
f"{budget_line}"
|
||||
)
|
||||
|
||||
@@ -62,12 +73,28 @@ def notify_sell(
|
||||
)
|
||||
|
||||
|
||||
def notify_signal(ticker: str, signal_price: float, vol_mult: float) -> None:
|
||||
def notify_signal(ticker: str, signal_price: float, vol_mult: float, fng: int = 0) -> None:
|
||||
"""거래량 축적 신호 감지 알림."""
|
||||
from .fng import FNG_MIN_ENTRY
|
||||
fng_label = (
|
||||
"극탐욕" if fng >= 76 else
|
||||
"탐욕" if fng >= 56 else
|
||||
"중립" if fng >= 46 else
|
||||
"약공포" if fng >= 41 else
|
||||
"공포" if fng >= 26 else
|
||||
"극공포"
|
||||
) if fng else ""
|
||||
fng_line = f"F&G: {fng} ({fng_label})\n" if fng else ""
|
||||
warn_line = (
|
||||
f"⚠️ F&G={fng} < {FNG_MIN_ENTRY} → <b>진입차단중</b>\n"
|
||||
if fng and fng < FNG_MIN_ENTRY else ""
|
||||
)
|
||||
_send(
|
||||
f"🔍 <b>[축적감지]</b> {ticker}\n"
|
||||
f"신호가: {signal_price:,.2f}원\n"
|
||||
f"거래량: {vol_mult:.1f}x 급증 + 2h 횡보\n"
|
||||
f"{fng_line}"
|
||||
f"{warn_line}"
|
||||
f"진입 목표: {signal_price * 1.048:,.2f}원 (+4.8%)"
|
||||
)
|
||||
|
||||
@@ -98,6 +125,20 @@ def notify_status(
|
||||
f"| 조건 TREND≥{regime['trend_pct']}% / VOL≥{regime['vol_mult']}x\n"
|
||||
)
|
||||
|
||||
# F&G 지수
|
||||
from .fng import get_fng, FNG_MIN_ENTRY
|
||||
fv = get_fng()
|
||||
fng_label = (
|
||||
"극탐욕" if fv >= 76 else
|
||||
"탐욕" if fv >= 56 else
|
||||
"중립" if fv >= 46 else
|
||||
"약공포" if fv >= 41 else
|
||||
"공포" if fv >= 26 else
|
||||
"극공포"
|
||||
)
|
||||
fng_status = "✅진입허용" if fv >= FNG_MIN_ENTRY else "🚫진입차단"
|
||||
fng_line = f"😨 F&G: {fv} ({fng_label}) {fng_status}\n"
|
||||
|
||||
# 1시간 이상 보유 포지션만 필터
|
||||
long_positions = {
|
||||
ticker: pos for ticker, pos in positions.items()
|
||||
@@ -112,7 +153,7 @@ def notify_status(
|
||||
)
|
||||
|
||||
# 포지션 없어도 레짐 정보는 전송
|
||||
header = f"📊 <b>[{now} 현황]</b>\n{regime_line}{budget_info}"
|
||||
header = f"📊 <b>[{now} 현황]</b>\n{regime_line}{fng_line}{budget_info}"
|
||||
|
||||
if not long_positions:
|
||||
_send(header + "1h+ 보유 포지션 없음")
|
||||
|
||||
Reference in New Issue
Block a user