Files
2026-07-16 23:55:16 +09:00

159 lines
5.8 KiB
Python

from __future__ import annotations
import ta
import pandas as pd
from app.engine.strategies.base import BaseStrategy, Signal
class TechnicalStrategy(BaseStrategy):
"""기술적 분석 기반 전략 (MACD, RSI, 볼린저밴드)"""
def evaluate(self, current_price: dict, price_history: list[dict], holding_qty: int) -> Signal:
df = self._to_dataframe(price_history)
if len(df) < 30:
return Signal(action="hold", stock_code=self.stock_code)
price = current_price.get("current_price", 0)
indicators = self.params.get("indicators", ["rsi"])
qty = self.params.get("qty", 1)
buy_signals = []
sell_signals = []
if "rsi" in indicators:
rsi_signal = self._evaluate_rsi(df, price, holding_qty, qty)
if rsi_signal.action == "buy":
buy_signals.append(rsi_signal)
elif rsi_signal.action == "sell":
sell_signals.append(rsi_signal)
if "macd" in indicators:
macd_signal = self._evaluate_macd(df, price, holding_qty, qty)
if macd_signal.action == "buy":
buy_signals.append(macd_signal)
elif macd_signal.action == "sell":
sell_signals.append(macd_signal)
if "bollinger" in indicators:
bb_signal = self._evaluate_bollinger(df, price, holding_qty, qty)
if bb_signal.action == "buy":
buy_signals.append(bb_signal)
elif bb_signal.action == "sell":
sell_signals.append(bb_signal)
if buy_signals:
best = max(buy_signals, key=lambda s: s.confidence)
return best
if sell_signals:
best = max(sell_signals, key=lambda s: s.confidence)
return best
return Signal(action="hold", stock_code=self.stock_code)
def _evaluate_rsi(
self, df: pd.DataFrame, price: int, holding_qty: int, qty: int
) -> Signal:
period = self.params.get("rsi_period", 14)
oversold = self.params.get("rsi_oversold", 30)
overbought = self.params.get("rsi_overbought", 70)
rsi = ta.momentum.RSIIndicator(df["close"], window=period).rsi()
current_rsi = rsi.iloc[-1] if not rsi.empty else 50
if current_rsi <= oversold and holding_qty == 0:
return Signal(
action="buy",
stock_code=self.stock_code,
qty=qty,
price=price,
reason=f"RSI 과매도: {current_rsi:.1f} <= {oversold}",
confidence=(oversold - current_rsi) / oversold if oversold > 0 else 0,
)
if current_rsi >= overbought and holding_qty > 0:
return Signal(
action="sell",
stock_code=self.stock_code,
qty=min(qty, holding_qty),
price=price,
reason=f"RSI 과매수: {current_rsi:.1f} >= {overbought}",
confidence=(current_rsi - overbought) / (100 - overbought) if overbought < 100 else 0,
)
return Signal(action="hold", stock_code=self.stock_code)
def _evaluate_macd(
self, df: pd.DataFrame, price: int, holding_qty: int, qty: int
) -> Signal:
fast = self.params.get("macd_fast", 12)
slow = self.params.get("macd_slow", 26)
signal_period = self.params.get("macd_signal", 9)
macd_ind = ta.trend.MACD(df["close"], window_fast=fast, window_slow=slow, window_sign=signal_period)
macd_line = macd_ind.macd()
signal_line = macd_ind.macd_signal()
if len(macd_line) < 2 or len(signal_line) < 2:
return Signal(action="hold", stock_code=self.stock_code)
prev_macd = macd_line.iloc[-2]
prev_signal = signal_line.iloc[-2]
curr_macd = macd_line.iloc[-1]
curr_signal = signal_line.iloc[-1]
if prev_macd <= prev_signal and curr_macd > curr_signal and holding_qty == 0:
return Signal(
action="buy",
stock_code=self.stock_code,
qty=qty,
price=price,
reason=f"MACD 골든크로스: MACD({curr_macd:.2f}) > Signal({curr_signal:.2f})",
confidence=0.8,
)
if prev_macd >= prev_signal and curr_macd < curr_signal and holding_qty > 0:
return Signal(
action="sell",
stock_code=self.stock_code,
qty=min(qty, holding_qty),
price=price,
reason=f"MACD 데드크로스: MACD({curr_macd:.2f}) < Signal({curr_signal:.2f})",
confidence=0.8,
)
return Signal(action="hold", stock_code=self.stock_code)
def _evaluate_bollinger(
self, df: pd.DataFrame, price: int, holding_qty: int, qty: int
) -> Signal:
period = self.params.get("bb_period", 20)
std_dev = self.params.get("bb_std", 2.0)
bb = ta.volatility.BollingerBands(df["close"], window=period, window_dev=std_dev)
upper = bb.bollinger_hband().iloc[-1]
lower = bb.bollinger_lband().iloc[-1]
mid = bb.bollinger_mavg().iloc[-1]
if price <= lower and holding_qty == 0:
return Signal(
action="buy",
stock_code=self.stock_code,
qty=qty,
price=price,
reason=f"볼린저밴드 하단 돌파: 가격({price}) <= 하단({lower:.0f})",
confidence=0.7,
)
if price >= upper and holding_qty > 0:
return Signal(
action="sell",
stock_code=self.stock_code,
qty=min(qty, holding_qty),
price=price,
reason=f"볼린저밴드 상단 돌파: 가격({price}) >= 상단({upper:.0f})",
confidence=0.7,
)
return Signal(action="hold", stock_code=self.stock_code)