- Add market_regime.py: BTC/ETH/SOL/XRP weighted 2h trend score Bull(≥+1.5%) / Neutral / Bear(<-1%) regime detection with 10min cache - strategy.py: dynamic TREND/VOL thresholds based on current regime Bull: 3%/1.5x, Neutral: 5%/2.0x, Bear: 8%/3.5x - price_collector.py: always include leader coins in price history - trader.py: compound reinvestment (profit added to budget, floor at initial) - notify.py: regime info in hourly report, P&L icons (✅/❌, 💚/🔴) - main.py: hourly status at top-of-hour, filter positions held 1h+ - backtest.py: timestop/combo comparison modes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
111 lines
3.2 KiB
Python
111 lines
3.2 KiB
Python
"""시장 레짐(Bull/Neutral/Bear) 판단.
|
|
|
|
BTC·ETH·SOL·XRP 가중 평균 2h 추세로 레짐을 결정하고
|
|
매수 조건 파라미터(trend_pct, vol_mult)를 동적으로 반환한다.
|
|
계산된 현재가는 price_history DB에 저장해 재활용한다.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import time
|
|
|
|
import pyupbit
|
|
|
|
from .price_db import get_price_n_hours_ago, insert_prices
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# 대장 코인 가중치
|
|
LEADERS: dict[str, float] = {
|
|
"KRW-BTC": 0.40,
|
|
"KRW-ETH": 0.30,
|
|
"KRW-SOL": 0.15,
|
|
"KRW-XRP": 0.15,
|
|
}
|
|
TREND_HOURS = 2 # 2h 추세 기준
|
|
|
|
BULL_THRESHOLD = 1.5 # score ≥ 1.5% → Bull
|
|
BEAR_THRESHOLD = -1.0 # score < -1.0% → Bear
|
|
|
|
# 레짐별 매수 조건 파라미터
|
|
REGIME_PARAMS: dict[str, dict] = {
|
|
"bull": {"trend_pct": 3.0, "vol_mult": 1.5, "emoji": "🟢"},
|
|
"neutral": {"trend_pct": 5.0, "vol_mult": 2.0, "emoji": "🟡"},
|
|
"bear": {"trend_pct": 8.0, "vol_mult": 3.5, "emoji": "🔴"},
|
|
}
|
|
|
|
# 10분 캐시 (스캔 루프마다 API 호출 방지)
|
|
_cache: dict = {}
|
|
_cache_ts: float = 0.0
|
|
_CACHE_TTL = 600
|
|
|
|
|
|
def get_regime() -> dict:
|
|
"""현재 시장 레짐 반환.
|
|
|
|
Returns:
|
|
{
|
|
'name': 'bull' | 'neutral' | 'bear',
|
|
'score': float, # 가중 평균 2h 추세(%)
|
|
'trend_pct': float, # 매수 추세 임계값
|
|
'vol_mult': float, # 거래량 배수 임계값
|
|
'emoji': str,
|
|
}
|
|
"""
|
|
global _cache, _cache_ts
|
|
if _cache and (time.time() - _cache_ts) < _CACHE_TTL:
|
|
return _cache
|
|
|
|
score = 0.0
|
|
current_prices: dict[str, float] = {}
|
|
|
|
for ticker, weight in LEADERS.items():
|
|
try:
|
|
current = pyupbit.get_current_price(ticker)
|
|
if not current:
|
|
continue
|
|
current_prices[ticker] = current
|
|
|
|
# DB에서 2h 전 가격 조회 → 없으면 API 캔들로 대체
|
|
past = get_price_n_hours_ago(ticker, TREND_HOURS)
|
|
if past is None:
|
|
df = pyupbit.get_ohlcv(ticker, interval="minute60", count=4)
|
|
if df is not None and len(df) >= 3:
|
|
past = float(df["close"].iloc[-3])
|
|
|
|
if past:
|
|
trend = (current - past) / past * 100
|
|
score += trend * weight
|
|
logger.debug(f"[레짐] {ticker} {trend:+.2f}% (기여 {trend*weight:+.3f})")
|
|
|
|
except Exception as e:
|
|
logger.warning(f"[레짐] {ticker} 오류: {e}")
|
|
|
|
# 현재가 DB 저장 (다음 레짐 계산 및 추세 판단에 재활용)
|
|
if current_prices:
|
|
try:
|
|
insert_prices(current_prices)
|
|
except Exception as e:
|
|
logger.warning(f"[레짐] 가격 저장 오류: {e}")
|
|
|
|
# 레짐 결정
|
|
if score >= BULL_THRESHOLD:
|
|
name = "bull"
|
|
elif score < BEAR_THRESHOLD:
|
|
name = "bear"
|
|
else:
|
|
name = "neutral"
|
|
|
|
params = REGIME_PARAMS[name]
|
|
result = {"name": name, "score": round(score, 3), **params}
|
|
|
|
logger.info(
|
|
f"[레짐] score={score:+.3f}% → {params['emoji']} {name.upper()} "
|
|
f"(TREND≥{params['trend_pct']}% / VOL≥{params['vol_mult']}x)"
|
|
)
|
|
|
|
_cache = result
|
|
_cache_ts = time.time()
|
|
return result
|