feat: technical, news, and social analysis agents

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-20 17:52:51 +09:00
parent 46e06df131
commit 7e1d556385
6 changed files with 295 additions and 0 deletions

41
agents/news.py Normal file
View File

@@ -0,0 +1,41 @@
import logging
logger = logging.getLogger(__name__)
POSITIVE_WORDS = {"surge", "rally", "bullish", "high", "gain", "profit", "grows", "adoption", "breakout", "soar", "record"}
NEGATIVE_WORDS = {"crash", "drop", "bearish", "low", "loss", "fear", "down", "dump", "scam", "hack", "ban", "panic", "plunge"}
class NewsAgent:
def analyze(self, articles: list[dict]) -> float:
if not articles:
return 50.0
try:
scores = []
for article in articles:
vote_score = self._vote_sentiment(article.get("sentiment_votes", {}))
text_score = self._text_sentiment(article.get("title", ""))
if vote_score is not None:
scores.append(vote_score * 0.6 + text_score * 0.4)
else:
scores.append(text_score)
return round(max(0, min(100, sum(scores) / len(scores))), 1)
except Exception as e:
logger.error(f"News analysis error: {e}")
return 50.0
def _vote_sentiment(self, votes: dict) -> float | None:
pos = votes.get("positive", 0)
neg = votes.get("negative", 0)
total = pos + neg
if total == 0:
return None
return (pos / total) * 100
def _text_sentiment(self, text: str) -> float:
words = set(text.lower().split())
pos = len(words & POSITIVE_WORDS)
neg = len(words & NEGATIVE_WORDS)
total = pos + neg
if total == 0:
return 50.0
return (pos / total) * 100

33
agents/social.py Normal file
View File

@@ -0,0 +1,33 @@
import logging
logger = logging.getLogger(__name__)
class SocialAgent:
def analyze(self, sentiment: dict, mention_trend: float = 1.0) -> float:
total = sentiment.get("total", 0)
if total == 0:
return 50.0
try:
pos = sentiment.get("positive", 0)
ratio = pos / total
sentiment_score = ratio * 100
if mention_trend > 2.0:
trend_bonus = 15
elif mention_trend > 1.5:
trend_bonus = 10
elif mention_trend > 1.0:
trend_bonus = 5
elif mention_trend < 0.5:
trend_bonus = -10
else:
trend_bonus = 0
if ratio < 0.4 and mention_trend > 1.5:
trend_bonus = -15
score = sentiment_score + trend_bonus
return round(max(0, min(100, score)), 1)
except Exception as e:
logger.error(f"Social analysis error: {e}")
return 50.0

113
agents/technical.py Normal file
View File

@@ -0,0 +1,113 @@
import pandas as pd
import ta
import logging
logger = logging.getLogger(__name__)
class TechnicalAgent:
MIN_CANDLES = 30
def analyze(self, df: pd.DataFrame) -> float:
if len(df) < self.MIN_CANDLES:
return 50.0
try:
signals = []
# RSI (14)
rsi = ta.momentum.RSIIndicator(df["close"], window=14).rsi().iloc[-1]
if rsi < 30:
signals.append(65)
elif rsi < 45:
signals.append(70)
elif rsi < 55:
signals.append(50)
elif rsi < 70:
signals.append(35)
else:
signals.append(40)
# MACD
macd_ind = ta.trend.MACD(df["close"])
macd_diff = macd_ind.macd_diff().iloc[-1]
prev_diff = macd_ind.macd_diff().iloc[-2]
if macd_diff > 0 and prev_diff <= 0:
signals.append(85)
elif macd_diff < 0 and prev_diff >= 0:
signals.append(15)
elif macd_diff > 0:
signals.append(65)
else:
signals.append(35)
# Bollinger Bands
bb = ta.volatility.BollingerBands(df["close"])
bb_high = bb.bollinger_hband().iloc[-1]
bb_low = bb.bollinger_lband().iloc[-1]
price = df["close"].iloc[-1]
bb_pct = (price - bb_low) / (bb_high - bb_low) if (bb_high - bb_low) > 0 else 0.5
if bb_pct < 0.2:
signals.append(55)
elif bb_pct > 0.8:
signals.append(40)
else:
signals.append(50)
# SMA trend (20 vs 50)
sma20 = df["close"].rolling(20).mean().iloc[-1]
sma50 = df["close"].rolling(50).mean().iloc[-1] if len(df) >= 50 else sma20
if price > sma20 > sma50:
signals.append(80)
elif price > sma20:
signals.append(65)
elif price < sma20 < sma50:
signals.append(20)
else:
signals.append(40)
# SMA 200 (long-term trend)
if len(df) >= 200:
sma200 = df["close"].rolling(200).mean().iloc[-1]
if price > sma200:
signals.append(70)
else:
signals.append(30)
# Volume trend
vol_avg = df["volume"].rolling(20).mean().iloc[-1]
vol_current = df["volume"].iloc[-1]
vol_ratio = vol_current / vol_avg if vol_avg > 0 else 1.0
if vol_ratio > 2.0 and price > sma20:
signals.append(80)
elif vol_ratio > 2.0:
signals.append(40)
else:
signals.append(50)
# OBV trend
obv = ta.volume.OnBalanceVolumeIndicator(df["close"], df["volume"]).on_balance_volume()
obv_sma = obv.rolling(20).mean()
if obv.iloc[-1] > obv_sma.iloc[-1]:
signals.append(65)
else:
signals.append(35)
# Candlestick patterns (simplified)
last = df.iloc[-1]
prev = df.iloc[-2]
body = last["close"] - last["open"]
prev_body = prev["close"] - prev["open"]
if prev_body < 0 and body > 0 and body > abs(prev_body):
signals.append(80) # bullish engulfing
elif prev_body > 0 and body < 0 and abs(body) > prev_body:
signals.append(20) # bearish engulfing
elif abs(body) < (last["high"] - last["low"]) * 0.1:
signals.append(50) # doji
else:
signals.append(50)
return round(sum(signals) / len(signals), 1)
except Exception as e:
logger.error(f"Technical analysis error: {e}")
return 50.0