Files
polymarket-whale-watcher/src/services/llm_analyzer.py
T
SII-leiyuandClaude 0254e2c01a Initial commit: Polymarket Whale Watcher
- Add trade monitoring and whale detection system
- Add LLM-powered trade analysis
- Add market data fetching from Polymarket API
- Include example environment configuration
- Add automated report generation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-07 15:41:05 +08:00

300 lines
10 KiB
Python

"""LLM analyzer service - analyzes whale trades using AI."""
import json
import logging
import os
import re
from datetime import datetime
from typing import Optional
from google import genai
from google.genai import types
from src.config import get_settings
from src.models.trade import WhaleTrade
from src.models.decision import LLMDecision, TradeRecommendation, TradeAction, TraderCredibility
from src.services.anomaly_detector import AnomalyDetector
from src.prompts.whale_analyzer import WhaleAnalyzerPrompts
logger = logging.getLogger(__name__)
class LLMAnalyzer:
"""
Analyzes whale trades using LLM (Google Gemini models).
Combines trade context with superforecaster methodology to generate
comprehensive analysis reports with trading recommendations.
"""
def __init__(self):
self.settings = get_settings()
# Configure Gemini API using new client SDK
os.environ["GOOGLE_API_KEY"] = self.settings.gemini_api_key
self.client = genai.Client()
self.anomaly_detector = AnomalyDetector()
self.prompts = WhaleAnalyzerPrompts()
def _extract_json_from_response(self, response: str) -> Optional[dict]:
"""
Extract JSON from LLM response.
Args:
response: The LLM response text
Returns:
Parsed JSON dict or None
"""
# Try to find JSON in code blocks
json_pattern = r"```(?:json)?\s*([\s\S]*?)```"
matches = re.findall(json_pattern, response)
for match in matches:
try:
return json.loads(match.strip())
except json.JSONDecodeError:
continue
# Try to find raw JSON
try:
# Find JSON-like content
start = response.find("{")
end = response.rfind("}") + 1
if start >= 0 and end > start:
return json.loads(response[start:end])
except json.JSONDecodeError:
pass
return None
def _parse_recommendation(self, json_data: dict) -> TradeRecommendation:
"""
Parse JSON data into TradeRecommendation.
Args:
json_data: Parsed JSON from LLM
Returns:
TradeRecommendation object
"""
action_str = json_data.get("action", "HOLD").upper()
try:
action = TradeAction(action_str)
except ValueError:
action = TradeAction.HOLD
confidence = float(json_data.get("confidence", 0.0))
# Clamp confidence to valid range
confidence = max(0.0, min(1.0, confidence))
suggested_price = json_data.get("suggested_price")
if suggested_price is not None:
suggested_price = float(suggested_price)
suggested_size = float(json_data.get("suggested_size_percent", 0.1))
suggested_size = max(0.0, min(1.0, suggested_size))
# Parse insider trading assessment fields
insider_likelihood = float(json_data.get("insider_trading_likelihood", 0.0))
insider_likelihood = max(0.0, min(1.0, insider_likelihood))
credibility_str = json_data.get("trader_credibility", "UNKNOWN").upper()
try:
trader_credibility = TraderCredibility(credibility_str)
except ValueError:
trader_credibility = TraderCredibility.UNKNOWN
insider_evidence = str(json_data.get("insider_evidence", ""))
return TradeRecommendation(
action=action,
outcome=str(json_data.get("outcome", "")),
confidence=confidence,
suggested_price=suggested_price,
suggested_size_percent=suggested_size,
reasoning=str(json_data.get("reasoning", "")),
insider_trading_likelihood=insider_likelihood,
trader_credibility=trader_credibility,
insider_evidence=insider_evidence,
)
async def analyze_whale_trade(self, whale_trade: WhaleTrade) -> LLMDecision:
"""
Analyze a whale trade using LLM.
Args:
whale_trade: The whale trade to analyze
Returns:
LLMDecision with analysis and recommendation
"""
# Format trade context for LLM
trade_context = self.anomaly_detector.format_for_llm(whale_trade)
# Build prompt (Gemini uses single prompt with system instruction)
system_prompt = self.prompts.system_prompt()
user_prompt = self.prompts.analyze_whale_trade(trade_context)
full_prompt = f"{system_prompt}\n\n---\n\n{user_prompt}"
try:
# Call Gemini API with Google Search tool enabled
response = self.client.models.generate_content(
model=self.settings.llm_model,
contents=full_prompt,
config=types.GenerateContentConfig(
tools=[types.Tool(google_search=types.GoogleSearch())],
),
)
analysis_text = response.text
logger.debug(f"LLM response: {analysis_text[:500]}...")
# Extract JSON from response
json_data = self._extract_json_from_response(analysis_text)
if json_data:
recommendation = self._parse_recommendation(json_data)
else:
# Default to HOLD if we can't parse the response
logger.warning("Could not parse LLM response as JSON, defaulting to HOLD")
recommendation = TradeRecommendation(
action=TradeAction.HOLD,
outcome="",
confidence=0.0,
reasoning="Failed to parse LLM response",
)
return LLMDecision(
whale_trade_id=whale_trade.id,
market_id=whale_trade.market_id,
analysis=analysis_text,
recommendation=recommendation,
)
except Exception as e:
logger.error(f"Error calling LLM: {e}")
# Return a safe default decision
return LLMDecision(
whale_trade_id=whale_trade.id,
market_id=whale_trade.market_id,
analysis=f"Error during analysis: {str(e)}",
recommendation=TradeRecommendation(
action=TradeAction.HOLD,
outcome="",
confidence=0.0,
reasoning=f"Analysis failed: {str(e)}",
),
)
def format_full_report(self, whale_trade: WhaleTrade, decision: LLMDecision) -> str:
"""
Format a complete analysis report with trade info, analysis, and decision.
Args:
whale_trade: The whale trade
decision: The LLM decision
Returns:
Formatted report string
"""
trade = whale_trade.trade
rec = decision.recommendation
# Format outcome prices
prices_str = ""
if whale_trade.market_outcomes and whale_trade.market_outcome_prices:
prices_str = " | ".join([
f"{o}: {p:.1%}"
for o, p in zip(whale_trade.market_outcomes, whale_trade.market_outcome_prices)
])
# Action emoji and color indicator
action_indicator = {
TradeAction.BUY: "🟢 BUY",
TradeAction.SELL: "🔴 SELL",
TradeAction.HOLD: "⚪ HOLD",
}
# Insider trading likelihood indicator
insider_likelihood = rec.insider_trading_likelihood
if insider_likelihood >= 0.7:
insider_indicator = f"🔴 高度可疑 ({insider_likelihood:.0%})"
elif insider_likelihood >= 0.4:
insider_indicator = f"🟡 中等可能 ({insider_likelihood:.0%})"
else:
insider_indicator = f"🟢 普通交易 ({insider_likelihood:.0%})"
# Trader credibility indicator
credibility_indicators = {
TraderCredibility.HIGH: "🏆 高可信度 (前100名)",
TraderCredibility.MEDIUM: "⭐ 中等可信度 (100-500名)",
TraderCredibility.LOW: "📉 低可信度 (500名+)",
TraderCredibility.UNKNOWN: "❓ 未知 (未上榜)",
}
credibility_str = credibility_indicators.get(rec.trader_credibility, "❓ 未知")
# Trader ranking info
trader_ranking_str = ""
if whale_trade.trader_ranking:
tr = whale_trade.trader_ranking
rank_str = f"#{tr.rank}" if tr.rank else "未上榜"
pnl_str = f"${tr.pnl:,.2f}" if tr.pnl else "N/A"
trader_ranking_str = f"| **交易者排名** | {rank_str} (PnL: {pnl_str}) |"
report = f"""
{'='*70}
# 🐋 鲸鱼交易分析报告
{'='*70}
**生成时间**: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC
## 交易摘要
| 项目 | 详情 |
|------|------|
| **市场** | {whale_trade.market_question} |
| **交易金额** | ${trade.usdc_size:,.2f} USDC |
| **交易方向** | {trade.side} |
| **交易价格** | {trade.price:.4f} ({trade.price:.1%}) |
| **交易结果** | {trade.outcome} |
| **当前赔率** | {prices_str} |
| **交易时间** | {datetime.fromtimestamp(trade.timestamp).strftime('%Y-%m-%d %H:%M:%S') if trade.timestamp else 'N/A'} |
{trader_ranking_str}
{'='*70}
{decision.analysis}
{'='*70}
## 🔍 内幕交易评估
{'='*70}
| 项目 | 评估 |
|------|------|
| **内幕交易可能性** | {insider_indicator} |
| **交易者可信度** | {credibility_str} |
**关键证据**: {rec.insider_evidence or '无明确证据'}
{'='*70}
## 📊 决策摘要
{'='*70}
| 项目 | 建议 |
|------|------|
| **操作建议** | {action_indicator.get(rec.action, '⚪ HOLD')} |
| **目标结果** | {rec.outcome or 'N/A'} |
| **信心程度** | {rec.confidence:.1%} |
| **建议仓位** | {rec.suggested_size_percent:.1%} |
| **建议价格** | {f'{rec.suggested_price:.4f}' if rec.suggested_price else 'Market'} |
**决策理由**: {rec.reasoning}
{'='*70}
⚠️ 免责声明:本报告由AI生成,仅供参考,不构成投资建议。
预测市场具有高风险,请基于自身判断谨慎决策。
{'='*70}
"""
return report