From cc79650769264bd7c80d6f52e7d3da42f5578f95 Mon Sep 17 00:00:00 2001 From: gavindiaz Date: Sun, 12 Jul 2026 09:18:28 +0800 Subject: [PATCH] first commit --- .dockerignore | 13 + .env.example | 68 ++ .gitignore | 47 ++ Dockerfile | 21 + Makefile | 43 ++ README.md | 525 +++++++++++++ data/signals.db | Bin 0 -> 958464 bytes data/signals.db-shm | Bin 0 -> 32768 bytes data/signals.db-wal | Bin 0 -> 383192 bytes deploy.sh | 54 ++ docker-compose.yml | 31 + docs/examples/sample_briefing.md | 84 +++ docs/examples/sample_report.md | 139 ++++ docs/examples/sample_terminal.txt | 54 ++ pyproject.toml | 49 ++ ...Iran_diplomatic_meeting_by_June_30_2026.md | 143 ++++ scripts/telegram_auth.py | 42 ++ setup.sh | 220 ++++++ src/__init__.py | 1 + src/config/__init__.py | 4 + src/config/settings.py | 95 +++ src/dashboard.py | 224 ++++++ src/db/__init__.py | 4 + src/db/database.py | 404 ++++++++++ src/main.py | 604 +++++++++++++++ src/models/__init__.py | 13 + src/models/anomaly_signal.py | 94 +++ src/models/decision.py | 59 ++ src/models/leading_signal.py | 124 ++++ src/models/market.py | 44 ++ src/models/trade.py | 240 ++++++ src/prompts/__init__.py | 4 + src/prompts/volatility_analyzer.py | 222 ++++++ src/prompts/whale_analyzer.py | 315 ++++++++ src/services/__init__.py | 14 + src/services/anomaly_detector.py | 391 ++++++++++ src/services/anomaly_history.py | 147 ++++ src/services/coingecko.py | 169 +++++ src/services/congress.py | 205 +++++ src/services/daily_briefing.py | 390 ++++++++++ src/services/ddg_search.py | 62 ++ src/services/defillama.py | 340 +++++++++ src/services/etherscan.py | 319 ++++++++ src/services/fred.py | 180 +++++ src/services/llm_analyzer.py | 466 ++++++++++++ src/services/market_fetcher.py | 531 +++++++++++++ src/services/polygon.py | 157 ++++ src/services/price_monitor.py | 428 +++++++++++ src/services/resolution_tracker.py | 115 +++ src/services/rtds_client.py | 237 ++++++ src/services/serper_search.py | 96 +++ src/services/stats_engine.py | 90 +++ src/services/tavily_search.py | 141 ++++ src/services/telegram_search.py | 191 +++++ src/services/tools.py | 467 ++++++++++++ src/services/trade_monitor.py | 698 ++++++++++++++++++ src/services/trader_profiler.py | 81 ++ src/services/twitter_search.py | 220 ++++++ src/services/volatility_analyzer.py | 372 ++++++++++ src/services/web_search.py | 79 ++ src/utils/__init__.py | 4 + src/utils/http.py | 32 + src/utils/logger.py | 120 +++ 63 files changed, 10726 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 data/signals.db create mode 100644 data/signals.db-shm create mode 100644 data/signals.db-wal create mode 100644 deploy.sh create mode 100644 docker-compose.yml create mode 100644 docs/examples/sample_briefing.md create mode 100644 docs/examples/sample_report.md create mode 100644 docs/examples/sample_terminal.txt create mode 100644 pyproject.toml create mode 100644 reports/20260505/20260505_000000_BUY_9599USD_US_x_Iran_diplomatic_meeting_by_June_30_2026.md create mode 100644 scripts/telegram_auth.py create mode 100644 setup.sh create mode 100644 src/__init__.py create mode 100644 src/config/__init__.py create mode 100644 src/config/settings.py create mode 100644 src/dashboard.py create mode 100644 src/db/__init__.py create mode 100644 src/db/database.py create mode 100644 src/main.py create mode 100644 src/models/__init__.py create mode 100644 src/models/anomaly_signal.py create mode 100644 src/models/decision.py create mode 100644 src/models/leading_signal.py create mode 100644 src/models/market.py create mode 100644 src/models/trade.py create mode 100644 src/prompts/__init__.py create mode 100644 src/prompts/volatility_analyzer.py create mode 100644 src/prompts/whale_analyzer.py create mode 100644 src/services/__init__.py create mode 100644 src/services/anomaly_detector.py create mode 100644 src/services/anomaly_history.py create mode 100644 src/services/coingecko.py create mode 100644 src/services/congress.py create mode 100644 src/services/daily_briefing.py create mode 100644 src/services/ddg_search.py create mode 100644 src/services/defillama.py create mode 100644 src/services/etherscan.py create mode 100644 src/services/fred.py create mode 100644 src/services/llm_analyzer.py create mode 100644 src/services/market_fetcher.py create mode 100644 src/services/polygon.py create mode 100644 src/services/price_monitor.py create mode 100644 src/services/resolution_tracker.py create mode 100644 src/services/rtds_client.py create mode 100644 src/services/serper_search.py create mode 100644 src/services/stats_engine.py create mode 100644 src/services/tavily_search.py create mode 100644 src/services/telegram_search.py create mode 100644 src/services/tools.py create mode 100644 src/services/trade_monitor.py create mode 100644 src/services/trader_profiler.py create mode 100644 src/services/twitter_search.py create mode 100644 src/services/volatility_analyzer.py create mode 100644 src/services/web_search.py create mode 100644 src/utils/__init__.py create mode 100644 src/utils/http.py create mode 100644 src/utils/logger.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..98da910 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +venv/ +.env +.git/ +__pycache__/ +*.pyc +data/ +reports/ +daily_briefings/ +leading_signals/ +price_volatility/ +anomaly_signals/ +*.db +*.json.bak diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1543aa6 --- /dev/null +++ b/.env.example @@ -0,0 +1,68 @@ +# ============================================================ +# Polymarket Whale Watcher - 配置文件 +# ============================================================ +# 复制到 .env 并填入你的 API 密钥: +# cp .env.example .env +# +# 只需 LLM_API_KEY 是必填项,其余均为可选。 +# ============================================================ + +# --- 必填项 --- +# 任意 OpenAI 兼容的 API +LLM_API_KEY=sk-your-key-here +LLM_BASE_URL=https://api.openai.com/v1/ +LLM_MODEL=gpt-4o + +# --- 交易数据源 --- +# "official" = Polymarket 公共 API(无需认证,默认) +# "internal" = 私有 API(需配置 INTERNAL_API_URL + INTERNAL_API_KEY) +TRADE_API_MODE=official + +# 内部 API(仅当 TRADE_API_MODE=internal 时需要) +# INTERNAL_API_URL=http://103.197.25.170:18088 +# INTERNAL_API_KEY=your_internal_api_key_here + +# --- LLM 设置 --- +LLM_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai/ +LLM_MODEL=gemini-3-flash-preview +LLM_TEMPERATURE=0 + +# --- HTTP 代理(用于无法直连 Polymarket 的网络环境,如中国大陆) --- +HTTP_PROXY=http://127.0.0.1:7890 + +# --- 可选:搜索与数据 API(增强 LLM 分析能力) --- +# TAVILY_API_KEY= # 网页搜索 (https://tavily.com) +# TWITTER_API_KEY= # Twitter 社交情绪 +# SERPER_API_KEY= # 网页搜索备用 (https://serper.dev) +# POLYGON_API_KEY= # 股票、外汇数据 (https://polygon.io) +# FRED_API_KEY= # 美国经济指标 (https://fred.stlouisfed.org) +# ETHERSCAN_API_KEY= # 链上数据 (https://etherscan.io) +# CONGRESS_API_KEY= # 美国立法动态 (https://api.congress.gov) + +# --- 可选:Telegram 频道监控 --- +# TELEGRAM_API_ID= +# TELEGRAM_API_HASH= +# TELEGRAM_SESSION_STRING= +# TELEGRAM_CHANNELS= + +# --- 鲸鱼检测参数调优 --- +MIN_TRADE_SIZE_USD=1000 +MIN_PRICE=0 +MAX_PRICE=0.7 +FETCH_INTERVAL_SECONDS=15 +TRENDING_MARKETS_LIMIT=50 + +# --- 邮件提醒(可选) --- +EMAIL_ENABLED=false +# EMAIL_SMTP_SERVER=smtp.qq.com +# EMAIL_SMTP_PORT=465 +# EMAIL_SENDER= +# EMAIL_PASSWORD= +# EMAIL_RECIPIENT= + +# --- 交易执行(默认关闭,谨慎使用) --- +ENABLE_TRADE_EXECUTION=false +# POLYGON_WALLET_PRIVATE_KEY= + +# --- 日志 --- +LOG_LEVEL=INFO \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..44a80a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Environment variables +.env + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +venv/ +ENV/ +env/ +.venv/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Reports (optional - uncomment if you don't want to track reports) +# reports/ + +# Data files +data/*.json +data/*.csv diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1d9eb42 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY pyproject.toml . +RUN pip install --no-cache-dir . + +# Copy application code +COPY src/ src/ + +# Create data directories +RUN mkdir -p data reports daily_briefings + +# Default command +CMD ["python", "-m", "src.main", "run"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7eb3e43 --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +.PHONY: setup run dashboard briefing markets test lint clean docker-build docker-run help + +PYTHON = python3 +VENV = venv +PIP = $(VENV)/bin/pip +PY = $(VENV)/bin/python + +help: ## Show this help + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +setup: ## One-click setup (venv + dependencies + .env) + @chmod +x setup.sh && ./setup.sh + +run: ## Start the whale watcher + $(PY) -m src.main run + +run-debug: ## Start with debug logging + $(PY) -m src.main run --debug + +dashboard: ## Start the web dashboard + $(PY) -m src.main dashboard + +briefing: ## Generate today's briefing + $(PY) -m src.main briefing --today + +markets: ## Show trending markets + $(PY) -m src.main check-markets --limit 20 + +test: ## Run tests + $(PY) -m pytest tests/ -v + +lint: ## Run linter + $(VENV)/bin/ruff check src/ + +clean: ## Remove generated files (keep data) + find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true + find . -type f -name '*.pyc' -delete 2>/dev/null || true + +docker-build: ## Build Docker image + docker build -t polymarket-whale-watcher . + +docker-run: ## Run in Docker + docker run --env-file .env -v $(PWD)/data:/app/data -v $(PWD)/reports:/app/reports polymarket-whale-watcher diff --git a/README.md b/README.md new file mode 100644 index 0000000..b40c398 --- /dev/null +++ b/README.md @@ -0,0 +1,525 @@ +
+ +# 🐋 Polymarket Whale Watcher + +**AI-Powered Whale Trade Intelligence for Polymarket Prediction Markets** + +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-3776AB.svg?style=for-the-badge&logo=python&logoColor=white)](https://www.python.org/downloads/) +[![License: MIT](https://img.shields.io/badge/License-MIT-22c55e.svg?style=for-the-badge)](LICENSE) +[![Polymarket](https://img.shields.io/badge/Polymarket-API-8B5CF6.svg?style=for-the-badge)](https://docs.polymarket.com/) +[![LLM](https://img.shields.io/badge/LLM-OpenAI_Compatible-412991.svg?style=for-the-badge&logo=openai&logoColor=white)](https://platform.openai.com/) + +
+ +**Real-time monitoring** of 700+ markets  ·  **14 autonomous research tools**  ·  **Multi-step deep analysis**  ·  **Signal accuracy tracking** + +
+ +[Quick Start](#-quick-start)   |   [How It Works](#-how-it-works)   |   [Sample Report](#-sample-report)   |   [Configuration](#%EF%B8%8F-configuration)   |   [Dashboard](#-dashboard) + +--- + +pipeline + +
+ +
+ +## What It Does + +Whale Watcher continuously monitors **700+ active Polymarket markets** across three volume tiers, detects large trades with anomalous patterns, and deploys an LLM agent with **14 autonomous research tools** to conduct multi-step deep investigations. Each whale trade undergoes a structured 7-step analysis pipeline — from trader profiling and cross-market position mapping to information gap assessment — producing an **Information Asymmetry Score** that quantifies the likelihood of non-public information advantage. + +
+ +## Live Demo + +
+Terminal Output — Real-time whale detection and analysis + +
+ +``` +$ python -m src.main run + +============================================================ +WHALE WATCHER STARTED +============================================================ +Monitoring: 765 markets +Interval: 10 seconds +Min Trade Size: $10,000 USD +Price Range: 0.1 - 0.9 +============================================================ + +Tiered monitoring: Tier1=8 (>500K), Tier2=198 (>10K), Tier3=559 (>1K) + +[23:41:12] WHALE TRADE DETECTED! + Amount: $9,600.00 USDC + Side: BUY Yes + Price: 0.7142 + Market: US x Iran diplomatic meeting by June 30, 2026? + + Generating analysis report... + Round 1: LLM requested 3 tool call(s) + → search_web("US Iran diplomatic meeting June 2026") + → search_twitter("US Iran meeting diplomacy") + → get_wallet_transfers("0xceza...rn132") + Round 2: LLM requested 2 tool call(s) + → search_web("Islamabad Iran talks Witkoff April 2026") + → search_twitter("POLYMARKET Iran meeting odds fading") + Round 3: LLM requested 1 tool call(s) + → search_web("Iran FM Araghchi 3 phase deal proposal") + + Analysis complete after 3 round(s) + Information Asymmetry Score: 0.32 (LOW) + Trader Credibility: MEDIUM (#1733, PnL: $84K) + Verdict: Thesis continuation / loss recovery — HOLD/PASS + Report saved: reports/20260504/... +``` + +
+ +
+ +
+Sample Report — 7-Step Deep Analysis (click to expand) + +
+ +Each whale trade generates a comprehensive markdown report with structured multi-step analysis: + +> **Full example**: [docs/examples/sample_report.md](docs/examples/sample_report.md) + +#### Report Structure + +``` +====================================================================== +# Whale Trade Analysis Report +====================================================================== + +┌─ Trade Summary ─────────────────────────────────────────────────────┐ +│ Market, trade size, direction, price, odds, time, trader rank │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─ Step 2: Trade Signal Analysis ─────────────────────────────────────┐ +│ Trader profile: rank, PnL, avg size, large trade ratio │ +│ Domain expertise detection, trade timing analysis │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─ Step 3: Event-Related Position Analysis ───────────────────────────┐ +│ Cross-market positions, roll-forward detection │ +│ Loss recovery patterns, hedge identification │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─ Step 4: Market Long/Short Analysis ────────────────────────────────┐ +│ Top 5 bulls & bears with rankings and PnL │ +│ Smart money consensus assessment │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─ Step 5: Information Gap Analysis ──────────────────────────────────┐ +│ Public information audit (web, Twitter, Telegram) │ +│ Market pricing efficiency check │ +│ Non-public information evidence search │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─ Step 6: Historical Pattern ────────────────────────────────────────┐ +│ Trader's past bets on related events │ +│ Strategy pattern recognition (laddering, hedging, etc.) │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─ Step 7: Information Asymmetry Assessment ──────────────────────────┐ +│ Score (0–1), trader credibility, evidence, reasoning │ +│ Multi-factor summary table with signal strength │ +│ Recommended action: BUY / HOLD / PASS │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +#### Example Summary Table + +| Factor | Assessment | Signal | +|--------|-----------|--------| +| Trader Rank/PnL | Rank #1733, $84K PnL | Moderate | +| Trade Size vs. Normal | ~$9.6K vs. avg $10K | Routine — neutral | +| Related Position | Heavy loser on May 15 market (-$6.5K) | Suppresses signal | +| Domain Expertise | Iran geopolitics specialist | Supportive | +| Public Info Coverage | Extensive public news | Reduces asymmetry | +| Smart Money Bulls | Rank #278 also long | Modest support | +| **Overall** | **Thesis continuation, not insider signal** | **Low-Medium** | + +
+ +
+ +
+Daily Briefing — Automated intelligence summary + +
+ +Daily briefings are generated at **10:00 AM local time** and emailed automatically. + +> **Full example**: [docs/examples/sample_briefing.md](docs/examples/sample_briefing.md) + +**Includes:** +- High-confidence signals (IAS >= 60%) with full analysis summaries +- Fallback: top 5 signals by score if none reach the threshold +- Abnormal price volatility alerts +- Historical signal performance — win rate and ROI by confidence tier + +
+ +
+ +--- + +## Quick Start + +### One-Click Setup + +```bash +git clone https://github.com/chaoleiyv/polymarket-whale-watcher.git +cd polymarket-whale-watcher +chmod +x setup.sh && ./setup.sh +``` + +The setup script will: +1. Check Python 3.10+ is installed +2. Create a virtual environment +3. Install all dependencies +4. Create `.env` from template + +Then add your API key and start: + +```bash +# Add your LLM API key (the only required key) +echo "LLM_API_KEY=your_key_here" >> .env + +# Activate the environment and run +source .venv/bin/activate +python -m src.main run +``` + +> **Get an API key**: [OpenAI](https://platform.openai.com/api-keys) · [Google AI Studio](https://aistudio.google.com/apikey) (free tier) · [DeepSeek](https://platform.deepseek.com/) + +### Docker + +```bash +docker build -t whale-watcher . +docker run --env-file .env -v ./data:/app/data -v ./reports:/app/reports whale-watcher +``` + +--- + +## How It Works + +```mermaid +flowchart LR + A[Polymarket API] --> B[Market Fetcher] + B --> C[700+ Tiered Markets] + C --> D[Trade Monitor] + D --> E{Whale\nTrade?} + E -->|No| D + E -->|Yes| F[Anomaly Detector] + F --> G{Score >= 0.65?} + G -->|No| D + G -->|Yes| H[LLM Analyzer] + H --> I[14 Research Tools] + I --> J[Signal + Report] + J --> K[Resolution Tracker] + K --> L[Dashboard + Email] +``` + +### Pipeline + +| Stage | What Happens | +|-------|-------------| +| **1. Market Selection** | Fetches all active markets from Polymarket Gamma API, classifies into 3 tiers by 24h volume, adds token launch markets. Refreshes every 15 minutes. | +| **2. Trade Monitoring** | Parallel async tasks per market (700+), polls official Polymarket data-api for new taker BUY trades, deduplicates by transaction hash. Connection pool: 50 connections, 120s timeout. | +| **3. Whale Pre-filter** | Price range 0.10–0.90, $5K hard floor, dynamic threshold scaled by volume ($5K–$100K), conviction check (must pay above mid), resolution window 6h–90d. | +| **4. Anomaly Scoring** | 5-factor model (max 1.0): base confidence (0.50) + premium ratio (0.20) + signal cleanliness (0.10) + depth ratio (0.10) + cluster tier (0.10). Threshold: >= 0.65. | +| **5. LLM Investigation** | Builds rich context (trade + trader profile + event positions + market top holders + historical signals). LLM autonomously selects tools for up to 3 rounds. Produces structured 7-step analysis with information asymmetry score (0–1). | +| **6. Signal Tracking** | Resolution tracker checks every 30 min, validates signal correctness, computes theoretical ROI. Daily briefings at 10:00 AM, emailed to recipients. | + +--- + +## Features + +| Feature | Description | +|---------|-------------| +| **Tiered Market Monitoring** | 700+ markets across 3 tiers: Tier1 (>$500K, 15s), Tier2 (>$10K, 60s), Tier3 (>$1K, 300s) | +| **5-Factor Anomaly Detection** | Premium ratio, signal cleanliness, depth ratio, cluster signals, base confidence | +| **7-Step Deep Analysis** | Trade signal → Event positions → Long/short mapping → Info gap → Historical pattern → Asymmetry score | +| **14 Autonomous Research Tools** | Web, Twitter, Telegram, crypto, DeFi, stocks, on-chain, legislation | +| **Cross-Market Position Analysis** | Detects roll-forwards, hedges, and loss recovery patterns across related markets | +| **Signal Accuracy Tracking** | Auto resolution checking every 30 min, win rate stats by confidence tier | +| **Daily Briefings** | 10:00 AM automated summary with high-confidence signals, emailed to recipients | +| **Real-time Email Alerts** | Instant notifications for high information-asymmetry signals (>= 60%) | +| **Web Dashboard** | FastAPI-based signal performance dashboard with ROI breakdowns | + +### 14 LLM Research Tools + +The LLM agent autonomously selects and chains these tools during its multi-round investigation: + +| Category | Tools | Use Case | +|----------|-------|----------| +| **Social & Sentiment** | `search_twitter` · `search_telegram` · `search_web` | Public sentiment, insider chatter, news coverage | +| **Crypto & DeFi** | `get_crypto_price` · `get_crypto_market_overview` · `get_protocol_tvl` · `get_token_unlocks` · `get_protocol_revenue` | Token prices, TVL, unlocks, protocol health | +| **Financial Data** | `get_stock_price` · `get_stock_news` · `get_economic_data` | Equities, ETFs, macro indicators | +| **On-Chain** | `get_wallet_transfers` · `get_contract_info` | Wallet activity, contract deployments | +| **Legislation** | `get_bill_status` · `get_recent_legislation` | US bills, regulatory actions | + +--- + +## Sample Report + +Every whale trade produces a structured multi-step report. Here's a condensed view: + +``` +====================================================================== +# Whale Trade Analysis Report +====================================================================== + +Trade Summary + Market: US x Iran diplomatic meeting by June 30, 2026? + Size: $9,600 USDC | Direction: BUY Yes (71.4%) | Trader: #1733 + +Step 2: Trade Signal Analysis + → Mid-tier trader, $84K PnL, Iran geopolitics specialist + → Trade size ($9.6K) matches avg ($10K) — routine, not exceptional + +Step 3: Event-Related Position Analysis ← KEY FINDING + → Losing -$6,508 on earlier "May 15 meeting" market (15.5% odds) + → This trade is a thesis roll-forward, not a fresh insider bet + +Step 4: Market Long/Short Analysis + → Biggest Yes holder is a chronic loser (PnL: -$5.5M) — red flag + → One elite trader (Rank #278) also long — modest support + +Step 5: Information Gap Analysis + → All supporting info widely reported in mainstream media + → Market at 69.5% — already fairly priced + +Step 6: Historical Pattern + → "Timeline ladder" strategy across multiple Iran-related deadlines + +Step 7: Information Asymmetry Assessment + → Score: 0.32 (LOW) | Credibility: MEDIUM + → Verdict: Thesis continuation / loss recovery — HOLD/PASS + +====================================================================== +``` + +> **Full report**: [docs/examples/sample_report.md](docs/examples/sample_report.md) + +--- + +## Configuration + +Copy `.env.example` to `.env` and configure: + +### Required + +| Variable | Description | Get It | +|----------|-------------|--------| +| `LLM_API_KEY` | LLM API key for analysis (OpenAI-compatible) | [OpenAI](https://platform.openai.com/api-keys) / [Google AI Studio](https://aistudio.google.com/apikey) | + +### Optional (enhances analysis quality) + +
+Data Source API Keys + +| Variable | Description | Get It | +|----------|-------------|--------| +| `TAVILY_API_KEY` | Web search (primary) | [tavily.com](https://tavily.com) | +| `SERPER_API_KEY` | Web search (fallback) | [serper.dev](https://serper.dev) | +| `TWITTER_API_KEY` | Twitter sentiment search | [twitterapi.io](https://twitterapi.io) | +| `POLYGON_API_KEY` | Stock/ETF/forex data | [polygon.io](https://polygon.io) | +| `FRED_API_KEY` | Economic indicators | [FRED](https://fred.stlouisfed.org/docs/api/api_key.html) | +| `ETHERSCAN_API_KEY` | On-chain wallet analysis (Polygon V2) | [etherscan.io](https://etherscan.io/apis) | +| `CONGRESS_API_KEY` | US legislation data | [congress.gov](https://api.congress.gov/) | +| `TELEGRAM_API_ID` / `TELEGRAM_API_HASH` | Telegram channel monitoring | [my.telegram.org](https://my.telegram.org) | + +
+ +
+LLM Settings + +| Variable | Default | Description | +|----------|---------|-------------| +| `LLM_MODEL` | `gemini-3-flash-preview` | Model name (any OpenAI-compatible) | +| `LLM_BASE_URL` | Google AI endpoint | OpenAI-compatible API base URL | +| `LLM_TEMPERATURE` | `0` | LLM temperature | + +
+ +
+Whale Detection Tuning + +| Variable | Default | Description | +|----------|---------|-------------| +| `MIN_TRADE_SIZE_USD` | `10000` | Minimum trade size to consider | +| `MIN_PRICE` / `MAX_PRICE` | `0.10` / `0.90` | Price range filter | +| `FETCH_INTERVAL_SECONDS` | `10` | Default polling interval | + +
+ +
+Tiered Market Monitoring + +| Variable | Default | Description | +|----------|---------|-------------| +| `FULL_MARKET_SCAN` | `true` | Enable tiered monitoring (all active markets) | +| `TIER1_VOLUME_MIN` | `500000` | Tier 1 volume threshold | +| `TIER2_VOLUME_MIN` | `10000` | Tier 2 volume threshold | +| `TIER3_VOLUME_MIN` | `1000` | Tier 3 volume threshold | +| `TIER1_POLL_INTERVAL` | `15` | Tier 1 polling interval (seconds) | +| `TIER2_POLL_INTERVAL` | `60` | Tier 2 polling interval (seconds) | +| `TIER3_POLL_INTERVAL` | `300` | Tier 3 polling interval (seconds) | + +
+ +
+Email Alerts + +| Variable | Default | Description | +|----------|---------|-------------| +| `EMAIL_ENABLED` | `false` | Enable email notifications | +| `EMAIL_SENDER` | — | Sender email address | +| `EMAIL_PASSWORD` | — | Sender email password (app password) | +| `EMAIL_RECIPIENT` | — | Comma-separated recipient emails | + +
+ +--- + +## Commands + +```bash +# Core +python -m src.main run [--debug] # Start monitoring +python -m src.main check-markets --limit 20 # View trending markets + +# Analysis +python -m src.main test-analyze # Test LLM on a specific market + +# Reports +python -m src.main briefing --today # Generate today's briefing +python -m src.main briefing --date 2026-04-17 # Briefing for a specific date + +# Dashboard +python -m src.main dashboard --port 8000 # Start web dashboard + +# Maintenance +python -m src.main migrate # Migrate legacy JSON to SQLite +``` + +--- + +## Dashboard + +```bash +python -m src.main dashboard +# Open http://localhost:8000 +``` + +The dashboard shows: +- Overall signal statistics (total signals, win rate, avg ROI) +- Performance breakdown by confidence tier +- Top best/worst signals by theoretical ROI +- Paginated signal history + +--- + +## Architecture + +``` + ┌──────────────────────────────┐ + │ Polymarket Gamma API │ + │ (all active markets) │ + └──────────────┬───────────────┘ + │ + ┌──────────────▼───────────────┐ + │ Market Fetcher │ + │ Tier1: >$500K (15s poll) │ + │ Tier2: >$10K (60s poll) │ + │ Tier3: >$1K (300s poll) │ + └──────────────┬───────────────┘ + │ + ┌────────────────────▼────────────────────┐ + │ Trade Monitor (async, 700+) │ + │ Official Polymarket data-api │ + │ Per-market parallel tasks │ + │ Pool: 50 connections, 120s timeout │ + └────────────────────┬────────────────────┘ + │ + ┌────────────────────▼────────────────────┐ + │ Pre-filter + Scoring │ + │ $5K+ size, 0.10-0.90 price, conviction │ + │ 5-factor anomaly score >= 0.65 │ + └────────────────────┬────────────────────┘ + │ + ┌────────────────────▼────────────────────┐ + │ LLM Analyzer — 7-Step Pipeline │ + │ 14 tools · up to 3 rounds │ + ├──────────┬────────┬────────┬────────────┤ + │ Twitter │ Web │ DeFi │ On-Chain │ + │ Telegram │ Search │ Crypto │ Legislation│ + └──────────┴───┬────┴────────┴────────────┘ + │ + ┌──────────────▼─────────────────────────┐ + │ Signal Storage (SQLite) │ + │ → Resolution Tracker (every 30min) │ + │ → Daily Briefing (10:00 AM + email) │ + │ → Email Alerts (IAS >= 60%) │ + │ → Dashboard (FastAPI) │ + └────────────────────────────────────────┘ +``` + +--- + +## Project Structure + +``` +src/ +├── config/settings.py # Environment configuration (Pydantic) +├── models/ # Data models +│ ├── market.py # Market, TrendingMarket +│ ├── trade.py # TradeActivity, WhaleTrade, TraderRanking +│ ├── decision.py # TradeRecommendation, LLMDecision +│ └── anomaly_signal.py # AnomalySignal (stored signal) +├── services/ # Business logic +│ ├── market_fetcher.py # Polymarket Gamma API (tiered market selection) +│ ├── trade_monitor.py # Per-market parallel monitoring (official API) +│ ├── anomaly_detector.py # 5-factor anomaly scoring +│ ├── llm_analyzer.py # LLM with tool-use (14 tools, 3 rounds) +│ ├── tools.py # Tool registry +│ ├── resolution_tracker.py # Market resolution checking +│ ├── stats_engine.py # Performance statistics +│ ├── daily_briefing.py # Daily summary generation + email +│ ├── twitter_search.py # Twitter API search +│ ├── telegram_search.py # Telegram channel monitoring +│ ├── web_search.py # Tavily/Serper/DuckDuckGo search +│ ├── coingecko.py # Crypto prices and market data +│ ├── defillama.py # DeFi TVL, revenue, token unlocks +│ ├── fred.py # FRED macroeconomic data +│ ├── polygon.py # Stock/ETF prices and news +│ ├── etherscan.py # On-chain data (Polygon, Etherscan V2) +│ └── congress.py # US legislation data +├── prompts/ # LLM system prompts +│ ├── whale_analyzer.py # Whale trade analysis prompt +│ └── volatility_analyzer.py # Price volatility analysis prompt +├── db/database.py # SQLite signal storage +└── main.py # CLI entry point (Typer) +``` + +--- + +## Disclaimer + +This system is for **research and educational purposes only**. Prediction market trading involves significant risk. The information asymmetry scores and analyses are AI-generated estimates — not financial advice. Always conduct your own research and verify independently before making any trading decisions. + +--- + +
+ +**MIT License**  ·  Built with Polymarket API + LLM + +
\ No newline at end of file diff --git a/data/signals.db b/data/signals.db new file mode 100644 index 0000000000000000000000000000000000000000..debd78382cd2a5c6041c9fa292fc8484df28e0d4 GIT binary patch literal 958464 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
;F;~Ztq=0n#RHDQ-+k-Au{Rkr#lT5S{r8>~ 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ĮSwNUdLjel-;TX)_ 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 z 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

%Dn z5p(!+HnLC&(SPS~x44@&& zmpucjh6LP#9$+;u_!m-a2#vTfC=Ls$k&=K$Qo*zO$il%{(6kA1DBTu0DT!SbG-ENz zlC1ZD^_3?dkOlZrpiAIPnzo4zT9EBido}t)cw#+LSS73vvQFX7jl=wB#6O0YY4}uV zn~iEKs@bH56PkUSE|kZ=FaRs>MPOEI1d-KO!2AXu93wB-WFZ2>(KKV%5RrmdM8ZRT zj4X_(cLpISjqc412--?*Zv|ZB^-x=1!7UkN^a}y%D{vmEiHgPjg6&_+G#R)MrnP*` zpjbc`>O-0hLWzMKSkn9L0hg&6#_auql_H^`lf?D6v7tS@P+I~S;QZT-V*+ss<~>;r z9xqW-s2QP|K0ozsqM#E|Ong5~H)FnLO#I4w5;&K)zgawcS>iSo=T&a4BMOuVPwz6c zGv3QvEjaD6SQ2Y8+A-SOh_qop&aDzA@Nj#WB5 zS~~nTCncgME1<>e(0xZ#kF9~M@nd@%(Ey|gq*vU<$snY0MU=Av0j}wx!TC@KtjtcQWIQ&YwUJLkzV1lq06Z_vS z-a4*v?h?a}?v)+G{^8O=HW`;oJBQ0R&yHU^Mj)*7337?imu2k_@Bx|xFYOGF_iSId z4fM!@c~E}m9SNru@Ln2pb+->ycx_kt+OQ74HR@hYN=$z67Ikv+3*{U8b;zynfxaJB zOvcxWSWx~o-9e8p=hu6_;<+2e8`t#Ev$Y9^dsAtP+M(#LN9F5UNzm`xDDnc=$8R42 zZcuu6)5MsF7gPlj&s_`0OK%HhBZen~@?w;H%;*?g1f7pD_c$p*{_}W%BBKr!{3S&G z6A20?CYfB?`%v-cw|(oI53JVp|DKMI_xy;W|MqFyBhVfJe*_Nv^l;>}!*ex$KSI{ZL92l0+t0^;aMex*Ifjl1a#A9nc3EaXWNQcg4_(M6IZRw|oI zNNy~aiRYk0J5DDH;)e|%n2k0UzCZU%5N@kAJ0c!EJLj4inzJD`Nj8xhWF1jAySo{c=1ik0w}N+Bx` z`&v+fW&n}%;wW(Uv*WDKfXxDF0R|ziN}XvG-p~%pb>@kTI_X%vQ|>De5KI!WLw3{b z!M?p(G}%4~USX1Gg+eW2Q$!;%APKUOYm8Rp5X!XXAyY>yYOlSpPvV282?p+4_|*Nr z6x-vEK86%q+LjHVVCpH2tKBz!YL=+gyYr-kG+$ZvV8e#LS^0$v@J)T*87e~YCDj9| zVK#zo?m$6KMJ~bFgX~-&sGbO=;Z=`ye<#mf>mf0NvF2k=wh%?|j6df?52YW$G zQyZ~Nf7%8qpoUU0VjqZR+AYo9;mHYpo&`jI$XSH#6RJ5&ivx=kXo#tvH}hw_v2oN< z8IIOfVlijz68Kku6&K)mFmqbJGbZI35C2~{LF7P? zXCwe0=(TzP`+#<+zT(vFY3#np%LBqN+S2&=kR>Be3$V->0a5D=+H>d6j$qN40!Awu zo*fbpGy)9(bLysf;3CWka6~C4ztN@-a92`-wn-Unm}Dei3-S0&ivF1W5g{vTw5D-H zNYi#j5k07Rsnz9VrvLn!FZ8ab$V-1+$b;y1WVf0g@R+^~DQo{a{`}AUK%8 zFw6duYk{_~bC;sA<%eqEhQlf9{_t8d$*U=^Y#KudH!Mi)V7YSY?| zuIf}auhp&uT)Xt%6|n_WI1U+ARUXp$1MRA)vjygOQ4tBy8B{lXKY{ZHaR;ot_O@RE z0<$r8g_cO0cb^jr0%2a%)In17qT~qJ`cWaU<0}6D8z8^ z3w=HXy2yo<;=1{)D?+H52q+)Ws5ZJb9wJ8(635xJum|I*kARTTtn{Ps{ri5OIQ*gF zk6UklC)zjvAO6L^5kY)`6JXb(0Wno;%`#6FST z0ZrozNj1yAP7Y%9?E%8%0MTXT%8~M^F+l=C-5(o?*u@iXYAk?+z(j4+&lTEp#hoLbs+ns7Uk zC|msyIOg?_~QhCo- zLT}DPFs#2WV1vWgwPv5S#LE@Fcf7}fY5{zZZ%GDW$2lR=Uq8he%4@Gu`##gT)S&?@0iEQWd5|B~gz?`4d%xgysRoxc-%=~+193{&y?VGT(r9(vV;Sda}V zZ$DpEdC=&POK)OrK)$1Ret2T{uwX84?t-Ks=T!L%L1gX+Qn^8=0Hn=3Bn419GBkeU zgwB`*NC4sgrvM1dBe4q54&a}P|3C5T9sIOE?Gb2?KzjscaRl~#X4t8fZbaU`^VLt! zS^vf1FV;&prXsmqE|K<77eo>v;;@xL0gBpF+Og9xelTBy*^@%?-A=k7g%fT(mVrN) z_Tq_r9_A0ljE}&M2U^vg_oMLr*!R?q2k67!jC^+fpZs91NH;3=`*;>245AWIK;)nq z^6Yl9RMWC)H=FQqdJ}g%H)97F2ow=wm^D#EQhcBYT(1{cZDffhPoo0Dl|J*qkt$Ni0Wm+^1DJr)VEk)l@e%(uGZJidD(84 z3x^bIu(s?h^LVxko*Pj`r9*GQ8Bmyrl?W5``05G5LCFFvukBa$3aZ5Tm7apgEcj-0 z_bqz%eoN#XmF~i-UQ{0YJ$InrS8kxQlZ>`MVs$6>2LusFB=1*oJ{MAScL7;F@a^Ja zS2K!DFbQG{I0Hbnh#Yi*AohU(B03P3mklno>iBbyFZlye8j2@~xgY{-02|b( zPA?LA=6i2Sc%=2Vnv713Nn2yvSSJutpbcgLouXG=1)cVY-vUocudp6^mMamG#msp1 z96^4xU~BPwDP>YM>-BV&O||mWK#jDqg;BINrUE#i}K_?o-PN(HW<9(0g9wxD7(H$uuVT7Wd1f~z! z&uTmTch!s5SNyl#GoFacAUfOauHh9`JJFFGLXl^R>$4!$U{RC?kgo&q0lSUQ1zVd| zSOZ2E!9exUWbcdR-VhK`p?J&G`rl1{$Q;!!GN;?dfq0w@KqJzpUM{%0Kq@4(m5z9Wqv>Q)@BU=@=t9jAKEMZ22gSR zl%xiB>?g=?9pTrUq}K5vjP^BtLdX0%yr)%uJ|ggE)Nl$Js4K@N4({Nhb@4y?TF8oE zK9aOm{r|4r9sIOE?GgA$jKIMa!|@uy$h+=#*G7(a&KdpKx?igojKs*!P9!p@A(9Nv zrx3eGQ$88T1l`MJNpacPs2j<}QrS#8lalDw$tE&6sHv#rJ6j5D z$v>39^oQl&`_ny}zCCDbdVDrZr*kZ2io-p^(T}H}GER`Z;r|XNubi(c;6Cs|bcpJfPoKAIERi$c3zqN9qk9b*cl^7^% zQt8s(zR*bg#TwD(P_B)FZ2ti^f5TRe;Vj1RA*vdY=Ow`_24)m_J+|{cs z?E6gH@8C<%dHvn!d}$A|zTOO=MQWo`ZN-YcfW9e)a`OM^vXYB#uuuyApt`wW^=cf+ zebFx88jxG2O#Hf~ENq-9tkz9MgjZT!ST8G=?gyz3NJSSvfC1t8_sck#BxQ7=`j&IB zs;pCU>+YrG)@R|V#y{m$QY8(H0r~EdP!p`hJ$ho0wmWun<~s9y`c-13GVVh?6*0P| z{zB3VU~Y^8UxL(_QLCHTIMh~15aM#M_BM(^jWpN@q-k6(5V#0r(2W9(l5nV@21}te z7|%X$xxh~I)cWOq`1SZkM{pR|wy1*}?P!STP?M;uh0!kLx2V9`lzx#YuNLBTZY1!c z6d_(E{184Vy@3=a<<-D5QUvyCqkl^WRy5XDodi|M)&)bU?>DN1#g)KE3SnWuC3vXM zN<;PJcI>7tjkGK+R$EHVoR7)DN3xd^JRbIu7=&YiBnNkR=1)-{27r^%;?-a#r`tqh z#zB@(+G&jJY>C}WZvJ#11bJYkOQxEFdJtOs$QdJMG)J#Z@;5XUj3Phpqe)eun?C~% z7IjX#yt~)b+d0`icJE`fpXG+AOa7&!1Ip8_~By`g8 ztKmQLQ#WO_~M=D#QQA0tL9T^>a#S6{R^a-pI!Fkcz0>jmeTgq zAj+|p=L}pcRQodph&fgHtzoLj61u_&IJmR2`=%ZZi09?g+d_aoI){=nIJ5!Wy$0!9 z;!cEq-T2kbTBn|q{b1;D@%#?`f3XA{zj^ovLq}*`$Zs#cwvJ)ewglQzPv`^sj*acF zoIOFzJ#qXRosb)%Zd9lcP(F5&^nmWPaPe;q(=A;7Dz`Tlw~s|*a%D6Mki=I2`^v_P z#q+zYh;5bM*=>D3Vz2D{d@Q+AG69v_N6TYpP!jF8f@1%nXqQzts@8KWye|(Hyn$y6 z{VOeo2k&AwEV-{0dU6Zh)tD%ETP8U|8~OPO4MjWP#sMG*C7zLvE??X%Sb?G8x;Ych z7GxN-?*Yw}Q(f2yp`jtPcN~%S$2E(UL!R_bl%(v~a}gxY>m`oA^FOXH{rbmM{QvP| z9sIOE?Gb2?z(;EY_W#;&LQ)}*n+oKSe5M?QJ zy`4NU1uEpZL`3W~sM+KBm(S-BbxLA#;bxHhPRDXd>izKMVqUz{VMFJij$IQGoovN1 z672T?=&ImUODkiX;ifUEMuS^rT+*p*HUS0EMRN)U9+=OJ7k7d0r&GZ8^SB;*NhAyT z4_ACE(8tctQfmv}PpypP<66Q!|oC=wGe=HA%-W`YA*MIi3NfX4pL~7H| zg(z`IllE$Y%oBtYaF3y(*iOn+;`;yDd-EVUuKUijMaqUw%L?0J%U(-qiZaL$yOo)h zS(OE^Rs_LY1PKwKWO?LJWMyUnB>-KRK1hOE+h_od8{Ghj`ydDsAixU%NgR!C9JAJp zy)(0>X2KpjcE;netghN8tb(<;l%x;j!kXn-h_5yJw^I7GXU+Fo z6(SMj@6qqiwEWqs5KY!Yexwa3rPB9P|M(|YAp&myAAj)BnO}VF2RixGh;l6ipvCNC z=|5cg`reRidovyHvsp=dvQ1r~NRjqZGRO+=Cmnj4_rT}tdj}`>>{1#30m$sNGcS$~ zjo>4Q8lfW2@1V^-w&!%3`i>twRO{Q!U)D}uO|AOIFYE!>PBCC&|9L4AP+8c2QD@~N zJH=ja_*KKD$e`;9QrNhDSS0ooA6^$BnntN|``z^;$25{bW9bFhfC}J=LtKeoIv2yA z508M_md*8@FH>ir3^VrTtF>KQYG($=Ppz-N`*Quv1-+CLuM9v)kitv`eNcaCsCIpW zcpUb} z4{&6wSR15OC5=I#48!qg1&(h=1ThgrpaP^SF;tSq&u$vMeg-7N)FMG~fm7^&S%P?{ z=Kue3+lT-7CoA!3ji$8%trcjkz)h~eKDVzR42Op@b&`$0e0cu6{_ehio-!Qh&QK=t zDU%e@ODckPaQqUr3A+SW)%P*^bmaeVMYC{TD&@gU*a1Bg2=-Qqk6@@~m-6jE5JXyu zvrTEX$Nfzyy;39z#m+4df6*{s_Au(i85)P`-t@HU$&Ns=n=p zkof(A2ZUpe##(uA!QV=eWb_lif7hKKfA-#?+P{!xl_E*OwNRIeN@8nD)tIV~8-(Se ziL0uQ!-88DN4CHVQZWIP1Yl3ocGF@4*ra0IN!ynn*qH*#NMIwhf=y5(E^7fSV~V}# zG67ixE2n{s%z>>!UQi;1c2+iwBF}A)g3xYvd_*GrqMvV%TqrI<2{+fW{Ug{a2tl&3 zF5|(QL0N}t3%iA%Y3ec->x6u?@F6MDiw62l7iNAZ5Ln5IN{=)xOr`DLgiQfe(govD zdz3aWRfsuT0S3T0V1?2D?aqXnQ;1bafQ4~py~kIXj^R4^WbqAuzL zJu4C*iiVCP>IIA8=Zmj9muVVoT5+{58xtjC!z$>rpkz z6blj``~V0OV{tF7nl++U0Mx6U`(yMx6Gc7U2I!^LnJFY!h=)0|HbFB`rs*nt(ZASm ziAVj;USa&(A`?H8z!!UA#>&i8JP1PMg$_EwAOrMIrW@WshfYdTKNg;Q!F9>UW~)rZ z<7^n6Xk2Ik31yUr}5UCQgahK3_I^;9U`V~G8dl$8j4l}2OYDT z$y}b?mj8NVy+o@O1f(ZFzg_EB3_hjQA6RnliKQWQK_(nVGed}YDefUXhODBzRI%dnY#SM>wlu}dVO%0VEmQDAX%z`L>-SB%ET6rM-?98< z->!Q9j=C~RmT|4yBp7~`AWPnz>hUR`McvqM0{^oF%d2b3ey3O@W7nrsaN!Q@+A;RV zc6|FJ?N`KporLSkN8$e5sfZ z!(8O~c0j@vQXreGIof!m3I#{i99`S)$WehsT`y0SkEAK7IJ%}+5QA&SB0J48)D~4& zg%q_y)1tgHM+`MvQp^TWmT1v+h+K}DH_6Y*?WUr#++0Ty(?_a*4*6@&BpJN1e5pzX zn>LOJvq`s!X#&)}@iZklqC!l2A=c{2ZEXzk0c2=cCp+0h-=qfIOBi2h%idiIe zH%%Dotgf!8a^6>?rH%Iq=;B;;0X3+RX-a<5ghjO&L0b!!P?E<449eQhY=!m4(nHqz_E;Q5_Q1Bn$ za5WVMpsD~zflrNJ+y>o%1)<|FdHj}r8f7mKf^Da3SJu@|iL75L)+cq27f$OQJ(JI_ z*x0obVM!<uD^M(wrc}W2Ze-KR$Uo&Hm3Hnc`f!a zHDUBD@j9Iq|Nq0v-W4C;r}_VX*LK_A&Gxcu{jRkF{}@)_(9iW14dC>Bx2Jt#qPF?R ze|g@fg&RMe;OX-y@D^wkmoS%cL(iuh>V+lHV=g8+tRC7!GO_J7#;#qO`(d*Szf18~`QRZ!j~|cFAjZj$*pv z6x9Lb=$%|1?{JKvC~ORdEaxjg&vC{s{B#=>b1BA zsnEiuD}V)!>eWWZ(^b8TBv4tJ1SIkwu^?^$qXQXaVz-21FTZ@aO1K;gTqhs;o$3mq ze=PP_hj8-MrD*nwzT+1prc$Gk&)y%ct_CP%V}yNkCw^iDe7W`&RRXI_NR{U=tGyC8 zt*uI9`GHjku2-sDNsxYsrB}*jV|f*39h%Y^nq2Nj`& zxg3?~po03DbyOx#st!*&w+$?bTrrPH8E_BsK0DXER=0SQ0PWIB@qG-j|2X;6!pIvUbq*X}UgY2Dj-SxpY=!D?4neI|u=l>MNxO0rB; zQhK_oYsI8Oe;FU`UA0!ej1AicEqb>|kz}?TBq@3x4H>yXvUg-jCN~c~SQCU+-nRI% ziPC(srEmR9&pTnt01?JOZ{N-~x2IdJTpoOoO{k%N8b)XqL|cG>i(Pgr*Hd!2#8gT|B{fArxep7K|Wan%%b?h6H`4bcrImt zJI!~N^>Cco@2nb?tXjj0CCdn6!KQ>ZIjkRfv)RO4wkX=i`T1mKuEP=oc(!=O@7h_v z^!E6=y)oea$homA2jZr#>Oqs@;WIz6_c~(oNgBRG%f*jgIWTr=9S5im`S_W0wX5Pf zuW{?+17OV0)sF9}T{#u;i6-p<7PqZiUcUkd{!Hum3*Uw7U-WIz;y@0ySPW~=1fa`BetW$ytd(YK(o~aM-!o83-xC}l+SBQ=G zRP_V8f*wP5I^^TRTi86znhHGO@;QMp?A*ti%9?DwJbwN(=cMFPfAe_Uy&v4o>dP{! z1=QO6+1n!*5*WidG3rw_6e$H;?zcw2RJ-5 zcIq6TikSIsBacyOEv#A;6e-*ckGjr#q|x0R+j>tcKrWD3V=TrP$;BZEY1HP zXuEyjA47iEnnP;^ewtU{Kxf|qX(}zwmM7X@`t-a_-&{XGZYq(}n^vCkL{LJXpE$W} z=Zki!fX-nQ7Rsff8Th#}O{TyPivm8U-GG%8xw#!-9UPT$5Dw{*xb2Q)ton4c0H&k+ zrz}wD^d_jfa@mQ<$icFEfr%bfIbY8CX1)+bxDw~>FtBmyqxfj~(g!JtH?JE+6iWhk zj>cPg@0nkc8pM0zi}%@~d;h)rtHN;8jU|d7K6ZFzoCr#eZ{`5Ok{nqjsm7RYgdzco zk5QD=AaJh|YeAwI)mX|ijjIojN6#4FxnCtCUe3$nY>u0&@q?rWBf)T#N1#sh#hoTXwGH_#IqvFchpV1A2!0O z(9qq?o4AkDq>E&#R?+fi&1W%Ula_bEPSTw0pr9C0j&vQD*M&>Jd2)#9Zn<$v7JvJv z5e@{aZA1LYrn6Jf?2OM9r>UGp;dJu$6>xi_vYHvuE$kRR%Cj&l6freNMH2S8GQvRSx(dOIk2 zfIlyyJ}T@8hb=VcTiLeNMx5I%n(#U=0T<;iyUiG2)f?5%xcdrxwe+Uw8Lkk_hqR1p?FTd8(`>2d!VRBGem!Qpq1 zJ0y!89T_3ZXXw!GCk`^(z4ar*GFyh!e-cE#UMx;xcGwOd9>0D>lK0^O0sPaxR|hdl z<4M1Br6IciBHQlfsBb$t(f5L$e0}6aZm(z>4DXR!qV6#1{MfEd zGeG+#wa+hb3&=iE0gwCt4j7narnkVv^bhb6iko?xX!+icJ|#b2SNZ?XoND7w>!-B> ztrcjkKx+kl@>XEaQ+>bHfHoFOjP||1K5x_Njo+SvHg?ey@I6SIIiH%0Y4I{JqoU=5 zOhEurN+Sh3ZwBZ>Mz-&{rF_YEDD6NDEEj|%PrZno!J*Muq%cIT9FQJ2nj&N>L;27I z^AQSy*)7;%v1B155Yk3M8~_ys=>Jmx$(txE6hmnDp@9sxz6lk4i_Lx&%B#<4)NyPa zAy?8Dn1pf6cGRStfjX*F1PgqlyB9=xcQ-I!Kq6_LOD#vA+Da=XY-|o{oU1xfI*h{P zG@l6Zv6u!X{a{pr&|ekKx(!N&k4_eICQGWT5gMSvCe<`P(4}Mm@{=d$z)6FI)ES|> zcg-4{Mx>VZ)m~V~DKw0;Bvqf-T_KcmT@Zs5w9?IAaZH zYHmb#+9JfJu1YeUrjQ^b$meablmf$*HGoh{dK2Ct(^&xs_zE@sOx8Fx)3=biE_P6c zkW#PwG-UjqSul|y5dl;P1DVX0HIKRj*0&oR)aunyS5N_S)V-pz28ef8wRianFi~Br zd!Lh=mTdbe*(7qeAlqgy(I5sA6TdU-Rg`)%O7#u}6gfjv%01h*x2Ew-Ko-ElH(}O= z0`HJ(Os}&QyBmHdr@gGyalC9LX3BoysFWv}f%6DO}bxDm*!lw8R@^;&o1kKL9X#v6897BWkxE~Bufo?Wa{?LZiEFG$oUjjVgLBSO$f-^o znt1;0+K&F(PQd9q>hIG3KdJOPs4nQ1xz6u_tpkQ&=32duxy7x0WuM2m^yKUBZk#ys z4i}_nuO~E>YCvyZ?#UGa$8$~`1C=qs7R!Q9U+X_TIy9`!4Va0(LvbnP$Z0KQsO9z% z0|b_j-2eCmY4OV>-+a^XY(sPh5ALiDZQ=a&9h>VrPRb1!KBaF1dI$pO-#A#?{qES& zbq&mcOYcbA{@VWfz!oqKa_vNzPfu2BDQKUCv8}JwFZ7LHzdDX+A`Aqj5-9Y8-3O#? z53ge9-ulk1wJqmZtC*PpTj0Va8I{srfDgc+8(aIfyTGOd0zQbM@a*{iBV)z)x{s>- z|093Y#-G+tYXw>>&{~1k3bap(b zW!{#rtS@R>7Y#@ZOMp$4eSo_KKuWgh`sD&8D9G++3E*oMb1z$i{Wyb6g&$N zQgBu|DwX__n6e>4Utr_4 zX4JttNOWmEHx|jdLx5V{%R_&8_m3AmMg0!FYmr`+qD6C!jNf*M4W0h&jmu-|t}m^0 zc7kRt+ zLQvx&8GhDS)C*xStqN&p*$i0f$=>d6F-(n1X;h$=RF$(Tb&Cw1esZg7ch*YRYOcYB z@Q{rpG z0UR|w8UJ5vGJcxa8S}H-c)o!CYyCq&`6W%;`$~PwrAbV`>ZR+}y70gUNP*uO)h+i+ zu<-?FKiRlHPHAD7Otp4pFpK#n-<;$Rs3L@1NcF?Xf@^()=;-V8Sr1W5{x@*WRV85j z%sb;}kJbjap_)hkTodW*BgbneUYmI3#T2Bjo#iQSFg(&#u0_ULzj#`=y}1zGeu(#3 zr$YAM+SPv7%BTm1|MNnBII@o7NbTG{K0P{gLQD$>UaOtCNZsM{IV;nM-%9afQfzN< zC&xDk-)k5GWQozCL+K3w4j~Eu;WM=P`9RjhaCB&=mhYcm-+x-*`6I*V_f5RKsYdEQ za0*Sm`Fvt*(bC{|^`2_|y7n ztw3u9S}SmKE3ki2-Cra@R3v~(_|`PTNnDo!$Cxc1j&6934jl-BRwYruDo~fA=Q!2sh@lQ z_kGTDkQG$Kjhb~xKn;G^^gTO_LKk~PldJ=4d6TpRqXE+|<=mt%T}Xm$TKSL`giwno zxD8i>4TBskD!2(9u_?@72_mUK&(;de%N(tqQBYxi38js{6Kw?!M);2UcJ~`zW_Z zjtk3#26TI|gcgm6QGg)_AK>^M&qSp)41?BbrCf=^I~IWY>y9+K_S*=5SE~Ho9Sa|O z?2h02ZcK)NuK*SJjs?^3BP<*pFhmqhFlAULJ*DPAUykVz=4|Gen+*3Qh*i2%OS&*C zHx^eq370i_^p_Ey#Cq$Nt^ zPHG|gB*~Cvap<&nEvK8VQjcn7mBH$Wgck=Ral(1|jDn10KQwFFoIOzHD@jgQv|N^- zouUSj$!}(wWC~N6Nri!&^cK3`1w>$k`Yd0@gp82oI?ptWrnPQBkP8K(fZ#!(1J~EdUCqxt{Iaddxk+hcn%oS!2_A#k)Ma0R7B1RcH?} zLtnM?8=>l11pllTS3AFHy7CGs&N^MH7eHh)_ckk;@II~wj(pbJ>dNv;35Z*n?x$kj zgY1sPKl(W^bTdqsTYsvgIko;C6na)n#W1)+*|)4|ne3Bq43agS%^9CBxMVV-6L%-7 zJ&Q#nj`T>%4oTWaLZcJDH1!pncAV~tQ(0=irkU~CoK~{4W5=5Fu#{k}dfarBH9vb2 zRsCvT{q6PSej;MO~9#@Jhk;E#~>>aKSR&DP15jPT%92=p|n^+{Cb|0e2jx|1#09)SBD z{q^l{8k6g2NFKVWUvfSrzo!h}c%d0-Ld!}EcmP2;Mk0!G@((mEq>$W9EJEnNzAsIPppXD$@(>r8rBgq$C5O90ZS#)$ zL8KbRZ-JfEv@i8unK;lle&)0m1B^=nukTOQ4zsC!enHM$V8LC15GL%b+4TQvp#Ky6 z|3`kSjX$lQ)(W&%ptS<66=>aHAC%T-f)&NCohbRIKFF{`$`iROW3AHawDO zsUk~Ql&(99q?k-PdCcC#^pbL5fR$Ok07;|}SoshHxyfHq9146SRPjl3BYtH@6pYGL z3fnDg!7MZmIo%AI7|M|%c7SOnP-ler%(6$1y@=Ce-f>L$AJSy^D6B3Q99cmsf$up|=ToEmtODFqxv(Pk@x1{WvJ zEF)#{+GqT=)Kr?we*D#3mEfQci9`vA1yB--5;LU&$04Fqk~!qY1-Kg*yL7`?m$7a= zM`i_{TH3Czl9)<{BH-TGKxfZQ->1%MI`!qv@``(*3u zddrPhOP^UEJOuf+# z(Bj250;U7T2^lSiTfgT`b&;GoU10>JMWg_-^ph!dIT0Y$^af>dD+Twojsq7COo^DR zGa)A{J@{iAFO&z%lS0ma6iuY(evsA_G;begIbp;P?5GbOuOE3mHj7i7eBHk$PZeo_ z17~BPdO~Bb>0^=xELCLQmRo!ovcm(>2s~`$XMSSnk_HIj^S5FFYg*ZwO$hb{#e{VXVbC5 zpsH91tw5KEMh52VQ`9bx)Hl34Uj*@X?Vc~verH~kUjDZI()_P)-^KVjsqWj8f~U=Y ztz#gL0RsZT7fyc`d$Hj{?QM}}9D8XG(_q52ZHM*xQTNa*ofbgqLodTp}`rTIkqT*&-OU%rGp6mHS7?x;%I)P`G zOW1}J%wi{1^mAU>v#ro07{pqj7!=c%v2<#5$cV_FbT&kLy2xwErQMK*jHoGL0Z&&e zmv=;rIU6*l&z@-l$M@AQ?v{tyYX0|aFZ2r%a<`+)S7?U`?zfk`g5Mqz{pY-pTWNaV zMlPE-aFG~S90I;9ekGjpxpyc)Wm6i%{EoONvOO* z6A+DnG*$8_YLG2qf^K;b*_*Z2-q#Mn0gER3tWX21*sROfgyoo;$j2xaV_La_f{@y8 zpi)n@r^7B3c!W+)Utt^)Ohp5dhW7{_@XJ9O_3~&#t|I=PB;GANqGsL&>N{f3z5*iT z&_QffRBk*8q=m07LScnsUJ~$RV7I;vb4VYU`J5!AfBlN6vYbc*3S;!@5y7`7sgI#r z;Be^=Z$b8a{9^yuL0sN8r|D1@NuaUkbdl>vwx&dI=`rux4T>9P_UPrUlF=P`Gj6&b zSyz8)NR-nQ%N%MoNfe1Nc_fYASdDIQCmEvZ^1_@SsPBJG;N(ZPgQhne{PniP`KZbw z>Aebzf7=!4?uIMB9UXp^1X3R(X`Vs`#QCSnjU^vb86#uPO2gInoCWbt76(=!n;bI2 z6x@J?dwtI}o&M_V4qZ6;$hD*34{Apbkwa@0K*>=zy+g8(Y8={pIw9o96mQX-#18L8 zPG76&lWOX`e*D6oTHkdL{U9pHQMH(V?Ofl)9)bUZsTYXAxeX#ow+>mst;~q~g{NNq z;2Cm#O?r^|Y61Ukhh;%jf1OS}*>&2fPcv0b;@26vB(yKTNKQ*a%Yx{W$k_(ZP8@xS zE6Uk2S#+%L$8sGWLX{xSs9EIAn{*#ubLDG88%Wnv2L(QbdcWk-C=~)3QT2^j9N^NW z`qA~QM!NpUD=>b>BWtca3q)|?o#W%@w!~G94gIxE7eB9s|p-Tzk;Rx@6s)QOj zBau?drFSL{UJ|LpGcOA1K>&{~0?@D&)iukYT^0zY^ng&+Ka-?nM#yzO7u=r{9^pcw@vhaP`WE|}#! zY$T}Q`8-OPVgXVJA>2ehtwjgtR0Q*L4iF zX?vqH34#T=9Hr9fs;D^f0p>?5f#SlAOQUVhfP2QC2^LppA@>XFYt`)@q==`IRxM3U>j(q1^EMQQVLD($sgvv(4XwDT;fXW}wPf<>n?rYv7SUhg|gBCB#c(Kshx(cpE- z@DgZ!<~(P~uywtFk3hv9KM%nAlH_Dc*Is{ClDS>G)7C1Z7I^s8*yX-H@Y2L9$AF+S zKWGf8VLNpvnaIdV;ln2-iM%vpQ{cRO!YzXQrt}hiqtY2J^4CLrZMTB`Q#Lqy49YzVKc5VG{>0|ZLQ}vdaLZ;gc5Z-lrG@A&V0h8t zCuOAXoaV%n&2j~v=T{R)U)I8X8iXI;TLs0h?|W_H)qSLo)IY}0T&wT9n#Hs;C5Q%D z5q>VM52#;wVeHClVBj^iL8rMBFK+}SFL&qqL1~U}K$t)r4%u|E)W1>|pgJ=4N`K0g z7fAcdN5?OSHXt+7qJ%4vr4*89(*BC>@-kE_|Il{#By?|CPVduXok|zy0sp_|y7ntw3u9{!y>MfuHMJDCmpSUr@=& zzf_#pKfmwy;-ES87c}$88$(gYV-Oa;T_QRT1a3hHU7|PO;{vE+#!_HL9(qt^97p_8 zXo>?M&V+?_LAHK?{(^Hd-pYIb<`*Ppj{OClQ57wh{}DS2dM*$}++cxOOvORC{I&xD z27W81*+Q$oaUlP`&<5|vkSl=_v^)tbFyen=JPBt?(@3&r1WHT=1Gn4;qdOJ=&T6?0 zqWDp0xecmSXt@mv804Fgwr~|-ma+k9fZSl#g1nG3EBxi3w%g#p(?M>WWGbaWUBnQ~ z_{hnzJ>p=O=DvoY`fVYrXVPxTT(yv(;^Ai<>C)WQ5RnM2cSx$jO9;4TPd2nLyb&3mPr{4w4z_~?79~?-6s;AtbmHe`jinYQ0^$q=FBd>o+R}m&& zdm9x2KFr8#*ZL&OSNQsD4JQR3Fh{f*rFw%5!wK9A1rEZjr3kZ6%3P^o0bT&gN|1`h zMa`YiwF+4Q)v(u~1Wh2&bDyl5QQ?T|Zj$cniiM>2LwnLy(!v;&eBc?RLV;AFCJsu2 zKbAb$&`5jH!ms$Ch!S%$+RA(X_L3y(KhQ)8V}Vd4pv>Cn2nHb@ZzV5R zcI*h0u;n;R)d`$1$~lFwT*@b^deDSpjs(9zx~`*uDmeU9obNIm2@9<~6uI+IS3Q^t zDhtliB-Q{K_d5k~-psCjUnGh6B<+>e{_?0Db_R^ub}#Rg%4K-au%zvFr|d`VB^Qak zFyx-orZmyqn_Gqi^5gkj0rPI*YfI`sopv_IbmR(?$J|VLKw;dMt*EZ{yFss_pc5%I zYzgdmWqHPv(VE`Us!D+1+Zrtxo#gqq$a;~NzAk|{0f>J1~~2^910TJ+f6 zN-FQJhaY+9;k!}SqQy4JK@gm8@}b8dI#dAt53zbT!aq06NX*i1tQj~%ZNQ?ibW61X%26FQta%OZ&tm(FqLP<@mM8fX zH&2#nNG1^21PJYAW!$EgDgaXAy42uZo`ulg{PNEX=0?yd54%5ut)Ds!o&rtW5>f7( zyQM& z0{jiLl#}VMnP*>mARf3rX!!mK zld)mr&ocdtjA-LmAJq_;8rkpM)Y%J50{aM&+el{*u$q#&n^7p3Vb(KmHfEh;?FHQt z8yGf#-jSova?w?qD_Qe}jiRWib7w(0?9gZiB{GPpf~3Td=P#cVu|6e5mnF|@t2vDy zsGT`L8Z2glR|X*yOuY84W;19rKVT9Q2d) z()$Ly{O;3~96({jE%=?AG5H(cw!U_9khRo_H<$;I z^)j$4U^WJ_6bBYl=n)DoVtn3HSbxpk*Cvq0B-y35`K8gz@Kbi+e~1YI+my$M-+c@c@|5x`WrGkSCtlxQ z8+@e^Bw(8hOlN`Xf&b@OH|C4FmLOZ=56|R|IajI^>d-`M!7H-RQ9QHXCwE)2>6#ZI$W3frw5J`^7yR)0opbbhwnoaBxDgE2cakga(Jb<~C3|WkwtLcvJvS&2`J#itEd!1dTyo0gV z`$Yf$`e6hKNEnSfbLKtlP6Gfrh<7ayzzI~68`~ey492xx4$c|1b*KoOgrOi!XeDN# zjRQrl|I#t^41kdX-IxCyxKv-iRl3?|4$<)@)zc|9ZEvv~7p1PU+8G$_J2VeMtPl*HRq9)7=x#UU za(D!cO6U!P```$S9lNd(@0yIDZ35&xI>%Q)bt(zSD$spLZvb39X?X4M`TEw^=;xE3 zS{o1OtyoH?v-FL&X3?VaI0XQ(E)d^%8Y!AgM0sbIF3+0`l*&g*Bo0$x76vX)s z=Q@n|tY;3PK*3FBbn%iXdjy8V%$7x6@{s8g3(Tmqr)w=!q>i8qE2Bei@j31#whfxp zkzHl1{jeg?1E`(c&265N+S{=E3kZ5pfx3bI|6dO;SiGzK3o8HrnLlsiPwS_(0<9Hj ztw3u9e(G0X&u{kK-;i*LANu@*pPskzmjI7^bm1?2bY7exp|V-T^BjHR&<%0=&Xo%V z+l)|2FMCC@swgTTzzO}qG*M-=@=-AtcqIgl9V9u+0kmPjBgip-kO|kE47l>%fBBwD zxc+ePTX+5a_wN1kul{VAAC-2IK8D3G3{B89B@)qcDRlh2MFs}N#lx&Ju!^N}AxvN- z9?pWKKb@<<=;j=;Ompd)<*=D256R?QG=`AFBOxnDDwfTKyzf;F9?5aFh?eq;YwlMH}3j&ge1F4l~p~)qU!3^NKO*6se>PnN?m^UN}s>)>RiDfI1A@~Z1QwV{6PO5 zsm~2G)9<-sam8Oz63WmJE1ZogtDN|^=2Bni6 z1J)nky_Q(?>!7U?Nw}J~v`h9(amOUWL)$oC=VPI;vsKNh|sEZE7B@ygbsW*y*(D8NB z{Xm>(>-qvcelaab1SFY_z95K%iT$VRr<7u0Mq@CQlhCe~S#Lop5MkDsEv=Bx)8LWVZ_A=XAkh2zu#Y#~Ls?O^F_g9~lpZ`ho|9jhR@BOI{dRr4{t-w#m3LKo*_dr8> zCfNR8JNEf`{a;)ET}jVA@}bG}EcAnX!OvlaRmhoTvWi?OUkr0@DR5!=V2)6>{ai60 zmd#S$3274*BfA_*f5(b&TFGsxJ|lBAiqP)!!o>ukNfDR@ya-n_#Q_-p*i!;Ty}ls>og& z)iqI<--FL?wkfxeHBjM?YJ@GNamQgC(bH{2v3o$$!pro;)F&8A6)9~su(&&=dwpC% zBQi6cDfJ2Y+bV2;Ql*-50eJ6DM^#WXkeTA}CL8#h98II!!n8SM7DzhN#j_Awj+qYI zuTL|x)d4NLTge~+u*U9Kx^0_{rwcGg@W#U7BhRig8?my}&ar9nnu2=1wA5B+M+wso zXCotllnJNvBp@!Pd3GVsFQi(N1lKpSj*WwG#b4gc<}*`6dbl(eiEV-0Co>8}2&oD= z&>3=M#v%@XqfunZq6>xH*_wl0)!U0!+t9)h< zQ&Ky4O*uNtVvZ1d5M&la2uC?q1TsMm7E&hBo9VX9ydc8gV7Fz)GRa`r>*jz~;5}IchtjYxGk=e?Az|aq$+1uK?hW=S+f=HAR+8>F>=gbZvo~hG zx;FTmdUdS9Gb=IWuCVTjqg`eNT-Wr3{{R4`fSp~#5T^aIW*;_|8_n*XJSG^D9H`Jd=Lj%)%iZ6%cqa+?^@H@J>o35CKY~I%4f0s?FC!G7 z-TLTZ&)1!5GzIEgc8Q>2?XCUD6pm}DfGztNd=@JJ)?wn{YjW7GSAjUB>ArXr4(=9O z{DA>2=07Qezi;#C$i+#yJS73C$qbxFlJ6vG)|G~oNiS2}d#=9o45a_DU7JAvXAS)1 z(wW3xsu#&hWhxkuCk6xupw?`3@5)+n zf20P`H^t#k$^iKV7V)et#e7{Lm_Y{cf2jO_-laDFw0>GE&{~1k3ba2)j=U_>{CGCqq3XLpZrKUSFmaF1AWA}Px)WK_TueB-_> zPTs{xq1`N|6+iIi8iDUWM}5y<6&Z^wd`wr_xC*gr^GBWletsXupd7of%z_#VJF9|* zm>kg(ij2MZccOv8XlAMxWHVGX&1IF;?lQ@6zcwJh+SWIwoK?6->7!20u&R?1k7L)-2yOUG7HUCZ`ifT54DP1b7Fi; zL)>=0d@oEpta5A}JqaGvv(JL|rEXmryG|S7UQ#^aShiJ32g!B7I5(5T?IYk=Fu$Z@;K3 z0ga6NNt-+ETL3pA+hu1AqJ-byFJ*?o-4p#6bfzn;1cd`g5ddk{-qbA@IsGk{5?_XO zwSgS7J#&aTnZoZessPt^e)uvzsQR{SLnak08si;JQff z=?Z~fhw-z0~`(MqVlbxOM?P!^DqjJnxUF4%IB>oxZO9czIthMu^rQXaGO*lW+A|DWmq z-}a&5whs+|^uPUNhdiy(HLk!etM8%D_1&#Wm7n?WFU%8+uY`b`7O($Ca|l=}!y+tT ziCoTw5$(p3UoH{`+CB_IsDYu2fSm0I9vuw2fQ7(ua76OTB^;CTxtt%gd-ew!0nWvE z2>`z>0bu>M-Y-1(u~+Z?m$`d{71jy^bFDBiC3m*Mz?s9cK z2treHEQ$Y)oW>4Yl#-5ZAH4!4HWq1`#%oMjFl!7WEe~z2)F4lU?6Gc_G)7v9qS0jV z#6{CajK+kbYlMuFj;UnKZyeA|lnz^mQaW5~XtL;NidIg+{DM(`bva2$Pa9QWh!eqIPD7XkN#n*l+8|S>kQwx}}M(nj$MIQVei-Pt*$s-g*Ny{VcSA+}|lVM@41A0I=z zq6wsFws7x1f8wvd|K-0lc6_q$VFM+GzAq=PiQ)kBrA^p=}L5m@dOYap^|?iidKRWf~)bY@kF^? z2_j+8Q$Tqp0^>td3xP|1&8$~{#eA05G}x{p7j+|QSe{q1N`X}fqC5yV@hkD+OgY z$Wvc~Ph|q#3ra!03~-NCL){@&q=if6iV=${H_=q4*hFU;Bc8mA_UvS_3G5%G9LsUd zSQ3F!P`2W9U$kUJwR1U1d^2E;zj|NbF^Uw8wWcCS`VP+`W`vz!vH_<;$7nu+@NlRa z-~ov+!%f$mmVNKM@!0oXUY#f>bVkp@d(mpJFknPM0VaE=zgo%^_f>iVh%v@fYXsPJ zfAyJ8quofQ6M#=yHbxpt7PpIQ;gZG11Izf3+K;zdOnSU``SPeoQ-PMP0Gt+eaVlhC z!6Ys)mQ)arfUI;^d%nZHJGsWUX5IAPYTorT)rh87I%uviAWlHGot2aq*ZJWgp4vR# zw1j||?;S(+Ku`iaQRLO4Qe+J>kDs<625=7Ce{$?3I{DXicFT+5&YZJY-U*6RpIYz9 z`${AUPse79$+KC54$_AnpQjQQu8J0hAvL;t3_O229*v+c=`zFE$RMOOOWTLNCX~3<_{HrCA5FBU%M5?z5eHSq$U8mpdb(E zU7JAAr>S@Ly(w~m=MD+{K%GsV_}DidKKv#A0cc;pR`sc60r|_JcuDkf>0fEdKwb+# zCR8n97)@W#T>M4BS;+7%Y=_-HdhM`W?#eJPwc57B@~d?tKMgdEX&~%vq7N+z28Zk0N|NDz#hY2Wz!mnu`HfzW=e$t7hYxSN`H- zU;gI{J9U*Q2WArLd_)}Td!a}Cmv`}C$a$_yY!`w>_An(cO*R{gmMON-y4i^D0MR|* z!aVBAP`adXk~I2klYk1{<)Q=3wlVYzqva6*dS7&<<|z0R24Yk2$A~(l2kPo>$~3_r z4m7-HP=W@A=JLm8_&07jYW{Rgw(jYU6{w)xiU!qUUqV+X&dt9UT74!GTNJH=CoYi~ zYU z&hB2NidLro+r6gRMc^w><%3amf;#E-TGJ3mvkwGd&EBvP`gzhDn{r;-Ta$;yw#4>$lG{)4Wv?By8 z4?PS<$I|t5RjX?_w2H$5?PD32q=ktP8Ol%w2_vJatmgK1Ml!;9>W04t*ACXPM~SmB z%<5{?n0i;Y^Tp|wGci*G+KF_Qm8CSr zFA0S}@9szfI5W4*H1T&{;F5QN*g$1`)|~&p^p6ymeEBzae5UV_>2v;#D>p1|&iPRk zG()mfzl`)ELW}ug5TVFeDukr=j>`kxLI=br`3E+UFNY2yA+V~VGCc)yS79;Xxbl;q z^UujV$@veZW9fkt1 zm26&g@OKRezTk4dM#$@QUqJZ7d&oBp4sW+QihQEBW0*EKNoVX+&mk%Uj?-u$=VZTo z2PgK33*FdT`^NU3B!|}LBL~$i{WRZ?3$#N!Db`8?e038tgyiV@cx^xsd+(|AI-5L8 zj(cts3GNumhg=J9z}HBT0YwP@fiq)|N997i4hHv;KLAqRA?ES zzNm%)NPk}&{tbX?ct=Y-sx$A2*G8=aNCs4E{7~K3Q}Jg3+ffh)~^#vhFYE@ls*nAti+D`OP6Z9hH6)Ub!<{Pj!=gT z3<_#n-r(Ak<%~gbA()c?yQbv-d7(bV1OTUf+Wh}xxBvIHd)f|v=u_{0s(jnkPyK4! z#LxcDyyt)JPj3JC&;7xN3LonKIs0e-;ghvb9+>ynpZw}4{=ZMW{fThi{U7?%+kWE{ zztVR2?eNq^hf^nNB-BQ zRN_|tbMc4!e(g5%*{H;82Q8%>SVaJe1vAXqR8L9E9Xq7RV$&Z->6^ z0Z8;B+`|Glk3k#|Pm`>K#U*E{b{Xnq3BeTCj^zp_C^dG=s8ZmZM^+DB8|&y}|9-#j-`W0$M+8G?(2= z=GQhB-_tUi9o1zUbuQO07jWIN?2=g`1uVmhN5_rb?|DuXIqbPIxwV-yNyqbSP9Nr? z*$!s4?MhqQZNG9WU&Pk2|1?%0*|Jdp?AD8rkzglLp>P6e|A8B_{1AzRh&KvY&@kY+ zgghV>i-KA9NEpfyfFj>&r<&un{X_EQEP168NAhUm!RqGxLN4U#=LI(B=2>UwLvZP| zy*p4{nz+xksTuU<+vrDluQ`sOi0U&2RRKh4D3== zV4RkpE7_E+ zkT=nvQ7qIU&4JuFy0 z)iPdaY`i?LMD8pvCfcUV5kX3i_h@K&+^iyHM9-I(sQ2q%<*lbq<`N~C5raV)pDi5l zvgO{P+gP#-Vi6Dm0k(oj6fDK2%rDTSrn!owip8s~z3AnaCexHAahOL}rI<5=z+<9p zqcAAhG;v9bEu=7=0-7r!z5_)w48hJJ#Nr52jxR`wfm!*sf#lgui<-xLk#*s9Sq4w% zI-!U30DCMH9Tz3q#BS&Dj_h8^4@>a)>C;3=P&&Mi^OoKALb7H>hkQKDBScAs)h-5> z>l1)bKMh=;EzdU>iv%P)Sq%B|QG)Y|`7&QIr(EK9h{6=CyOWdUA%j?DUe(-wbfei+ z$bCezQ%wR9`RYUamY5(YLWSo=Pmetx6iQAYM3Q2=O_1|Qax%Vk2!>h|4azJ_6u4{( z6SD?cvxv65jee$WMRecM*etmeM@u2=04Ooa0X!5H6z}%rWH~;}dgaPIgmTED;Cbqm zpy5Mj;bG%ZTBeVM3@Nk>qB#nzfo*bUaJlu0W|?9#@!2!U$(XaqMDT4Z7yLpAB~uP(F@s$ML);|%rXFozzvhPqbFFFAg)dJqN z4*wk#c{OK{?qb6&*F=L^G^foQOuFR7CZj2SX%zEUQ7s%#TV{{)}`-z+B1 zmmgRL-&xj*FFd>)ggZUoM*D~WCeL$?XLq5nIz1Vd7)TcqM7y@!%pgz16Ji7sP-u>d zvRV$i2DQcz7eCo(U|D%)7cDokOFV1|Z$FouEbqaK;>NKv5E6nM!>BDXfO4K!gxnWS z?*xI#E09Q;8S|AyK9vVsKoQR8jc2;clasl{0QM9dfY9NkDEEP&nA^#Nl!p?}6dpg& zt`HlMZO0~phvWx*drO#mlmN|mh1{o-ljTCua3a#DCMWw`HU--5AmatOcTj+%jc>{k( zmxk2C4oQ4yLzE*>9`ex0=awZWvkSaVOm4dq*{og2?ukS$)Uw&dZornw=lEW*fTf_2 z3wRUo6gWH*8)|7J zx+ctUH^E{QL}^+p`kTC)?GB*M2sI)VISIlLs0xuEjBA@@6(nyH5@Tsl?de(jc(q!c z28uBU!jTA0S(fFr2NwBXK(rm<^VaSV{k0c?+q7fY#yzD5;yeBv3rC9QLms088G@qD zf)HsIj+A4dn@=7CC-`OnAgcj>cZL47AWe3OwA~xm*m-fRKN^vXXNNd0T-5fD3dYF^Th=h*HY2t1GK2_@se< zNJJ;WFf8{go!vcrXo-IlLu=A0RHM^ZgAxxIp(@b#m8eM~2Ox@;3O6D!W@Xqmt74{} zbwLiksi9vvCz>tc}L_kj*D60%BjNvv(`b~XNJ+Ry{3~Q%zjOq)}4o{s z(%W66ss+l2LPnl;U`_}a@y);WrkI9p>&@+ww1@vH9nT!*Fu%F-$44%`ie6Z{a z(L@6p#5)vu4e-|#f12t@(P`0p?Coh}#~pPoLa?sin0y@?a0N6I4FALW#nU1xFmehb zKKN`X-t)mp>j4z+uMBfVMOQndMej9^K+~|G1dLw0ApQ9L7h{gTf-RsWFnaxH?d&Gb zrboyz6NfjAT|1F59d_0ayd>azrO#j61Q35a`iI~JgmwSoF2*J^RR)H1*s~IfjGsA< zUb!-M?C{v(YaFIIc;Eo^coccz>29OecVw*Z4f!w77-H}D&b8WR6dO0x-d@MR>WB72 z6k`CIuqT7r^p22_^qr_!pb98`TB@oZ zHEMOb8D3M1eewQ@sC~|4BCt<76wyiI7;;6f>gVacz*uyp z^GCIR{xR5~SVH+Uv(;-UCtU5o9@cAeG z&=*$U96{$rhpQ1fk1{rOTzq|K5>8E;Xbu$^ww+N|7VVoa;C(D zDvV#WqSEP;mORbQGM4$3XHfAc--^>Cd8NpqGh8nl?#*yWQ@%07QSlzV(UnxJHRCC| zFcA<9?iun9;j7+!;*kgCW#bT$Mx)t4&AV|hnq>zxMx%gu08?e7!>RJ*B47?2I2b$;bcGIgi>B3L9sb;z{H zG`sg27g%NrTs||?sS8Z^3zsu!6X_+fmz#{YZ@9|RpOqa@D1FZ9LUU|F-vTHqM&h+OBB8!+^XmB4;&ZLK*^FUQ*QtKI3T~Ay; zE1T{%6UWUWGx4b-0KSQn9VnN~w3~$Hru#0#W zVMqPo+0mixj7Kk_sJWr*vF;keYe2@6ympJu=Olr9>qPy{j$Eyl@ zpGhyle=D>&(s10R+J+0Hf3g7A-#{c@luL&9$Qm5HI=1m*yvll{uZZ})lHe0wyUgk8 zrugBiX^^M0U1E7b(&VrMm z)%BTMx;eS6hNJd@)o!O7K`o2cIZ^I>tYXv6k27gwDz>fOhac)HY>>e!*qeT)?La1MQyY}{a*;1MM zftV-`USgWM6o5$}MFM+)nZqCeYgc%vw&ee}wYhB{{aya=W`CZ)v+wanMw#@YJ~a@` z>;GJTp_x&J;tF6W!IXd|qtFP>2K+gR2P%aieL@Mt%cc`pWEw6I5+J1jBY+I@e4`=; zTCj+-&QCt0Y);7NC^T2c>s>ko&&tag5^Md zg89Rw+Jegf1Tjg6z(poXGAbgusiPt?MoG4%>$shY7Yp(A zxyV377+xtVGHFJ(G(cTuwNWr2$--XclqJz!4k4^|Lvof*m7=~1)ip{^{A4ezTi*!$ zHChUJ)hgWyTUGHZYpQ4`lhb9x^rVGDvxYXAYtSSsM?yg*7V7Tls;s0%h92`$M4&l^ zw5^gWIVMhZw>s~5c#0B>R@c*2hP1L9@@`kI&s{TBtLHoPl27;4RQI@@D%NGvMx!3;;bM$G=?hZJ)(OMgHmE? z7-wU1^S3L;qm|AzLZQw!H=9Sqb3K>sG^B4j;b`k;j)5u~Z(to+@MaT|xgkPpr{5mE zyep1BRdWI1po!OaNN|L}+z`IQ(uSZtA+eFr=CtUpuHTOhyhgxl+?7=#y4x6g<6!z; zpEz(~e3zmOs*`I$r1)j8tM^JISW5yma26F?5d~$UF}e@2{+(uE#v#Zgi;-zbcroR@ zt_hd1Jka6usizSFKtu-gN5+R<*K|xmq-9lU$6wtvc39Yx#@A6}i+a{hDihV#UE%z^D^}~+BqeXwuD(5S@a5XxO%j0g zK`PxfermlQE=1De>MzP|PW zDB+7z>mw=}8+o1C=zBKFc0`Nt;ud_GQ$8sD9eO(Yf=oRnu*QuQ#VbYSkW0rMCAHwA zBb@)d*eRiN1%Z>8c$37syRO z?`QrXE&U?4=WhG;dHqkU|219u#ZHdm50zg#6wp4{b%3G3%|Smy=TIdYABYDq&;Z65 zz}O;G2@ux86oaw?q8y!GP~16qGU$%0vn3C6Y zON0uc3A+Xmt_OAj(}Nz%u%E1uqwX?JWbN1sIzTBY`^4%{(gK|NI7t zf@)e$<=#JD=cGU$@V-GA;!On3HbhMZbwA3&;KlPM3s&6d_b0HmJejtUlyMAw`c!EIwn2(_Ck7z8A6j;rC`#x zE{QVf`^3w;Yi9;|p6l$HJ~ZVBGG-E4UTIQJIC3sc{Zr+<_%(fCKDtv7q?)pW)cznfqx!ksR3_pV-nu~w9lXCMUVF2?b)a@`kCdqT zw@~`PLs04*>cucRG*aKcy>@Lo4{HJ8$m?ZNQnp{)6r^<*DVb==yzGlH)eY2&fx0G= zUY3$jZQDDPKPXOda&2?~L?JagNM93a%RVaaRmn4z9mzNChLD|+f(!U7i~yAn?x&D z!U1`+`T@eRKFrUMwky;htcVJb8KJS*u)}Ez=lmSlEm&(HEzP1&VgaR)jGA96s)#O{ zSSlWvRfV)!gmgyfU5Ow?V+m_ok>15jiip65Ek(x~aWgookYE7Vmf+@zMuD3r!-nk( zF(oJmkUJ%YVLH&qAgfq&G*7~XMG`Lj$?~Vxp8b{0_wMGj0?e2 zNHt*qf7g~(3nM3o85H=xAWDlSG)7=x#sLGIKZ@)fdB8LXY0y*76i(nyQKg_O2Px7K z1>7d|Z>hJlb1fpV1XyzyF(`Z4hJXR_g#ukVXqrlfR}G`NpaFNP!z5rR<~*%IRk2|?<ÐQ?4b|2+7O! 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 literal 0 HcmV?d00001 diff --git a/data/signals.db-wal b/data/signals.db-wal new file mode 100644 index 0000000000000000000000000000000000000000..77c137a6a3e53c085ee8a7c6ecded62a1fa7e8e3 GIT binary patch literal 383192 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 d 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 R7sIrQx2Sx-{g z4z48!1c{3P*CXYey%rpXazF~EdH>^$2M*;|K8Y~^<7!yz>|*&nJ^J9jrghuVb2VEl zOLw;Lz~20k*)+Xgv=s-!8;yt8k+biHu2ci5z%XtHd_r?*Txic$ek?IRY}@=hl!{S6 zG_79@4IN%8?mk++V1;aCp2y({s*lK^z5;@tjFd)3{QYx-Jg$|2i(P(V=pMVi!-ejUfPbj+{GDpai+9}*2(b8 zU7YF8N|C!b<1WrHsnLJ`;>?0Qr4_Q)!$UQK5u%^MH9>93C#~t9wk|mLj(S9PPAf)s zE?joUDI%< zOd5pVvKrSwyc>*z2sR4X2tN}G<`}iPWJS!#$4S(gDUtz6(ug-iezm?4LhhJI-}w?@bFe3Bs<;I5c|le3Fu7&189P_Si$0l5r=SVC~O4cF^Ib+qN=^t17M&Ga7A&sr$X0o z7ZEB0<^myM#GhN%dqUwb!uWm}Y>&i%>0IlnoDw%vn2rDI7J0yb8N|{&R=)9vkSgt$ zd9aHg%#6-mj=j)hM|G~RBjD9=!lhLdDLYjx?*s=c@{lnV_)i})MjBCtIv)E7?202) z9C*wl6m6gs7%bBfm|vBSV5=HKvN;59ZUCUqkVX#U*e^zp8^n~XLWxjRp(>q8*CHA< zG*xd(*ASA9!YD&B$(dl-m4+vc8sOTXVml?BF;V`J;O-R>BpMWq?^;3DMXsfcN?9~N<#oJq{D`%Wx$quw8@jgM$%Yn@ZHi*WyESszD1| zmGv`GqtS$OB!{%dVn8ci!)wkFjlrXiz3Q{^*raT7rrCEe7GgCNk-%IGZ$pcu_Lm1r8aEw_yj`=AV2=wl5!GfPsjNxXehPEX_CtL+gwT*rZ2~9s>^MY+brGYudeo z8Yakq3Oqr;k7Y$bsWH`U#V>H~WxHzs@ZRkAKO}wuSEKOj-e7DM)Fjm4U?^y5E-l5*lfswGE_q=Yi5w7$s^DOYp@_flN)l=uW4NQbz z^xWhb>ABJ~*rR#+d-{4l@sGTGP{J?#mp{Zb|JG1n4Qn;Ja+ZDY;;DY! zLX%;p+X^r6+EL-12Y%7`U5tbU_v_5-t!vC7dq!gj0lGzW??8)%QE!TfV&Sm%g9+e(3uj-^)-mJmlN!d)l|% z_b4t5iDBn^3*oU|J;aL54aw1J#c&;u<-&w z3P6v*#S6d#E?(dv))F3M_5o%$GP{A<`&pY=&#a3VAolqbnJHv$BbNFkZB>(*OeR6b zBoikSqg|r3Wstd*%q05ki?nqMnTho47ijDAWNs#N6PX*y+(71fGF4ET#Tk!%-cpI1a1U^ZyZEphwweD1RQ`yZS@jd&7;^jPsEqmo$qh zay>zQc-6*=-pj9;dk*KJ2NNtv(RE@!jDp`t2a|kU4{J!|io`*;r6$xE;w+#O2NQi5 zWa?%p5dc4-8H$6H-Uups#E2S7JZMIXXBr0zZmFLNYL{5LrY4!o@!Sn!g#&?UZ7w6# z)kjm{n9CX|^9myDu2AKgiqg&%xpW0~u0U`KJ6G_XD`;nsAQ`cE40NTqI+9~z@qiJJ z1q>6Uz6mf!fDu2Y%L(MhMRhqA0*5>JlH$>j8B`KrbXOuc!w0Qo^7n_&qnx;-GjBWm z|DK5^xc=3UQp$z{ACnv+T4zR* z2)-kv()i^NSpGHe08^fvO@E{Yz_J)n{aP3lNjg$-kSgcQi9^!VWTQ5bG0hsIR#n4T z*#Fn1$APdLBuswr|7#IsV1jw7eSRG6;|FI`X&+SP*a>n~nfjVK=nd2&p)!Wlt0MVd zO){0tf%CQ{#WHI%rK6(PS4z_*&QfcFPce~1{uuHuhoI@CBt@spwGl`MyU9tJgpAP6 ziGg_AB~qnQbELc@YP%B{g?+2Gi_^0Vx~peEY9Ylqs3dG9(itMsmW*U3o&jACN;KSz znKe;77XgJ8oAC%aI2Mubd8|T(J17$|JdMw!bLm(*L_bS7S; zI2>w%5PwK4heSjp$Lbad=c*-EFbRd{?k74V=2{fLh2LmkU+$t%cU{y;3`OZGCBHpK zAqW6v-LHUn%bA0QhoOZG(0=X6*S5cRBav8`HBG0B+H)n!L8xwMPI|(232`7N1IL%H zvFDJ|;wYch&WMZ9(+mFRh=O1>ifPkNUz?66jVaD1*cEA4t0X;8p@HQUgvkT%=$lQ( zk&@T9y>9|4C>}vP1~PbOX(G;?ysn;;P-kgh#wiqAh%}8fp`GY5zuqX1xO4j%9yh?a@Z^yU74dT#l|0afU| z7%T$(c5O!s1EgeDikoH~Sc+>wOqol9@^^ggXsLe%hH#;42Ou9DeS;x^cy+5FNx><}q}H45==i#z2kte55RttutfwDSk5O2~kf2P)1!69QM`F6p>=}HTKth znB$kIkVT3>lnB=|jZ_7_44p6S;+vi6yP!s~DMZ;vBSSyjh{2*CY3Sw*l)2i(NPu_Y zY-gyN7*ta}m_%KtCbP+C3VunWr=CIV#&0yNlxowc7rkgR_@BCQmRcRB=x$K5yMDVk)`T%J;XOq9zJkLkxQwNlv)o(8tQc6GU_&@{xS$9 zgX#eI9$eQm;DKDMnmkA&4x2HN@|OR?Oe(!Osa>+Ur|Jre*vr;U{E~&^Ev`?34^q#GcW@DLT9aFKv)xh%1+?E}P8cgAp zf$O=aP(YlY%48r}o$dgJ7sdI2!{yw$v2-Q_l4hDf^b26XQJ2o*>ZFwu2QgzDqdg{_ z18!-^>P$^?9i3=BBx|x#Hi@Al^J%t*Xtqho3VUUf;CZBK^HyDxCMVQm_EBwQsi<`Z z<|~<6zceO-j^f@O4H4sfd{YQ#S^}wEMJb zG#_XnWs{SM%ez<%|5$3bI2!jdk)#`3Nv*8wW^YboUPsAWp0Hw}D#7J|2i3?{N7F_I zr$a-blhxN!omm@a4G1^TZeX1?14wTXmjjiZ`O5LIWF)v+BGDr}7S*^NX^iesoZ<~D zlAjj2Q28T}MT5+-mrU^hym>I*pR|+zaJ*C@jt{ei>2v! zUyLzv6LK^poRmo?+1+U8_W_=vWo!OzFTt)ZY@K>i@3V)U+rt`1qK(s&@#!GnVsFPu zZM<9j_~hbCbJW=3BW}XzztjEOIbqyb`yJcA&8Y{p+V{?0z0VGzM4`?~l#Cb4a?-KN zl8m=HGriZ_mm(Q&eSPu0Ucc?iW$t&b2V4*Qmw3QB`{dqdNAR|p-0p3YN=`9TNyv)X zubYo``nqY^wX63{SDpCq5tYMQzlJJ$e(dZa=k_RL<;vcUg&%X0QJ;f08{X5Um$XuR z752jYU}A#!zf~SV?62SN|D~N{{`%qM5x7|dA2rUwaxg*umprR(HCzw49&kP2dcgI7 z>w*754=`jncn5)>ck>9o!~Edp5x98-J?aUBpCgkcP=AKDZYNVmCQYW6Obr<~kDyPF zFu5s2-U!l`PFosn1<0uMt3q2cZ4II=KW$Zz8A!igN?VtZ89=5#nTyF>MCL-;r5~Bk zkm*aGT|iqBne*w_^T>Rf%%{klOXibg&LQ&&GG~)Hi_FLAurq1v3^J#aIgQMzWJLN_ zpe-L6FBuP+ax#6$oI<i|Fh;<50*MP;4a0Qw9Sr5BtVz(p8gmlB3(0gGdkh&py6>0oDDvB|xJPG{H^7Ak zpjV;*(g{ppgD?6a3$Lpa|$P>g)kOhIj5hR(q0-kCS=$oL~ z6a3Wyveb3pKcV0pi6D8wi5_R(9cvQ!Dg~%m*5O*THRyM1?NHJX2LjkT>{L)`?--5n z+kQ<3g4j_EDuE(L0Dc9~2)OeWWH2iS!KON4+~KJWbmRCdA)7O5>X=hdS7=WMyK)K? z%tI&)kkS!=ky(hh?_eMhDXWD2gaFMA1Hx>hwU1RATi~i)VKktmpzh=-OncX$GO{PtXaZKh~}3yrHnN$j+RC_GM84>i5%Q5<%@t4Fza+ zBr%0lT}N{cD`?U7-C?B}jdyMaP~a3n3-OAhW_&%O;nJ{bG*Z0VveIbNm<$D}tXYHJ z4G@ryn=$miy7UyJxl|-ep>!PI?##k~q>&ngW|53XD;SU+2_jv;bK1S}TQ?@=v|!hb zNe-(z(1a*ftEVdjk#;Q{2;l7)P$=)EmD9R-);n!Y8gesa4)K0<=C7{We&!uj#Cn&J zV&+UEQ;TddsXx+{rT~W;f`l5wbN35t0CO{FHE=4(l&V4RFxgDkr4iUj#(*saa5o8f zEBZB{?<-j7-e}H5r0BcDD`<$(>t3$8L z!K;p_i&TP6NDMkLSn96~V9@V=k%5qHDuRSjN^?3+1)wRU@lw>Wvm)hi?#|$3ZK>kgj`EysZOWRF&I*{X-rMx_Y~mB1R1tb$>@yfZW5Fmm%4b2F#|bh7ncS_ zJmxO-2L!xU>K7cS%V89iaLKG*#k&ot>kP@LtHVi9SI7urP(!B$a2b_JrEoVg(}2z$ zgsbBdGe!_>2Hghcb@GhX6-D=N0$^lg+Igr7HH60f5MZL|Hl+cFMJ1d!ssE^K%BVqB zCrW0drU1u%V=^}-ok##6j*Adt8lTx66p{_$0Sr=E>oTVUUY*5@fIz|=Zca6{5$5bw z635kw*!Vk zl~6qh#n0Snw{*0QOcHmTZr(s3g1Nh39bH3Ua%u1(h#b7WiHMCim#MGGt4g+_EAPz6CTN0~nrg*vK|0VUaMZ?&SJL41IML5cN{)D;<;|%}l5Vq6Ipe zouf6tP2}bg3XnD4X+T(^ ziDgWBDsa#7Koe>lZZeG;zOTcHtZk5c+ou)q=M^cUDhHWb0_I+Pr%6n(HB9vlezLl|2HG<)9Z{{L_H zz*|%TJCES5JO0pk=KF6BVEh7tScbpukLv-~1Fi>L54aw1J>Yu4^?>UE*8{ExTo3%; z@xX&Ut(mQu+!kdmg$U~)4ON)!v=S|XtZV$lSM+001L zfc^=1-PE{ZB%*RS6vG;S$OvKwJu2&Z(1^*P5{h7zI$k`}v0}Wf*Uam)71Jw{$iAy+ ziK$i;Bf=HY85OtH*P0ap;*C&)^7LTBPy&g7f@LC6kX38(U}RWNu*%K~H}`#n<4u!FCIk z8yI;I3Jv(RAQ+3l5apzCD=pn4mR5&mO*{eYpS>=y;3DtJsynW;sL5y2iKL4cP!Sz> z@d7ShphQpCf~aU8ymRpah&$FGa^d0y82YAt{`D#7(X$OFD z@d7Shz{Lx=cmWqL;Nk^rh!SB*{&VpH9G@Km@VZKt2dmm(z{&$b9Yn#HF<= 60%) +- Abnormal price volatility events: **2** + +--- + +## High-Confidence Information Asymmetry Signals + +### 1. Will MegaETH launch a token by June 30, 2026? + +| Metric | Value | +|--------|-------| +| Info Asymmetry | **72%** | +| Direction | BUY Yes Token (Bullish) | +| Entry Price | 0.4200 (Odds 2.4x) | +| Trade Size | **$92,336** USDC | +| Detected At | 2026-04-15 14:23:07 | + +**Analysis**: High-ranked trader (#47, PnL $284K) making concentrated bets. Unannounced smart contract deployed by MegaETH team 6 hours prior. KOL insider chatter on Twitter preceded the trade. + +**Insider Evidence**: New ERC-20 contract deployed by MegaETH deployer wallet, not yet publicly announced. + +### 2. US forces enter Iran by April 30 + +| Metric | Value | +|--------|-------| +| Info Asymmetry | **65%** | +| Direction | BUY Yes Token (Bullish) | +| Entry Price | 0.3100 (Odds 3.2x) | +| Trade Size | **$46,500** USDC | +| Detected At | 2026-04-15 09:21:53 | + +**Analysis**: Top-50 trader with $500K+ PnL placing aggressive bets. Multiple high-ranked traders converging on the same direction. Trade occurred hours before Pentagon press briefing. + +### 3. Will the next Prime Minister of Hungary be Viktor Orban? + +| Metric | Value | +|--------|-------| +| Info Asymmetry | **61%** | +| Direction | BUY No Token (Bearish) | +| Entry Price | 0.2800 (Odds 3.6x) | +| Trade Size | **$65,699** USDC | +| Detected At | 2026-04-15 16:31:22 | + +**Analysis**: Verified trader with strong European politics track record betting against Orban. Telegram channels discussing potential coalition shift not yet covered by mainstream media. + +--- + +## Abnormal Price Volatility + +| Market | Direction | Change | Start Price | End Price | Detected At | +|--------|-----------|--------|-------------|-----------|-------------| +| EdgeX FDV above 500M one day... | Up | 35.2% | 22.00% | 57.20% | 2026-04-15 03:41 | +| Iran leadership change by Dec... | Up | 22.8% | 31.00% | 53.80% | 2026-04-15 11:15 | + +--- + +## Signal History + +| Metric | Value | +|--------|-------| +| Total Signals | 847 | +| Resolved | 312 | +| Correct | 198 | +| Win Rate | **63.5%** | +| Avg ROI | **+28.3%** | +| Theoretical Total PnL | **+42.71x** | + +### Performance by Confidence Tier + +| Confidence Range | Signals | Resolved | Win Rate | Avg ROI | +|-----------------|---------|----------|----------|---------| +| 0.8 - 1.0 | 23 | 15 | 80.0% | +52.1% | +| 0.6 - 0.8 | 89 | 47 | 68.1% | +35.7% | +| 0.4 - 0.6 | 284 | 132 | 59.1% | +18.4% | + +--- + +*This briefing was automatically generated by Polymarket Whale Watcher* diff --git a/docs/examples/sample_report.md b/docs/examples/sample_report.md new file mode 100644 index 0000000..10335f3 --- /dev/null +++ b/docs/examples/sample_report.md @@ -0,0 +1,139 @@ + +====================================================================== +# Whale Trade Analysis Report +====================================================================== + +**Generated at**: 2026-05-04 16:00:00 UTC + +## Trade Summary + +| Field | Details | +|-------|---------| +| **Market** | US x Iran diplomatic meeting by June 30, 2026? | +| **Trade Size** | $9,600.00 USDC | +| **Direction** | BUY Yes Token (Bullish) | +| **Trade Price** | 0.7142 (71.4%) | +| **Current Odds** | Yes: 69.5% | No: 30.5% | +| **Trade Time** | 2026-05-04 23:41:12 | +| **Trader Rank** | #1733 (PnL: $84,695.33) | + +====================================================================== + +## Full Analysis Report + +### Step 2: Trade Signal Analysis + +**Trader Profile Assessment:** +- **Rank #1733** — mid-tier experienced trader, not top-tier elite +- **PnL: $84,695** — solid positive track record across ~$4M lifetime volume +- **Avg trade size: $10,018** — this $9,600 trade is perfectly in-line with his normal size, not an unusual outlier +- **Large trade ratio: 66%** — consistent whale behavior, large trades are routine for this trader +- **Domain expertise: Strong Iran/geopolitics focus** — recent trades include Iranian regime fall (April/June), US-Iran meetings (April/June), Russia-Ukraine ceasefire, Ukraine Donbas. This is clearly a geopolitics specialist with deep focus on Iran specifically. + +**Trade Timing:** The trade was made at 23:41 UTC on May 4, 2026. Given the active diplomatic situation, this is a routine positioning trade, not an oddly-timed pre-announcement bet. + +--- + +### Step 3: Event-Related Position Analysis + +The whale has a **losing position** in the earlier "US x Iran diplomatic meeting by May 15, 2026?" market: +- Holding Yes tokens @ avg 65.29%, now priced at **15.50%** +- Cost basis: $8,534 | Current value: $2,026 | **PnL: -$6,508 (LOSS)** + +This is critical context: +1. The trader **bet heavily on a May 15 meeting** and that bet is **nearly dead** (15.5% odds) +2. The June 30 buy ($9,600) appears to be a **portfolio roll-forward** — doubling down on the same thesis with a later deadline to recoup losses +3. This is NOT a fresh, confident insider bet — it's a **loss recovery / thesis extension trade** +4. The pattern suggests the trader is sticking to a bullish Iran-diplomacy thesis despite being wrong on the nearer-term deadline + +--- + +### Step 4: Market Long/Short Analysis + +**Bulls (Yes side):** +- **anoin123** (Rank #2,682,942, PnL **-$5.54M**) — massive loser, leads the Yes side with $39K. This is a **red flag** — the biggest Yes holder is a chronic loser +- **ArmageddonRewardsBilly** (Rank #278, PnL $525K) — elite trader, $11K on Yes. This is the strongest bull signal +- **cezarn132** (the whale) — Rank #1733, $9.3K Yes + +**Bears (No side):** +- **aaron107** (Rank #48,390) — low-ranked, leads No +- **yungstalin** (Rank #1,969, PnL $73,759) — solid mid-tier trader on No side + +**Smart Money Consensus:** Mixed. One elite trader (Rank #278) is bullish, but the biggest Yes position belongs to a massive loser. The No side lacks elite concentration too. No clear smart money consensus signal in either direction. + +--- + +### Step 5: Information Gap Analysis + +**Public Information Summary:** +- There was already an indirect round of talks in Islamabad, Pakistan in late April 2026 (confirmed by Wikipedia) +- US envoy Steve Witkoff stated Washington is "in conversation" with Iran +- Iran's FM Araghchi has made public proposals for a 3-phase deal +- Market odds for June 30 meeting are ~69.5% — already well-priced for the optimistic scenario +- However, the **May 15 market is at just 15.5%**, showing markets believe a near-term meeting is unlikely +- Twitter data shows @faststocknewss noting "POLYMARKET ODDS OF A US-IRAN DIPLOMATIC MEETING ARE FADING" — the June 30 market was previously at 80%+ and has dropped to 66-69% + +**Key Information Assessment:** +- All available information about Iran-US diplomacy is extensively covered in public media +- The market at 69.5% appears to **reasonably price** the known information +- No evidence of non-public information advantage — this appears to be public-information-based analysis +- The trade is consistent with this trader's established Iran geopolitics thesis + +--- + +### Step 6: Historical Pattern + +This trader has a **clear and consistent pattern**: +- Bought "Iranian regime fall by April 30" at 0.86 → **SOLD at 0.98** (big win) +- Bought "Iranian regime fall by June 30" at 0.76 and 0.74 → Still holding +- Bet on "US x Iran diplomatic meeting by April 25/May 15" → **LOSING** at 15.5% + +The pattern suggests this trader uses a "timeline ladder" approach — betting on Iran-related events across multiple deadlines. When early deadlines miss, they roll to later ones. This reduces the information asymmetry signal significantly. + +--- + +## Step 7: Information Asymmetry Assessment + +```json +{ + "information_asymmetry_score": 0.32, + "trader_credibility": "MEDIUM", + "reasoning": "Trader cezarn132 (Rank #1733, PnL $84K) is a confirmed geopolitics specialist with deep Iran-focused trading history. However, several factors suppress the information asymmetry score: (1) The $9,600 trade is exactly in-line with this trader's average trade size ($10K), making it routine rather than an exceptional conviction bet. (2) The trader currently holds a heavily losing position in the nearer 'May 15' meeting market (down $6,508), and this June 30 buy appears to be a thesis roll-forward/loss recovery strategy rather than a fresh insider signal. (3) All supporting information (Islamabad talks, Witkoff statements, Iranian proposals) is extensively public and already priced into the ~69.5% market odds.", + "insider_evidence": "No insider evidence found. All information supporting the bullish Iran diplomacy view is widely reported in mainstream media. The trade is most consistent with a thesis-continuation/loss-recovery strategy following a losing May 15 position." +} +``` + +--- + +### Summary Table + +| Factor | Assessment | Signal Strength | +|--------|-----------|-----------------| +| Trader Rank/PnL | Rank #1733, $84K PnL | Moderate | +| Trade Size vs. Normal | ~$9.6K vs. avg $10K | Routine — neutral | +| Related Position | Heavy loser on May 15 meeting (-$6.5K) | Suppresses signal | +| Domain Expertise | Iran geopolitics specialist | Supportive | +| Public Info Coverage | Extensive public news on US-Iran talks | Reduces asymmetry | +| Market Pricing | Already at 69.5% — well-priced | Reduces asymmetry | +| Smart Money Bulls | Rank #278 also long | Modest support | +| Biggest Yes Holder | Rank #2.6M, -$5.5M PnL | Negative signal | +| **Overall** | **Likely thesis continuation, not insider signal** | **Low-Medium** | + +**Recommended Action: HOLD/PASS** — This trade reflects a geopolitics analyst rolling forward a losing thesis rather than a trader acting on non-public information. + +====================================================================== +## Information Asymmetry Assessment +====================================================================== + +| Field | Assessment | +|-------|------------| +| **Information Asymmetry** | Low Information Asymmetry (32%) | +| **Trader Credibility** | Medium Credibility (#1733) | + +**Key Evidence**: No insider evidence found. All information supporting the bullish Iran diplomacy view is widely reported in mainstream media. The trade is most consistent with a thesis-continuation/loss-recovery strategy following a losing May 15 position. + +**Reasoning**: Trader cezarn132 (Rank #1733, PnL $84K) is a confirmed geopolitics specialist with deep Iran-focused trading history. However, several factors suppress the information asymmetry score: (1) routine trade size, (2) losing position on nearer deadline, (3) extensive public information coverage, (4) declining market odds, (5) mixed smart money picture, (6) large geopolitical market with many participants. + +====================================================================== +Disclaimer: This report is AI-generated for informational purposes only and does not constitute investment advice. +====================================================================== diff --git a/docs/examples/sample_terminal.txt b/docs/examples/sample_terminal.txt new file mode 100644 index 0000000..e424d78 --- /dev/null +++ b/docs/examples/sample_terminal.txt @@ -0,0 +1,54 @@ +$ python -m src.main run + +╭──────────────────────────────────────────────────────────╮ +│ 🐋 Polymarket Whale Watcher │ +│ │ +│ Markets Monitored: 50 │ +│ Polling Interval: 15s │ +│ Min Trade Size: $1,000 │ +│ Price Range: 0 - 0.7 │ +╰──────────────────────────────────────────────────────────╯ + +[14:22:41] 🔄 Refreshing trending markets... Found 50 active markets +[14:22:43] 👀 Monitoring: Will MegaETH launch a token by June 30, 2026? +[14:22:43] 👀 Monitoring: US forces enter Iran by April 30 +[14:22:43] 👀 Monitoring: Will the next Prime Minister of Hungary be Viktor Orban? + ... (47 more markets) + +[14:22:51] 🐋 WHALE DETECTED on "Will MegaETH launch a token by June 30, 2026?" + BUY Yes @ 0.4200 | $92,336 USDC | Wallet: 0x7a3b...f91e + Anomaly Score: 0.78/1.00 + +[14:22:52] 🔍 Analyzing whale trade... + → Fetching trader ranking... Rank #47 (PnL: $284,521) + → Fetching trader history... 156 recent trades + → Fetching event positions... 3 related markets + → Fetching market top traders... 5 bulls, 5 bears + +[14:22:53] 🤖 LLM Analysis started (model: gemini-3-flash-preview) + → Tool call: search_web("MegaETH token launch date 2026") + → Tool call: search_twitter("MegaETH $METH token TGE") + → Tool call: get_protocol_tvl("megaeth") + → Tool call: get_contract_info("0x4f9b...2a1c") + → Tool call: search_telegram("MegaETH launch") + +[14:23:07] ✅ Analysis complete + Information Asymmetry Score: 0.72 (HIGH) + Recommendation: BUY Yes | Confidence: 0.75 + Report saved: reports/20260415/20260415_142307_BUY_92336USD_MegaETH_token.md + +[14:23:07] 📧 Email alert sent (IAS >= 60%) + +[14:23:15] 🐋 WHALE DETECTED on "US forces enter Iran by April 30" + BUY Yes @ 0.3100 | $46,500 USDC | Wallet: 0x2c8e...a4d2 + Anomaly Score: 0.71/1.00 + +[14:23:16] 🔍 Analyzing whale trade... + ... + +[14:37:41] 🔄 Refreshing trending markets... Found 48 active markets (2 resolved) + +[15:00:00] 📊 Resolution check: 3 markets resolved since last check + → "EdgeX FDV above 400M" resolved YES — Signal was CORRECT (ROI: +142%) + → "Will Trump talk to Rutte" resolved NO — Signal was INCORRECT + → "Over 9M committed to P2P" resolved YES — Signal was CORRECT (ROI: +67%) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3b66cfd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,49 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "polymarket-whale-watcher" +version = "0.1.0" +description = "AI-powered whale trade detection and analysis bot for Polymarket" +readme = "README.md" +requires-python = ">=3.10" +license = {text = "MIT"} +dependencies = [ + "httpx>=0.27.0", + "websockets>=13.0", + "requests>=2.28.0", + "pydantic>=2.0.0", + "pydantic-settings>=2.0.0", + "python-dotenv>=1.0.0", + "openai>=1.0.0", + "rich>=13.0.0", + "typer>=0.9.0", + "duckduckgo-search>=7.0.0", + "telethon>=1.36.0", + "fastapi>=0.100.0", + "uvicorn>=0.20.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "pytest-asyncio>=0.23.0", + "ruff>=0.1.0", + "mypy>=1.0.0", +] + +[project.scripts] +whale-watcher = "src.main:main" + +[tool.ruff] +line-length = 100 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "I", "W"] + +[tool.mypy] +python_version = "3.10" +warn_return_any = true +warn_unused_configs = true \ No newline at end of file diff --git a/reports/20260505/20260505_000000_BUY_9599USD_US_x_Iran_diplomatic_meeting_by_June_30_2026.md b/reports/20260505/20260505_000000_BUY_9599USD_US_x_Iran_diplomatic_meeting_by_June_30_2026.md new file mode 100644 index 0000000..780e222 --- /dev/null +++ b/reports/20260505/20260505_000000_BUY_9599USD_US_x_Iran_diplomatic_meeting_by_June_30_2026.md @@ -0,0 +1,143 @@ + +====================================================================== +# Whale Trade Analysis Report +====================================================================== + +**Generated at**: 2026-05-04 16:00:00 UTC + +## Trade Summary + +| Field | Details | +|-------|---------| +| **Market** | US x Iran diplomatic meeting by June 30, 2026? | +| **Trade Size** | $9,600.00 USDC | +| **Direction** | BUY Yes Token (Bullish) | +| **Trade Price** | 0.7142 (71.4%) | +| **Current Odds** | Yes: 69.5% | No: 30.5% | +| **Trade Time** | 2026-05-04 23:41:12 | +| **Trader Rank** | #1733 (PnL: $84,695.33) | + +====================================================================== + +I have comprehensive data. Let me now conduct a thorough analysis. + +--- + +## Full Analysis Report + +### Step 2: Trade Signal Analysis + +**Trader Profile Assessment:** +- **Rank #1733** — mid-tier experienced trader, not top-tier elite +- **PnL: $84,695** — solid positive track record across ~$4M lifetime volume +- **Avg trade size: $10,018** — this $9,600 trade is perfectly in-line with his normal size, not an unusual outlier +- **Large trade ratio: 66%** — consistent whale behavior, large trades are routine for this trader +- **Domain expertise: Strong Iran/geopolitics focus** — recent trades include Iranian regime fall (April/June), US-Iran meetings (April/June), Russia-Ukraine ceasefire, Ukraine Donbas. This is clearly a geopolitics specialist with deep focus on Iran specifically. + +**Trade Timing:** The trade was made at 23:41 UTC on May 4, 2026. Given the active diplomatic situation, this is a routine positioning trade, not an oddly-timed pre-announcement bet. + +--- + +### Step 3: Event-Related Position Analysis — 🚨 KEY FINDING + +The whale has a **losing position** in the earlier "US x Iran diplomatic meeting by May 15, 2026?" market: +- Holding Yes tokens @ avg 65.29%, now priced at **15.50%** +- Cost basis: $8,534 | Current value: $2,026 | **PnL: -$6,508 (LOSS)** + +This is critical context: +1. The trader **bet heavily on a May 15 meeting** and that bet is **nearly dead** (15.5% odds) +2. The June 30 buy ($9,600) appears to be a **portfolio roll-forward** — doubling down on the same thesis with a later deadline to recoup losses +3. This is NOT a fresh, confident insider bet — it's a **loss recovery / thesis extension trade** +4. The pattern suggests the trader is sticking to a bullish Iran-diplomacy thesis despite being wrong on the nearer-term deadline + +--- + +### Step 4: Market Long/Short Analysis + +**Bulls (Yes side):** +- **anoin123** (Rank #2,682,942, PnL **-$5.54M**) — massive loser, leads the Yes side with $39K. This is a **red flag** — the biggest Yes holder is a chronic loser +- **ArmageddonRewardsBilly** (Rank #278, PnL $525K) — elite trader, $11K on Yes. This is the strongest bull signal +- **cezarn132** (the whale) — Rank #1733, $9.3K Yes + +**Bears (No side):** +- **aaron107** (Rank #48,390) — low-ranked, leads No +- **yungstalin** (Rank #1,969, PnL $73,759) — solid mid-tier trader on No side + +**Smart Money Consensus:** Mixed. One elite trader (Rank #278) is bullish, but the biggest Yes position belongs to a massive loser. The No side lacks elite concentration too. No clear smart money consensus signal in either direction. + +--- + +### Step 5: Information Gap Analysis + +**Public Information Summary:** +- There was already an indirect round of talks in Islamabad, Pakistan in late April 2026 (confirmed by Wikipedia) +- US envoy Steve Witkoff stated Washington is "in conversation" with Iran +- Iran's FM Araghchi has made public proposals for a 3-phase deal +- Market odds for June 30 meeting are ~69.5% — already well-priced for the optimistic scenario +- However, the **May 15 market is at just 15.5%**, showing markets believe a near-term meeting is unlikely +- Twitter data shows @faststocknewss noting "POLYMARKET ODDS OF A US-IRAN DIPLOMATIC MEETING ARE FADING" — the June 30 market was previously at 80%+ and has dropped to 66-69% + +**Key Information Assessment:** +- All available information about Iran-US diplomacy is extensively covered in public media +- The market at 69.5% appears to **reasonably price** the known information +- No evidence of non-public information advantage — this appears to be public-information-based analysis +- The trade is consistent with this trader's established Iran geopolitics thesis + +--- + +### Step 6: Historical Pattern + +This trader has a **clear and consistent pattern**: +- Bought "Iranian regime fall by April 30" at 0.86 → **SOLD at 0.98** (big win ✅) +- Bought "Iranian regime fall by June 30" at 0.76 and 0.74 → Still holding +- Bet on "US x Iran diplomatic meeting by April 25/May 15" → **LOSING** at 15.5% + +The pattern suggests this trader uses a "timeline ladder" approach — betting on Iran-related events across multiple deadlines. When early deadlines miss, they roll to later ones. This reduces the information asymmetry signal significantly. + +--- + +## Step 7: Information Asymmetry Assessment + +```json +{ + "information_asymmetry_score": 0.32, + "trader_credibility": "MEDIUM", + "reasoning": "Trader cezarn132 (Rank #1733, PnL $84K) is a confirmed geopolitics specialist with deep Iran-focused trading history. However, several factors suppress the information asymmetry score: (1) The $9,600 trade is exactly in-line with this trader's average trade size ($10K), making it routine rather than an exceptional conviction bet. (2) The trader currently holds a heavily losing position in the nearer 'May 15' meeting market (down $6,508), and this June 30 buy appears to be a thesis roll-forward/loss recovery strategy rather than a fresh insider signal. (3) All supporting information (Islamabad talks, Witkoff statements, Iranian proposals) is extensively public and already priced into the ~69.5% market odds. (4) The market was previously at 80%+ and has been declining — this is a contrarian hold, not a momentum play. (5) The smart money picture is mixed — the largest Yes holder is a chronic losing trader. (6) This is a large geopolitical market with many participants, where public analysis rather than non-public information drives most trades. The trader demonstrates domain expertise and pattern consistency, but no evidence of non-public information access.", + "insider_evidence": "No insider evidence found. All information supporting the bullish Iran diplomacy view (Islamabad round 1 talks, Witkoff statements, Iranian FM proposals, Oman mediation) is widely reported in mainstream media. The trade is most consistent with a thesis-continuation/loss-recovery strategy following a losing May 15 position, rather than a fresh signal based on non-public information." +} +``` + +--- + +### Summary Table + +| Factor | Assessment | Signal Strength | +|--------|-----------|-----------------| +| Trader Rank/PnL | Rank #1733, $84K PnL | Moderate ✅ | +| Trade Size vs. Normal | ~$9.6K vs. avg $10K | Routine — neutral ➖ | +| Related Position | Heavy loser on May 15 meeting (-$6.5K) | Suppresses signal ❌ | +| Domain Expertise | Iran geopolitics specialist | Supportive ✅ | +| Public Info Coverage | Extensive public news on US-Iran talks | Reduces asymmetry ❌ | +| Market Pricing | Already at 69.5% — well-priced | Reduces asymmetry ❌ | +| Smart Money Bulls | Rank #278 also long | Modest support ✅ | +| Biggest Yes Holder | Rank #2.6M, -$5.5M PnL | Negative signal ❌ | +| **Overall** | **Likely thesis continuation, not insider signal** | **Low-Medium** | + +**Recommended Action: HOLD/PASS** — This trade reflects a geopolitics analyst rolling forward a losing thesis rather than a trader acting on non-public information. The market is already fairly priced at ~69.5% based on extensive public information about ongoing US-Iran diplomatic channels. + +====================================================================== +## Information Asymmetry Assessment +====================================================================== + +| Field | Assessment | +|-------|------------| +| **Information Asymmetry** | Low Information Asymmetry (32%) | +| **Trader Credibility** | Medium Credibility (#1733) | + +**Key Evidence**: No insider evidence found. All information supporting the bullish Iran diplomacy view (Islamabad round 1 talks, Witkoff statements, Iranian FM proposals, Oman mediation) is widely reported in mainstream media. The trade is most consistent with a thesis-continuation/loss-recovery strategy following a losing May 15 position, rather than a fresh signal based on non-public information. + +**Reasoning**: Trader cezarn132 (Rank #1733, PnL $84K) is a confirmed geopolitics specialist with deep Iran-focused trading history. However, several factors suppress the information asymmetry score: (1) The $9,600 trade is exactly in-line with this trader's average trade size ($10K), making it routine rather than an exceptional conviction bet. (2) The trader currently holds a heavily losing position in the nearer 'May 15' meeting market (down $6,508), and this June 30 buy appears to be a thesis roll-forward/loss recovery strategy rather than a fresh insider signal. (3) All supporting information (Islamabad talks, Witkoff statements, Iranian proposals) is extensively public and already priced into the ~69.5% market odds. (4) The market was previously at 80%+ and has been declining — this is a contrarian hold, not a momentum play. (5) The smart money picture is mixed — the largest Yes holder is a chronic losing trader. (6) This is a large geopolitical market with many participants, where public analysis rather than non-public information drives most trades. The trader demonstrates domain expertise and pattern consistency, but no evidence of non-public information access. + +====================================================================== +Disclaimer: This report is AI-generated for informational purposes only and does not constitute investment advice. +====================================================================== diff --git a/scripts/telegram_auth.py b/scripts/telegram_auth.py new file mode 100644 index 0000000..38347d1 --- /dev/null +++ b/scripts/telegram_auth.py @@ -0,0 +1,42 @@ +""" +One-time script to generate a Telegram session string. + +Usage: + python scripts/telegram_auth.py + +You'll need: +1. Go to https://my.telegram.org and create an application +2. Get your api_id and api_hash +3. Run this script and enter your phone number +4. Enter the verification code sent to your Telegram +5. Copy the session string into your .env file as TELEGRAM_SESSION_STRING +""" +import asyncio +from telethon import TelegramClient +from telethon.sessions import StringSession + + +async def main(): + print("=== Telegram Session Generator ===\n") + print("Get api_id and api_hash from https://my.telegram.org\n") + + api_id = input("Enter api_id: ").strip() + api_hash = input("Enter api_hash: ").strip() + + client = TelegramClient(StringSession(), int(api_id), api_hash) + + await client.start() + + session_string = client.session.save() + + print(f"\n{'='*60}") + print("Your session string (add to .env):\n") + print(f"TELEGRAM_SESSION_STRING={session_string}") + print(f"\n{'='*60}") + print("Keep this string secret! It provides access to your account.") + + await client.disconnect() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..bc80047 --- /dev/null +++ b/setup.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env bash +# Polymarket Whale Watcher - One-Click Setup +# Usage: chmod +x setup.sh && ./setup.sh + +set -e + +CYAN='\033[0;36m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +DIM='\033[2m' +BOLD='\033[1m' +NC='\033[0m' + +echo -e "${CYAN}" +echo "╭──────────────────────────────────────────────────────────╮" +echo "│ 🐋 Polymarket Whale Watcher Setup │" +echo "╰──────────────────────────────────────────────────────────╯" +echo -e "${NC}" + +# ============================================================ +# Step 1: Check Python +# ============================================================ +echo -e "${CYAN}[1/4]${NC} Checking Python version..." +if ! command -v python3 &> /dev/null; then + echo -e "${RED}Error: Python 3 is required but not installed.${NC}" + echo " Install from https://www.python.org/downloads/" + exit 1 +fi + +PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') +PYTHON_MAJOR=$(echo "$PYTHON_VERSION" | cut -d. -f1) +PYTHON_MINOR=$(echo "$PYTHON_VERSION" | cut -d. -f2) + +if [ "$PYTHON_MAJOR" -lt 3 ] || ([ "$PYTHON_MAJOR" -eq 3 ] && [ "$PYTHON_MINOR" -lt 10 ]); then + echo -e "${RED}Error: Python 3.10+ required, found $PYTHON_VERSION${NC}" + exit 1 +fi +echo -e " ${GREEN}✓${NC} Python $PYTHON_VERSION" + +# ============================================================ +# Step 2: Virtual environment +# ============================================================ +echo -e "${CYAN}[2/4]${NC} Setting up virtual environment..." +if [ ! -d "venv" ]; then + python3 -m venv venv + echo -e " ${GREEN}✓${NC} Virtual environment created" +else + echo -e " ${GREEN}✓${NC} Virtual environment already exists" +fi + +# ============================================================ +# Step 3: Install dependencies +# ============================================================ +echo -e "${CYAN}[3/4]${NC} Installing dependencies..." +source venv/bin/activate +pip install --upgrade pip -q +pip install -r requirements.txt -q +echo -e " ${GREEN}✓${NC} Dependencies installed" + +# Create data directories +mkdir -p data reports daily_briefings + +# ============================================================ +# Step 4: API Key Configuration +# ============================================================ +echo -e "${CYAN}[4/4]${NC} Configuring API keys..." +echo "" + +if [ -f ".env" ]; then + echo -e " ${YELLOW}Found existing .env file.${NC}" + read -p " Overwrite and reconfigure? [y/N] " overwrite + if [[ ! "$overwrite" =~ ^[Yy]$ ]]; then + echo -e " ${GREEN}✓${NC} Keeping existing .env" + echo "" + echo -e "${GREEN}Setup complete! Run:${NC}" + echo " source venv/bin/activate" + echo " python -m src.main run" + exit 0 + fi +fi + +cp .env.example .env + +# Helper function: prompt for a key and write to .env +set_key() { + local var_name="$1" + local prompt_text="$2" + local url="$3" + local required="$4" + + echo "" + if [ "$required" = "required" ]; then + echo -e " ${BOLD}${var_name}${NC} ${RED}(required)${NC}" + else + echo -e " ${BOLD}${var_name}${NC} ${DIM}(optional, press Enter to skip)${NC}" + fi + echo -e " ${DIM}$prompt_text${NC}" + echo -e " ${DIM}→ $url${NC}" + + while true; do + read -p " Key: " key_value + if [ -z "$key_value" ]; then + if [ "$required" = "required" ]; then + echo -e " ${RED}This key is required. Please enter a value.${NC}" + continue + else + echo -e " ${DIM}Skipped${NC}" + return + fi + fi + break + done + + # Replace the placeholder in .env + if grep -q "^${var_name}=" .env; then + sed -i.bak "s|^${var_name}=.*|${var_name}=${key_value}|" .env + elif grep -q "^# ${var_name}=" .env; then + sed -i.bak "s|^# ${var_name}=.*|${var_name}=${key_value}|" .env + else + echo "${var_name}=${key_value}" >> .env + fi + rm -f .env.bak + echo -e " ${GREEN}✓ Saved${NC}" +} + +echo -e "${CYAN}╭──────────────────────────────────────────────────────────╮${NC}" +echo -e "${CYAN}│ API Key Configuration │${NC}" +echo -e "${CYAN}│ │${NC}" +echo -e "${CYAN}│ The more keys you add, the better the analysis. │${NC}" +echo -e "${CYAN}│ Only LLM_API_KEY is required. Others are optional but │${NC}" +echo -e "${CYAN}│ strongly recommended for full coverage. │${NC}" +echo -e "${CYAN}╰──────────────────────────────────────────────────────────╯${NC}" + +# --- Required --- +echo "" +echo -e "${YELLOW}━━━ Required (LLM Engine) ━━━${NC}" + +set_key "LLM_API_KEY" \ + "Powers the LLM analysis (OpenAI-compatible)" \ + "https://platform.openai.com/api-keys" \ + "required" + +# --- Core Search --- +echo "" +echo -e "${YELLOW}━━━ Web Search (strongly recommended) ━━━${NC}" +echo -e "${DIM} Without these, LLM falls back to DuckDuckGo (lower quality)${NC}" + +set_key "TAVILY_API_KEY" \ + "Best web search quality, 1000 free searches/month" \ + "https://app.tavily.com/home" + +set_key "SERPER_API_KEY" \ + "Google search fallback, 2500 free searches" \ + "https://serper.dev" + +# --- Social Sentiment --- +echo "" +echo -e "${YELLOW}━━━ Social Sentiment ━━━${NC}" + +set_key "TWITTER_API_KEY" \ + "Twitter/X sentiment & breaking news search" \ + "https://developer.x.com/en/portal/dashboard" + +# --- Financial Data --- +echo "" +echo -e "${YELLOW}━━━ Financial & On-Chain Data ━━━${NC}" + +set_key "POLYGON_API_KEY" \ + "Stock, ETF, forex, commodities data (free tier)" \ + "https://polygon.io/dashboard/signup" + +set_key "FRED_API_KEY" \ + "US economic indicators (free, instant approval)" \ + "https://fred.stlouisfed.org/docs/api/api_key.html" + +set_key "ETHERSCAN_API_KEY" \ + "On-chain wallet & contract analysis (free tier)" \ + "https://etherscan.io/myapikey" + +set_key "CONGRESS_API_KEY" \ + "US legislation & bills tracking (free)" \ + "https://api.congress.gov/sign-up/" + +# --- Summary --- +echo "" +echo "" + +# Count configured keys +configured=0 +total=8 +for var in LLM_API_KEY TAVILY_API_KEY SERPER_API_KEY TWITTER_API_KEY POLYGON_API_KEY FRED_API_KEY ETHERSCAN_API_KEY CONGRESS_API_KEY; do + val=$(grep "^${var}=" .env 2>/dev/null | cut -d= -f2-) + if [ -n "$val" ] && [ "$val" != "your_api_key_here" ]; then + configured=$((configured + 1)) + fi +done + +echo -e "${GREEN}╭──────────────────────────────────────────────────────────╮${NC}" +echo -e "${GREEN}│ ✅ Setup Complete! │${NC}" +echo -e "${GREEN}│ │${NC}" +echo -e "${GREEN}│ API Keys configured: ${configured}/${total} │${NC}" +echo -e "${GREEN}╰──────────────────────────────────────────────────────────╯${NC}" + +if [ "$configured" -lt 4 ]; then + echo "" + echo -e " ${YELLOW}Tip: More API keys = better analysis coverage.${NC}" + echo -e " ${YELLOW}You can edit .env anytime to add more keys later.${NC}" +fi + +echo "" +echo " Start monitoring:" +echo -e " ${BOLD}source venv/bin/activate${NC}" +echo -e " ${BOLD}python -m src.main run${NC}" +echo "" +echo " Other commands:" +echo " python -m src.main check-markets # View trending markets" +echo " python -m src.main dashboard # Web dashboard" +echo " python -m src.main briefing --today # Daily briefing" +echo "" \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..8eaa512 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1 @@ +"""Polymarket Whale Watcher - AI-powered whale trade detection and analysis.""" diff --git a/src/config/__init__.py b/src/config/__init__.py new file mode 100644 index 0000000..b3838a7 --- /dev/null +++ b/src/config/__init__.py @@ -0,0 +1,4 @@ +"""Configuration module.""" +from .settings import Settings, get_settings + +__all__ = ["Settings", "get_settings"] diff --git a/src/config/settings.py b/src/config/settings.py new file mode 100644 index 0000000..5ef74f9 --- /dev/null +++ b/src/config/settings.py @@ -0,0 +1,95 @@ +"""Application settings and configuration.""" +import os +from functools import lru_cache +from typing import Optional + +from dotenv import load_dotenv +from pydantic import Field +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + """Application settings loaded from environment variables.""" + + # LLM API (OpenAI-compatible proxy) + llm_api_key: str = Field(default="", alias="LLM_API_KEY") + llm_base_url: str = Field(default="https://generativelanguage.googleapis.com/v1beta/openai/", alias="LLM_BASE_URL") + + # Twitter API (for social sentiment search) + twitter_api_key: str = Field(default="", alias="TWITTER_API_KEY") + + # Tavily API (for web search, replaces Google Search) + tavily_api_key: str = Field(default="", alias="TAVILY_API_KEY") + + # Serper API (web search fallback) + serper_api_key: str = Field(default="", alias="SERPER_API_KEY") + + # FRED API (macroeconomic data) + fred_api_key: str = Field(default="", alias="FRED_API_KEY") + + # Polygon.io API (stocks, forex, commodities) + polygon_api_key: str = Field(default="", alias="POLYGON_API_KEY") + + # Congress.gov API (U.S. legislation) + congress_api_key: str = Field(default="", alias="CONGRESS_API_KEY") + + # Etherscan API (on-chain data) + etherscan_api_key: str = Field(default="", alias="ETHERSCAN_API_KEY") + + # Telegram API (crypto channel monitoring) + telegram_api_id: str = Field(default="", alias="TELEGRAM_API_ID") + telegram_api_hash: str = Field(default="", alias="TELEGRAM_API_HASH") + telegram_session_string: str = Field(default="", alias="TELEGRAM_SESSION_STRING") + telegram_channels: str = Field(default="", alias="TELEGRAM_CHANNELS") + + + # MongoDB + mongodb_uri: str = Field(default="mongodb://localhost:27017/whale_watcher", alias="MONGODB_URI") + + # SQLite database + db_path: str = Field(default="data/signals.db", alias="DB_PATH") + + # HTTP Proxy (for users behind firewalls) + http_proxy: str = Field(default="", alias="HTTP_PROXY") + + # Whale Detection Settings + min_trade_size_usd: float = Field(default=1000.0, alias="MIN_TRADE_SIZE_USD") + min_price: float = Field(default=0.10, alias="MIN_PRICE") + max_price: float = Field(default=0.90, alias="MAX_PRICE") + + # Monitoring Settings + trending_markets_limit: int = Field(default=50, alias="TRENDING_MARKETS_LIMIT") + + # Market coverage (volume thresholds for monitoring list) + full_market_scan: bool = Field(default=True, alias="FULL_MARKET_SCAN") + tier1_volume_min: float = Field(default=500_000, alias="TIER1_VOLUME_MIN") + tier2_volume_min: float = Field(default=10_000, alias="TIER2_VOLUME_MIN") + tier3_volume_min: float = Field(default=1_000, alias="TIER3_VOLUME_MIN") + + # LLM Settings + llm_model: str = Field(default="gemini-3.1-pro-preview", alias="LLM_MODEL") + llm_temperature: float = Field(default=0.0, alias="LLM_TEMPERATURE") + + + # Email notification + email_smtp_server: str = Field(default="smtp.qq.com", alias="EMAIL_SMTP_SERVER") + email_smtp_port: int = Field(default=465, alias="EMAIL_SMTP_PORT") + email_sender: str = Field(default="", alias="EMAIL_SENDER") + email_password: str = Field(default="", alias="EMAIL_PASSWORD") + email_recipient: str = Field(default="1253608463@qq.com,lyk@sii.edu.cn,1286874010@qq.com,tianhao.alex.huang@gmail.com", alias="EMAIL_RECIPIENT") + email_enabled: bool = Field(default=False, alias="EMAIL_ENABLED") + + # Logging + log_level: str = Field(default="INFO", alias="LOG_LEVEL") + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + extra = "ignore" + + +@lru_cache +def get_settings() -> Settings: + """Get cached settings instance.""" + load_dotenv() + return Settings() \ No newline at end of file diff --git a/src/dashboard.py b/src/dashboard.py new file mode 100644 index 0000000..7cd570c --- /dev/null +++ b/src/dashboard.py @@ -0,0 +1,224 @@ +"""FastAPI dashboard for signal performance tracking.""" +import logging +from typing import Optional + +from fastapi import FastAPI, Query +from fastapi.responses import HTMLResponse + +from src.config import get_settings +from src.db.database import SignalDatabase +from src.services.stats_engine import StatsEngine + +logger = logging.getLogger(__name__) + +app = FastAPI(title="Polymarket Whale Watcher - Signal Dashboard") + + +def _get_db() -> SignalDatabase: + settings = get_settings() + return SignalDatabase(settings.db_path) + + +@app.get("/api/stats") +def api_stats(): + """Overall signal performance statistics.""" + db = _get_db() + return db.get_stats() + + +@app.get("/api/stats/tiers") +def api_stats_tiers(): + """Signal stats by information_asymmetry_score tier.""" + db = _get_db() + return db.get_stats_by_tier() + + +@app.get("/api/signals") +def api_signals( + limit: int = Query(50, ge=1, le=500), + offset: int = Query(0, ge=0), +): + """Paginated signal list (newest first).""" + db = _get_db() + signals = db.get_all_signals(limit=limit, offset=offset) + return [s.model_dump(mode="json") for s in signals] + + +@app.get("/api/signals/best-worst") +def api_best_worst(n: int = Query(5, ge=1, le=20)): + """Best and worst signals by theoretical ROI.""" + db = _get_db() + result = db.get_best_worst(n=n) + return { + "best": [s.model_dump(mode="json") for s in result["best"]], + "worst": [s.model_dump(mode="json") for s in result["worst"]], + } + + +@app.get("/", response_class=HTMLResponse) +def dashboard_page(): + """HTML dashboard page.""" + return HTML_TEMPLATE + + +HTML_TEMPLATE = """ + + + + +Polymarket Whale Watcher - Signal Dashboard + + + + +

Polymarket Whale Watcher

+

Signal Performance Dashboard

+

+ +
Loading...
+ + + + +""" \ No newline at end of file diff --git a/src/db/__init__.py b/src/db/__init__.py new file mode 100644 index 0000000..60049d6 --- /dev/null +++ b/src/db/__init__.py @@ -0,0 +1,4 @@ +"""Database module for signal storage and tracking.""" +from src.db.database import SignalDatabase + +__all__ = ["SignalDatabase"] diff --git a/src/db/database.py b/src/db/database.py new file mode 100644 index 0000000..3855214 --- /dev/null +++ b/src/db/database.py @@ -0,0 +1,404 @@ +"""SQLite database for anomaly signal storage and resolution tracking.""" +import json +import logging +import sqlite3 +from datetime import datetime +from pathlib import Path +from typing import List, Optional + +from src.models.anomaly_signal import AnomalySignal +from src.models.trade import TraderRanking, TraderHistory + +logger = logging.getLogger(__name__) + + +class SignalDatabase: + """SQLite-backed storage for anomaly signals with resolution tracking.""" + + def __init__(self, db_path: str = "data/signals.db"): + self.db_path = Path(db_path) + self.db_path.parent.mkdir(parents=True, exist_ok=True) + self._init_db() + + def _get_conn(self) -> sqlite3.Connection: + conn = sqlite3.connect(str(self.db_path)) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA journal_mode=WAL") + return conn + + def _init_db(self): + with self._get_conn() as conn: + # Migrate: rename old column if it exists + try: + conn.execute( + "ALTER TABLE signals RENAME COLUMN insider_trading_likelihood TO information_asymmetry_score" + ) + logger.info("Migrated column: insider_trading_likelihood -> information_asymmetry_score") + except Exception: + pass # Column already renamed or table doesn't exist yet + + conn.execute(""" + CREATE TABLE IF NOT EXISTS signals ( + id TEXT, + market_id TEXT NOT NULL, + market_question TEXT NOT NULL, + market_slug TEXT, + condition_id TEXT, + transaction_hash TEXT UNIQUE NOT NULL, + trade_timestamp INTEGER NOT NULL, + trade_side TEXT NOT NULL, + trade_price REAL NOT NULL, + trade_size_usd REAL NOT NULL, + trade_outcome TEXT NOT NULL, + trader_wallet TEXT, + trader_ranking_json TEXT, + trader_history_json TEXT, + information_asymmetry_score REAL NOT NULL DEFAULT 0.0, + reasoning TEXT DEFAULT '', + insider_evidence TEXT DEFAULT '', + detected_at TEXT NOT NULL, + market_resolved INTEGER DEFAULT 0, + market_resolved_at TEXT, + resolved_outcome TEXT, + signal_correct INTEGER, + theoretical_roi REAL + ) + """) + conn.execute(""" + CREATE INDEX IF NOT EXISTS idx_signals_market_id + ON signals(market_id) + """) + conn.execute(""" + CREATE INDEX IF NOT EXISTS idx_signals_market_resolved + ON signals(market_resolved) + """) + conn.execute(""" + CREATE INDEX IF NOT EXISTS idx_signals_likelihood + ON signals(information_asymmetry_score DESC) + """) + + def insert_signal(self, signal: AnomalySignal) -> bool: + """Insert a signal, deduplicating by transaction_hash. Returns True if inserted.""" + try: + with self._get_conn() as conn: + conn.execute(""" + INSERT OR IGNORE INTO signals ( + id, market_id, market_question, market_slug, condition_id, + transaction_hash, trade_timestamp, trade_side, trade_price, + trade_size_usd, trade_outcome, trader_wallet, + trader_ranking_json, trader_history_json, + information_asymmetry_score, reasoning, insider_evidence, + detected_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + signal.id, + signal.market_id, + signal.market_question, + signal.market_slug, + signal.condition_id, + signal.transaction_hash, + signal.trade_timestamp, + signal.trade_side, + signal.trade_price, + signal.trade_size_usd, + signal.trade_outcome, + signal.trader_wallet, + signal.trader_ranking.model_dump_json() if signal.trader_ranking else None, + signal.trader_history.model_dump_json() if signal.trader_history else None, + signal.information_asymmetry_score, + signal.reasoning, + signal.insider_evidence, + signal.detected_at.isoformat(), + )) + return conn.total_changes > 0 + except sqlite3.IntegrityError: + logger.debug(f"Signal already exists: {signal.transaction_hash}") + return False + except Exception as e: + logger.error(f"Failed to insert signal: {e}") + return False + + def _row_to_signal(self, row: sqlite3.Row) -> AnomalySignal: + """Convert a database row to an AnomalySignal.""" + trader_ranking = None + if row["trader_ranking_json"]: + try: + trader_ranking = TraderRanking.model_validate_json(row["trader_ranking_json"]) + except Exception: + pass + + trader_history = None + if row["trader_history_json"]: + try: + trader_history = TraderHistory.model_validate_json(row["trader_history_json"]) + except Exception: + pass + + return AnomalySignal( + id=row["id"] or "", + market_id=row["market_id"], + market_question=row["market_question"], + market_slug=row["market_slug"], + condition_id=row["condition_id"], + transaction_hash=row["transaction_hash"], + trade_timestamp=row["trade_timestamp"], + trade_side=row["trade_side"], + trade_price=row["trade_price"], + trade_size_usd=row["trade_size_usd"], + trade_outcome=row["trade_outcome"], + trader_wallet=row["trader_wallet"], + trader_ranking=trader_ranking, + trader_history=trader_history, + information_asymmetry_score=row["information_asymmetry_score"], + reasoning=row["reasoning"] or "", + insider_evidence=row["insider_evidence"] or "", + detected_at=datetime.fromisoformat(row["detected_at"]), + market_resolved=bool(row["market_resolved"]), + market_resolved_at=( + datetime.fromisoformat(row["market_resolved_at"]) + if row["market_resolved_at"] else None + ), + resolved_outcome=row["resolved_outcome"], + signal_correct=bool(row["signal_correct"]) if row["signal_correct"] is not None else None, + theoretical_roi=row["theoretical_roi"], + ) + + def get_signals_for_market( + self, + market_id: str, + top_recent: int = 5, + top_likelihood: int = 5, + min_likelihood: float = 0.4, + ) -> List[AnomalySignal]: + """Get top recent + top likelihood signals for a market, deduplicated. + Only returns signals with likelihood >= min_likelihood (for LLM context).""" + with self._get_conn() as conn: + # Top recent (above threshold only) + recent_rows = conn.execute( + "SELECT * FROM signals WHERE market_id = ? AND information_asymmetry_score >= ? ORDER BY trade_timestamp DESC LIMIT ?", + (market_id, min_likelihood, top_recent), + ).fetchall() + + # Top likelihood (above threshold only) + likelihood_rows = conn.execute( + "SELECT * FROM signals WHERE market_id = ? AND information_asymmetry_score >= ? ORDER BY information_asymmetry_score DESC LIMIT ?", + (market_id, min_likelihood, top_likelihood), + ).fetchall() + + seen = set() + combined = [] + for row in list(recent_rows) + list(likelihood_rows): + tx_hash = row["transaction_hash"] + if tx_hash not in seen: + seen.add(tx_hash) + combined.append(self._row_to_signal(row)) + + combined.sort(key=lambda s: s.trade_timestamp, reverse=True) + return combined + + def get_unresolved_market_ids(self) -> List[str]: + """Return distinct market_ids that have unresolved signals.""" + with self._get_conn() as conn: + rows = conn.execute( + "SELECT DISTINCT market_id FROM signals WHERE market_resolved = 0" + ).fetchall() + return [row["market_id"] for row in rows] + + def mark_market_resolved( + self, + market_id: str, + resolved_outcome: str, + resolved_at: datetime, + ) -> int: + """ + Mark all signals for a market as resolved and compute correctness/ROI. + Returns number of updated rows. + """ + with self._get_conn() as conn: + rows = conn.execute( + "SELECT transaction_hash, trade_outcome, trade_price FROM signals WHERE market_id = ? AND market_resolved = 0", + (market_id,), + ).fetchall() + + updated = 0 + for row in rows: + correct = row["trade_outcome"] == resolved_outcome + if correct: + roi = (1.0 - row["trade_price"]) / row["trade_price"] if row["trade_price"] > 0 else 0.0 + else: + roi = -1.0 + + conn.execute(""" + UPDATE signals SET + market_resolved = 1, + market_resolved_at = ?, + resolved_outcome = ?, + signal_correct = ?, + theoretical_roi = ? + WHERE transaction_hash = ? + """, ( + resolved_at.isoformat(), + resolved_outcome, + int(correct), + roi, + row["transaction_hash"], + )) + updated += 1 + + return updated + + def get_stats(self) -> dict: + """Aggregate statistics: total, resolved, correct, win_rate, avg_roi.""" + with self._get_conn() as conn: + row = conn.execute(""" + SELECT + COUNT(*) as total, + SUM(CASE WHEN market_resolved = 1 THEN 1 ELSE 0 END) as resolved, + SUM(CASE WHEN signal_correct = 1 THEN 1 ELSE 0 END) as correct, + AVG(CASE WHEN market_resolved = 1 THEN theoretical_roi END) as avg_roi, + SUM(CASE WHEN market_resolved = 1 THEN theoretical_roi ELSE 0 END) as total_pnl + FROM signals + """).fetchone() + + total = row["total"] + resolved = row["resolved"] or 0 + correct = row["correct"] or 0 + win_rate = correct / resolved if resolved > 0 else 0.0 + + last_updated = None + if total > 0: + last_row = conn.execute( + "SELECT detected_at FROM signals ORDER BY detected_at DESC LIMIT 1" + ).fetchone() + last_updated = last_row["detected_at"] if last_row else None + + return { + "total_signals": total, + "resolved": resolved, + "correct": correct, + "win_rate": win_rate, + "avg_roi": row["avg_roi"] or 0.0, + "total_theoretical_pnl": row["total_pnl"] or 0.0, + "last_updated": last_updated, + } + + def get_stats_by_tier(self) -> List[dict]: + """Stats grouped by information_asymmetry_score tiers.""" + tiers = [ + ("0.4-0.6", 0.4, 0.6), + ("0.6-0.8", 0.6, 0.8), + ("0.8-1.0", 0.8, 1.01), + ] + results = [] + with self._get_conn() as conn: + for label, low, high in tiers: + row = conn.execute(""" + SELECT + COUNT(*) as total, + SUM(CASE WHEN market_resolved = 1 THEN 1 ELSE 0 END) as resolved, + SUM(CASE WHEN signal_correct = 1 THEN 1 ELSE 0 END) as correct, + AVG(CASE WHEN market_resolved = 1 THEN theoretical_roi END) as avg_roi + FROM signals + WHERE information_asymmetry_score >= ? AND information_asymmetry_score < ? + """, (low, high)).fetchone() + + resolved = row["resolved"] or 0 + correct = row["correct"] or 0 + results.append({ + "tier": label, + "total": row["total"], + "resolved": resolved, + "correct": correct, + "win_rate": correct / resolved if resolved > 0 else 0.0, + "avg_roi": row["avg_roi"] or 0.0, + }) + + return results + + def get_all_signals(self, limit: int = 50, offset: int = 0) -> List[AnomalySignal]: + """Paginated query of all signals, newest first.""" + with self._get_conn() as conn: + rows = conn.execute( + "SELECT * FROM signals ORDER BY detected_at DESC LIMIT ? OFFSET ?", + (limit, offset), + ).fetchall() + return [self._row_to_signal(row) for row in rows] + + def get_all_market_ids(self) -> List[str]: + """Get all distinct market IDs.""" + with self._get_conn() as conn: + rows = conn.execute("SELECT DISTINCT market_id FROM signals").fetchall() + return [row["market_id"] for row in rows] + + def get_signal_count(self, market_id: Optional[str] = None) -> int: + """Count signals, optionally filtered by market_id.""" + with self._get_conn() as conn: + if market_id: + row = conn.execute( + "SELECT COUNT(*) as cnt FROM signals WHERE market_id = ?", (market_id,) + ).fetchone() + else: + row = conn.execute("SELECT COUNT(*) as cnt FROM signals").fetchone() + return row["cnt"] + + def cleanup_old_signals(self, max_age_days: int = 30) -> int: + """Remove signals older than max_age_days. Returns count removed.""" + from datetime import timedelta + cutoff = (datetime.utcnow() - timedelta(days=max_age_days)).isoformat() + with self._get_conn() as conn: + cursor = conn.execute( + "DELETE FROM signals WHERE detected_at < ?", (cutoff,) + ) + return cursor.rowcount + + def get_recent_resolved(self, limit: int = 20) -> List[AnomalySignal]: + """Get recently resolved signals.""" + with self._get_conn() as conn: + rows = conn.execute( + "SELECT * FROM signals WHERE market_resolved = 1 ORDER BY market_resolved_at DESC LIMIT ?", + (limit,), + ).fetchall() + return [self._row_to_signal(row) for row in rows] + + def get_best_worst(self, n: int = 5) -> dict: + """Get best and worst signals by ROI.""" + with self._get_conn() as conn: + best_rows = conn.execute( + "SELECT * FROM signals WHERE market_resolved = 1 ORDER BY theoretical_roi DESC LIMIT ?", + (n,), + ).fetchall() + worst_rows = conn.execute( + "SELECT * FROM signals WHERE market_resolved = 1 ORDER BY theoretical_roi ASC LIMIT ?", + (n,), + ).fetchall() + return { + "best": [self._row_to_signal(r) for r in best_rows], + "worst": [self._row_to_signal(r) for r in worst_rows], + } + + def migrate_from_json(self, json_dir: Path) -> int: + """One-time migration from JSON files to SQLite. Returns count migrated.""" + if not json_dir.exists(): + logger.warning(f"JSON directory not found: {json_dir}") + return 0 + + count = 0 + for json_file in json_dir.glob("*.json"): + try: + with open(json_file, 'r', encoding='utf-8') as f: + data = json.load(f) + + signals = data if isinstance(data, list) else data.get("signals", []) + for item in signals: + try: + signal = AnomalySignal.model_validate(item) + if self.insert_signal(signal): + count += 1 + except Exception as e: + logger.warning(f"Failed to parse signal from {json_file.name}: {e}") + + except Exception as e: + logger.error(f"Failed to load {json_file}: {e}") + + logger.info(f"Migrated {count} signals from JSON to SQLite") + return count \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..50f5655 --- /dev/null +++ b/src/main.py @@ -0,0 +1,604 @@ +""" +Polymarket Whale Watcher - Main Entry Point + +This bot monitors Polymarket markets for large (whale) trades via +real-time WebSocket (RTDS) and generates AI-powered analysis reports. + +Flow: +1. Fetch market list (for enrichment metadata) +2. RTDS WebSocket receives ALL trades in real-time (zero missed trades) +3. Filter for whale trades (size, price range, conviction) +4. Generate analysis reports using LLM +5. Output reports for user review (no automatic trading) +""" +import asyncio +import os +import re +import signal +import smtplib +import sys +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from datetime import datetime, timedelta +from pathlib import Path +from typing import Optional + +import typer + +from src.config import get_settings +from src.db.database import SignalDatabase +from src.services.market_fetcher import MarketFetcher +from src.services.trade_monitor import TradeMonitor +from src.services.price_monitor import PriceMonitor, VolatilityAlert +from src.services.llm_analyzer import LLMAnalyzer +from src.services.volatility_analyzer import VolatilityAnalyzer +from src.services.daily_briefing import DailyBriefingGenerator +from src.services.resolution_tracker import ResolutionTracker +from src.models.trade import WhaleTrade +from src.utils.logger import setup_logging, WhaleWatcherLogger + +app = typer.Typer(help="Polymarket Whale Watcher - AI-powered whale trade analysis") +logger = WhaleWatcherLogger() + + +class WhaleWatcher: + """Main whale watcher application.""" + + # Reports directory + REPORTS_DIR = Path(__file__).parent.parent / "reports" + + def __init__(self): + self.settings = get_settings() + self.market_fetcher = MarketFetcher() + self.trade_monitor = TradeMonitor(on_whale_detected=self.on_whale_detected) + self.llm_analyzer = LLMAnalyzer() + + # Volatility analyzer for detecting "price leads news" signals + self.volatility_analyzer = VolatilityAnalyzer() + + # Price monitor for ALL active markets (independent from trade monitor) + self.price_monitor = PriceMonitor( + window_seconds=3600, # 1 hour + threshold=0.20, # 20% + poll_interval=60, # Poll every 60 seconds + on_volatility_detected=self.on_volatility_detected, + ) + + # Database and resolution tracker + self.db = SignalDatabase(self.settings.db_path) + self.resolution_tracker = ResolutionTracker(self.db) + + # Daily briefing generator + self.briefing_generator = DailyBriefingGenerator(self.settings.db_path) + + self._running = False + self._refresh_interval = 900 # Refresh markets every 15 minutes + self._resolution_check_interval = 1800 # Check resolutions every 30 minutes + self._last_briefing_date = None # Track last briefing date + + # Ensure reports directory exists + self.REPORTS_DIR.mkdir(parents=True, exist_ok=True) + + def _sanitize_filename(self, text: str, max_length: int = 50) -> str: + """Sanitize text for use in filename.""" + # Remove special characters, keep alphanumeric and spaces + sanitized = re.sub(r'[^\w\s-]', '', text) + # Replace spaces with underscores + sanitized = re.sub(r'\s+', '_', sanitized) + # Truncate if too long + return sanitized[:max_length] + + def _save_report(self, whale_trade: WhaleTrade, full_report: str) -> str: + """ + Save report to a markdown file. + + Args: + whale_trade: The whale trade + full_report: The formatted report + + Returns: + Path to the saved file + """ + trade = whale_trade.trade + date_str = datetime.now().strftime("%Y%m%d") + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + market_name = self._sanitize_filename(whale_trade.market_question) + + day_dir = self.REPORTS_DIR / date_str + day_dir.mkdir(parents=True, exist_ok=True) + + filename = f"{timestamp}_{trade.side}_{int(trade.usdc_size)}USD_{market_name}.md" + filepath = day_dir / filename + + with open(filepath, "w", encoding="utf-8") as f: + f.write(full_report) + + return str(filepath) + + async def on_whale_detected(self, whale_trade: WhaleTrade) -> None: + """ + Callback when a whale trade is detected. + + Args: + whale_trade: The detected whale trade + """ + trade = whale_trade.trade + + # Log detection + logger.whale_detected( + amount=trade.usdc_size, + side=f"BUY {trade.outcome}", + price=trade.price, + market=whale_trade.market_question, + ) + + # Analyze with LLM + logger.info("Generating analysis report...") + decision = await self.llm_analyzer.analyze_whale_trade(whale_trade) + + # Print the full report (includes analysis + decision summary) + full_report = self.llm_analyzer.format_full_report( + whale_trade, + decision, + historical_signal_count=self.llm_analyzer.last_historical_signal_count, + ) + print(full_report) + + # Save report to file + filepath = self._save_report(whale_trade, full_report) + logger.info(f"Report saved to: {filepath}") + + # Real-time email alert for high information asymmetry (>= 60%) + ias = decision.recommendation.information_asymmetry_score + if ias >= 0.6: + self._send_alert_email(whale_trade, full_report, ias) + + logger.separator() + + def _send_alert_email(self, whale_trade: WhaleTrade, report: str, likelihood: float): + """Send real-time email alert for high information asymmetry signals.""" + settings = get_settings() + if not settings.email_enabled or not settings.email_sender or not settings.email_password: + return + + alert_recipient = "1253608463@qq.com" + trade = whale_trade.trade + + try: + subject = ( + f"Anomalous Trade Alert ({likelihood:.0%}) — " + f"BUY {trade.outcome} @ {trade.price:.4f} " + f"${trade.usdc_size:,.0f} — {whale_trade.market_question[:50]}" + ) + + msg = MIMEMultipart("alternative") + msg["Subject"] = subject + msg["From"] = settings.email_sender + msg["To"] = alert_recipient + msg.attach(MIMEText(report, "plain", "utf-8")) + + with smtplib.SMTP_SSL(settings.email_smtp_server, settings.email_smtp_port) as server: + server.login(settings.email_sender, settings.email_password) + server.sendmail(settings.email_sender, alert_recipient, msg.as_string()) + + logger.info(f"Insider alert email sent to {alert_recipient} (likelihood: {likelihood:.0%})") + except Exception as e: + logger.error(f"Failed to send alert email: {e}") + + async def on_volatility_detected(self, alert: VolatilityAlert) -> None: + """ + Callback when price volatility is detected. + + Analyzes the volatility to determine if it's a "price leads news" signal. + + Args: + alert: The volatility alert + """ + logger.info( + f"Volatility detected: {alert.market_question[:50]}... " + f"{alert.direction} {abs(alert.price_change_percent):.1%}" + ) + + # Analyze with LLM to check if price leads news + logger.info("Analyzing volatility for leading signal detection...") + signal = await self.volatility_analyzer.analyze_volatility(alert) + + if signal: + # Print the analysis report + report = self.volatility_analyzer.format_signal_report(signal) + print(report) + + if signal.is_leading_signal: + logger.info( + f"LEADING SIGNAL recorded: {alert.market_question[:50]}... " + f"Time advantage: {signal.time_advantage_minutes} minutes" + ) + else: + logger.info( + f"Signal analyzed: {signal.signal_type.value} " + f"(confidence: {signal.confidence:.1%})" + ) + + # Print stats + stats = self.volatility_analyzer.get_leading_signals_stats() + logger.info( + f"Dataset stats: {stats['total_signals']} total, " + f"{stats['leading_signals']} leading signals" + ) + + logger.separator() + + async def refresh_markets(self) -> None: + """Fetch and update the list of monitored markets.""" + if self.settings.full_market_scan: + await self._refresh_markets_tiered() + else: + await self._refresh_markets_legacy() + + async def _refresh_markets_tiered(self) -> None: + """Full-coverage tiered monitoring (mirrors options flow passive approach).""" + logger.info("Fetching ALL active markets for tiered monitoring...") + + tiers = self.market_fetcher.get_tiered_markets() + + # Also merge token launch markets into appropriate tiers + existing_ids = set() + for tier_markets in tiers.values(): + for tm in tier_markets: + existing_ids.add(tm.market.id) + + token_markets = self.market_fetcher.get_token_launch_markets() + token_added = 0 + for tm in token_markets: + if tm.market.id not in existing_ids: + # Assign to tier based on volume + vol = tm.volume_24hr + if vol >= self.settings.tier1_volume_min: + tiers["tier1"].append(tm) + elif vol >= self.settings.tier2_volume_min: + tiers["tier2"].append(tm) + else: + tiers["tier3"].append(tm) + existing_ids.add(tm.market.id) + token_added += 1 + + if token_added: + logger.info(f"Added {token_added} token launch markets to tiers") + + total = sum(len(v) for v in tiers.values()) + if total > 0: + self.trade_monitor.set_tiered_markets(tiers) + logger.info(f"Tiered monitoring active: {total} markets total") + else: + logger.error("Failed to fetch any markets") + + async def _refresh_markets_legacy(self) -> None: + """Original Top-N trending markets mode.""" + logger.info("Fetching trending markets...") + + trending_markets = self.market_fetcher.get_trending_markets( + limit=self.settings.trending_markets_limit + ) + + existing_ids = {tm.market.id for tm in trending_markets} + + token_markets = self.market_fetcher.get_token_launch_markets() + token_added = 0 + for tm in token_markets: + if tm.market.id not in existing_ids: + trending_markets.append(tm) + existing_ids.add(tm.market.id) + token_added += 1 + + if token_added: + logger.info( + f"Added {token_added} token launch markets " + f"(total: {len(trending_markets)})" + ) + + if trending_markets: + self.trade_monitor.set_monitored_markets(trending_markets) + logger.info(f"Now monitoring {len(trending_markets)} markets") + else: + logger.error("Failed to fetch trending markets") + + async def run(self) -> None: + """Run the main whale watcher loop.""" + self._running = True + + # Initial market fetch + await self.refresh_markets() + + # Log startup + logger.monitoring_started( + market_count=len(self.trade_monitor._monitored_markets), + min_trade_size=self.settings.min_trade_size_usd, + min_price=self.settings.min_price, + max_price=self.settings.max_price, + ) + + # Start monitoring tasks: + # 1. Trade monitor - RTDS WebSocket real-time trade stream + # 2. Market refresh - refreshes market metadata periodically + # 3. Daily briefing - generates daily summary at midnight + # 4. Resolution check - checks if markets with signals have resolved + # NOTE: Price volatility monitor is temporarily disabled + monitor_task = asyncio.create_task(self.trade_monitor.run()) + # price_monitor_task = asyncio.create_task(self.price_monitor.run()) + refresh_task = asyncio.create_task(self._refresh_loop()) + briefing_task = asyncio.create_task(self._briefing_loop()) + resolution_task = asyncio.create_task(self._resolution_check_loop()) + + try: + await asyncio.gather(monitor_task, refresh_task, briefing_task, resolution_task) + except asyncio.CancelledError: + logger.info("Shutting down...") + finally: + self.trade_monitor.stop() + self.price_monitor.stop() + await self.trade_monitor.close() + + async def _refresh_loop(self) -> None: + """Periodically refresh the market list.""" + while self._running: + await asyncio.sleep(self._refresh_interval) + if self._running: + await self.refresh_markets() + + def _briefing_already_sent(self, date: datetime) -> bool: + """Check if briefing for a date was already generated (file exists).""" + from src.services.daily_briefing import BRIEFINGS_DIR + date_str = date.strftime("%Y-%m-%d") + return (BRIEFINGS_DIR / f"briefing_{date_str}.md").exists() + + async def _briefing_loop(self) -> None: + """Generate daily briefing for previous day at 10:00 local time.""" + while self._running: + now = datetime.now() + today = now.date() + yesterday = now - timedelta(days=1) + + # Generate at 10:00 local time, skip if already sent (survives restart) + if now.hour == 10 and now.minute >= 0: + if self._last_briefing_date != today and not self._briefing_already_sent(yesterday): + try: + filepath = self.briefing_generator.generate_briefing() + if filepath: + logger.info(f"Daily briefing generated: {filepath}") + self._last_briefing_date = today + except Exception as e: + logger.error(f"Error generating daily briefing: {e}") + else: + self._last_briefing_date = today + + # Check every minute + await asyncio.sleep(60) + + async def _resolution_check_loop(self) -> None: + """Periodically check if markets with signals have resolved.""" + # Initial delay to let the system start up + await asyncio.sleep(60) + + while self._running: + try: + result = await self.resolution_tracker.check_all() + if result["resolved"] > 0: + logger.info( + f"Resolution check: {result['resolved']} markets resolved, " + f"{result['signals_updated']} signals updated" + ) + except Exception as e: + logger.error(f"Error in resolution check: {e}") + + await asyncio.sleep(self._resolution_check_interval) + + def stop(self) -> None: + """Stop the whale watcher.""" + self._running = False + self.trade_monitor.stop() + self.price_monitor.stop() + + +# Global instance for signal handling +_watcher: Optional[WhaleWatcher] = None + + +def signal_handler(signum, frame): + """Handle shutdown signals.""" + logger.info("Received shutdown signal...") + if _watcher: + _watcher.stop() + sys.exit(0) + + +@app.command() +def run( + debug: bool = typer.Option(False, "--debug", "-d", help="Enable debug logging"), +): + """Start the whale watcher bot.""" + global _watcher + + # Setup logging + setup_logging("DEBUG" if debug else "INFO") + + # Setup signal handlers + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # Create and run watcher + _watcher = WhaleWatcher() + + try: + asyncio.run(_watcher.run()) + except KeyboardInterrupt: + logger.info("Interrupted by user") + finally: + logger.info("Whale watcher stopped") + + +@app.command() +def check_markets( + limit: int = typer.Option(10, "--limit", "-l", help="Number of markets to show"), +): + """Check current trending markets.""" + setup_logging("INFO") + + fetcher = MarketFetcher() + markets = fetcher.get_trending_markets(limit=limit) + + print(f"\n{'='*80}") + print(f"Top {len(markets)} Trending Markets by 24hr Volume") + print(f"{'='*80}\n") + + for tm in markets: + m = tm.market + prices = ", ".join( + [f"{o}: {p:.2%}" for o, p in zip(m.outcomes, m.outcome_prices)] + ) + print(f"#{tm.rank} | Vol24h: ${tm.volume_24hr:,.0f}") + print(f" Question: {m.question}") + print(f" Prices: {prices}") + print(f" ID: {m.id}") + print() + + +@app.command() +def test_analyze( + market_id: str = typer.Argument(..., help="Market ID to test analysis on"), +): + """Test LLM analysis on a specific market (simulates a whale trade).""" + setup_logging("INFO") + + fetcher = MarketFetcher() + market = fetcher.get_market_by_id(market_id) + + if not market: + print(f"Market {market_id} not found") + raise typer.Exit(1) + + # Create a simulated whale trade + from src.models.trade import TradeActivity, WhaleTrade + import time + + fake_activity = TradeActivity( + transaction_hash="test_" + str(int(time.time())), + timestamp=int(time.time()), + condition_id=market.condition_id or "", + asset=market.clob_token_ids[0] if market.clob_token_ids else "", + side="BUY", + size=50000.0, + usdc_size=25000.0, # Simulated $25k trade + price=0.45, # Simulated price + outcome=market.outcomes[0] if market.outcomes else "", + outcome_index=0, + title=market.question, + ) + + whale_trade = WhaleTrade( + id=f"test_{market_id}", + trade=fake_activity, + market_id=market.id, + market_question=market.question, + market_description=market.description, + market_outcomes=market.outcomes, + market_outcome_prices=market.outcome_prices, + ) + + print(f"\nSimulating whale trade analysis for:") + print(f" Market: {market.question}") + print(f" Trade: $25,000 BUY @ 0.45") + print(f"\nAnalyzing with LLM...\n") + + analyzer = LLMAnalyzer() + decision = asyncio.run(analyzer.analyze_whale_trade(whale_trade)) + + print(analyzer.format_decision_report(decision)) + print("\nFull Analysis:") + print("-" * 60) + print(decision.analysis) + + +@app.command() +def briefing( + date: str = typer.Option(None, "--date", "-d", help="Date in YYYY-MM-DD format (defaults to yesterday)"), + today: bool = typer.Option(False, "--today", "-t", help="Generate briefing for today instead of yesterday"), +): + """Generate daily briefing manually.""" + setup_logging("INFO") + + settings = get_settings() + generator = DailyBriefingGenerator(settings.db_path) + + if today: + filepath = generator.generate_today_briefing() + elif date: + try: + target_date = datetime.strptime(date, "%Y-%m-%d") + filepath = generator.generate_briefing(target_date) + except ValueError: + print(f"Invalid date format: {date}. Use YYYY-MM-DD") + raise typer.Exit(1) + else: + filepath = generator.generate_briefing() # Yesterday by default + + if filepath: + print(f"\nBriefing generated: {filepath}") + + # Print the content + with open(filepath, 'r', encoding='utf-8') as f: + print("\n" + "=" * 80) + print(f.read()) + else: + print("\nNo signals found for the specified date. No briefing generated.") + + +@app.command() +def migrate(): + """Migrate anomaly signals from JSON files to SQLite database.""" + setup_logging("INFO") + + settings = get_settings() + db = SignalDatabase(settings.db_path) + + json_dir = Path(__file__).parent.parent / "anomaly_signals" + print(f"Migrating signals from {json_dir} to {settings.db_path}") + + count = db.migrate_from_json(json_dir) + print(f"Migration complete: {count} signals migrated") + + # Show stats + stats = db.get_stats() + print(f"\nDatabase stats:") + print(f" Total signals: {stats['total_signals']}") + print(f" Resolved: {stats['resolved']}") + + +@app.command() +def dashboard( + port: int = typer.Option(8517, "--port", "-p", help="Port to run the dashboard on"), + host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host to bind to"), +): + """Start the signal performance dashboard web server.""" + setup_logging("INFO") + + import asyncio + import uvicorn + from src.dashboard import app as dashboard_app + + print(f"Starting dashboard at http://{host}:{port}") + + config = uvicorn.Config(dashboard_app, host=host, port=port) + server = uvicorn.Server(config) + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(server.serve()) + + +def main(): + """Entry point.""" + app() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/models/__init__.py b/src/models/__init__.py new file mode 100644 index 0000000..81dad60 --- /dev/null +++ b/src/models/__init__.py @@ -0,0 +1,13 @@ +"""Data models module.""" +from .market import Market, TrendingMarket +from .trade import WhaleTrade, TradeActivity +from .decision import LLMDecision, TradeRecommendation + +__all__ = [ + "Market", + "TrendingMarket", + "WhaleTrade", + "TradeActivity", + "LLMDecision", + "TradeRecommendation", +] diff --git a/src/models/anomaly_signal.py b/src/models/anomaly_signal.py new file mode 100644 index 0000000..a57235e --- /dev/null +++ b/src/models/anomaly_signal.py @@ -0,0 +1,94 @@ +"""Anomaly signal models for storing historical anomalous trades.""" +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, Field + +from src.models.trade import TraderRanking, TraderHistory + + +class AnomalySignal(BaseModel): + """ + Represents a stored anomaly signal for a market. + + This captures the raw trade and trader information for trades with medium + or higher information asymmetry score. The information_asymmetry_score is stored + for sorting/filtering purposes, but NOT shown to LLM - the model will + re-analyze all signals (historical + current) together without bias. + """ + + # Unique identifier + id: str = Field(default_factory=lambda: "") + + # Market identification + market_id: str + market_question: str + market_slug: Optional[str] = None + condition_id: Optional[str] = None + + # Trade information + transaction_hash: str + trade_timestamp: int # Unix timestamp of the trade + trade_side: str # BUY or SELL + trade_price: float + trade_size_usd: float + trade_outcome: str + + # Trader information + trader_wallet: Optional[str] = None + trader_ranking: Optional[TraderRanking] = None + trader_history: Optional[TraderHistory] = None + + # Information asymmetry score (for sorting/filtering only, NOT shown to LLM) + information_asymmetry_score: float = Field(default=0.0, ge=0.0, le=1.0) + + # LLM analysis results + reasoning: str = "" + insider_evidence: str = "" + + # Metadata + detected_at: datetime = Field(default_factory=datetime.utcnow) + + # Resolution tracking + market_resolved: bool = False + market_resolved_at: Optional[datetime] = None + resolved_outcome: Optional[str] = None + signal_correct: Optional[bool] = None + theoretical_roi: Optional[float] = None + + def to_context_string(self) -> str: + """ + Format this anomaly signal as a context string for LLM. + + Returns: + Formatted string describing this historical anomaly signal. + """ + trade_time = datetime.fromtimestamp(self.trade_timestamp).strftime('%Y-%m-%d %H:%M:%S') + + # Trader ranking info + trader_rank_str = "Unranked" + trader_pnl_str = "N/A" + trader_vol_str = "N/A" + if self.trader_ranking: + if self.trader_ranking.rank: + trader_rank_str = f"#{self.trader_ranking.rank}" + if self.trader_ranking.pnl is not None: + trader_pnl_str = f"${self.trader_ranking.pnl:,.2f}" + if self.trader_ranking.volume is not None: + trader_vol_str = f"${self.trader_ranking.volume:,.2f}" + + # Trader history info + trader_history_str = "" + if self.trader_history: + trader_history_str = f""" +- Recent Trades: {self.trader_history.total_trades} +- Total Volume: ${self.trader_history.total_volume:,.2f} +- Large Trades: {self.trader_history.large_trades_count}""" + + return f"""**Trade Time**: {trade_time} +**Direction**: {self.trade_side} +**Trade Size**: ${self.trade_size_usd:,.2f} USDC +**Trade Price**: {self.trade_price:.4f} +**Outcome**: {self.trade_outcome} +**Trader Wallet**: {self.trader_wallet or 'Unknown'} +**Trader Rank**: {trader_rank_str} (PnL: {trader_pnl_str}, Volume: {trader_vol_str}){trader_history_str}""" diff --git a/src/models/decision.py b/src/models/decision.py new file mode 100644 index 0000000..24293d7 --- /dev/null +++ b/src/models/decision.py @@ -0,0 +1,59 @@ +"""LLM decision models.""" +from datetime import datetime +from typing import Optional +from enum import Enum + +from pydantic import BaseModel, Field + + +class TradeAction(str, Enum): + """Recommended trade action.""" + + BUY = "BUY" + SELL = "SELL" + HOLD = "HOLD" # Do not trade + + +class TraderCredibility(str, Enum): + """Trader credibility level based on leaderboard ranking.""" + + HIGH = "HIGH" # Top 100 + MEDIUM = "MEDIUM" # 100-500 + LOW = "LOW" # 500+ + UNKNOWN = "UNKNOWN" # Not on leaderboard + + +class TradeRecommendation(BaseModel): + """Trade recommendation from LLM.""" + + action: TradeAction + outcome: str # Which outcome to trade + confidence: float = Field(ge=0.0, le=1.0) # 0-1 confidence score + suggested_price: Optional[float] = None + suggested_size_percent: float = Field(default=0.1, ge=0.0, le=1.0) # % of balance + reasoning: str = "" + + # Information asymmetry assessment fields + information_asymmetry_score: float = Field(default=0.0, ge=0.0, le=1.0) # 0-1 score + trader_credibility: TraderCredibility = TraderCredibility.UNKNOWN + insider_evidence: str = "" # Evidence supporting information asymmetry assessment + + +class LLMDecision(BaseModel): + """Complete LLM decision for a whale trade.""" + + whale_trade_id: str + market_id: str + analysis: str # Full LLM analysis text + recommendation: TradeRecommendation + created_at: datetime = Field(default_factory=datetime.utcnow) + executed: bool = False + execution_result: Optional[str] = None + + @property + def should_trade(self) -> bool: + """Check if we should execute this trade.""" + return ( + self.recommendation.action != TradeAction.HOLD + and self.recommendation.confidence >= 0.6 + ) diff --git a/src/models/leading_signal.py b/src/models/leading_signal.py new file mode 100644 index 0000000..a5205a9 --- /dev/null +++ b/src/models/leading_signal.py @@ -0,0 +1,124 @@ +"""Data models for leading signal detection - price moves before news.""" +from dataclasses import dataclass, field +from datetime import datetime +from enum import Enum +from typing import List, Optional + + +class SignalType(str, Enum): + """Type of price volatility signal.""" + LEADING_SIGNAL = "LEADING_SIGNAL" # Price moved before news + NEWS_DRIVEN = "NEWS_DRIVEN" # Price reacted to news + SOCIAL_DRIVEN = "SOCIAL_DRIVEN" # Price driven by social media + SPECULATION = "SPECULATION" # No clear information source + + +@dataclass +class LeadingSignal: + """ + A case where price movement preceded public news. + + This is used to build a dataset of "price leads news" events + for research purposes. + """ + # Basic info + id: str + market_id: str + market_question: str + + # Price movement details + price_change_percent: float # e.g., 0.25 for 25% + direction: str # "UP" or "DOWN" + start_price: float + end_price: float + window_seconds: int + + # Timing + detected_at: str # ISO format timestamp + volatility_detected_at: str # When price volatility was detected + + # LLM analysis results + signal_type: SignalType + confidence: float # 0-1 + is_leading_signal: bool + + # News analysis + news_found: bool + earliest_news_time: Optional[str] = None # ISO format + key_news_headlines: List[str] = field(default_factory=list) + + # Social media analysis + earliest_social_time: Optional[str] = None # ISO format + key_social_posts: List[str] = field(default_factory=list) + + # Time advantage + time_advantage_minutes: int = 0 # How many minutes price led news + + # Analysis + reasoning: str = "" + potential_information_source: str = "" + + # Full LLM analysis text + full_analysis: str = "" + + def to_dict(self) -> dict: + """Convert to dictionary for JSON serialization.""" + return { + "id": self.id, + "market_id": self.market_id, + "market_question": self.market_question, + "price_change_percent": self.price_change_percent, + "direction": self.direction, + "start_price": self.start_price, + "end_price": self.end_price, + "window_seconds": self.window_seconds, + "detected_at": self.detected_at, + "volatility_detected_at": self.volatility_detected_at, + "signal_type": self.signal_type.value if isinstance(self.signal_type, SignalType) else self.signal_type, + "confidence": self.confidence, + "is_leading_signal": self.is_leading_signal, + "news_found": self.news_found, + "earliest_news_time": self.earliest_news_time, + "key_news_headlines": self.key_news_headlines, + "earliest_social_time": self.earliest_social_time, + "key_social_posts": self.key_social_posts, + "time_advantage_minutes": self.time_advantage_minutes, + "reasoning": self.reasoning, + "potential_information_source": self.potential_information_source, + "full_analysis": self.full_analysis, + } + + @classmethod + def from_dict(cls, data: dict) -> "LeadingSignal": + """Create from dictionary.""" + signal_type = data.get("signal_type", "SPECULATION") + if isinstance(signal_type, str): + try: + signal_type = SignalType(signal_type) + except ValueError: + signal_type = SignalType.SPECULATION + + return cls( + id=data["id"], + market_id=data["market_id"], + market_question=data["market_question"], + price_change_percent=data["price_change_percent"], + direction=data["direction"], + start_price=data["start_price"], + end_price=data["end_price"], + window_seconds=data["window_seconds"], + detected_at=data["detected_at"], + volatility_detected_at=data["volatility_detected_at"], + signal_type=signal_type, + confidence=data.get("confidence", 0.0), + is_leading_signal=data.get("is_leading_signal", False), + news_found=data.get("news_found", False), + earliest_news_time=data.get("earliest_news_time"), + key_news_headlines=data.get("key_news_headlines", []), + earliest_social_time=data.get("earliest_social_time"), + key_social_posts=data.get("key_social_posts", []), + time_advantage_minutes=data.get("time_advantage_minutes", 0), + reasoning=data.get("reasoning", ""), + potential_information_source=data.get("potential_information_source", ""), + full_analysis=data.get("full_analysis", ""), + ) diff --git a/src/models/market.py b/src/models/market.py new file mode 100644 index 0000000..8455a2d --- /dev/null +++ b/src/models/market.py @@ -0,0 +1,44 @@ +"""Market data models.""" +from datetime import datetime +from typing import Optional, List + +from pydantic import BaseModel, Field + + +class Market(BaseModel): + """Polymarket market data model.""" + + id: str + question: str + condition_id: Optional[str] = None + slug: Optional[str] = None + description: Optional[str] = None + end_date: Optional[str] = None + outcomes: List[str] = Field(default_factory=list) + outcome_prices: List[float] = Field(default_factory=list) + clob_token_ids: List[str] = Field(default_factory=list) + volume: float = 0.0 + volume_24hr: float = 0.0 + liquidity: float = 0.0 + active: bool = True + closed: bool = False + neg_risk: bool = False + + +class TrendingMarket(BaseModel): + """Trending market with additional metrics.""" + + market: Market + volume_24hr: float = 0.0 + liquidity: float = 0.0 + rank: int = 0 + fetched_at: datetime = Field(default_factory=datetime.utcnow) + + @property + def is_valid_for_monitoring(self) -> bool: + """Check if market is valid for whale monitoring.""" + return ( + self.market.active + and not self.market.closed + and len(self.market.clob_token_ids) > 0 + ) diff --git a/src/models/trade.py b/src/models/trade.py new file mode 100644 index 0000000..8e5e635 --- /dev/null +++ b/src/models/trade.py @@ -0,0 +1,240 @@ +"""Trade data models.""" +from datetime import datetime +from typing import Optional +from enum import Enum + +from pydantic import BaseModel, Field + + +class TradeSide(str, Enum): + """Trade side enum.""" + + BUY = "BUY" + SELL = "SELL" + + +class TradeActivity(BaseModel): + """Raw trade activity from Polymarket API.""" + + transaction_hash: str + timestamp: int + condition_id: str + asset: str + side: str + size: float # Token size + usdc_size: float # USD value + price: float + outcome: str + outcome_index: int + title: str + slug: Optional[str] = None + event_slug: Optional[str] = None + proxy_wallet: Optional[str] = None + name: Optional[str] = None + + +class TraderRanking(BaseModel): + """Trader ranking information from leaderboard.""" + + rank: Optional[int] = None # Position on leaderboard (None if not ranked) + pnl: Optional[float] = None # Profit/Loss + volume: Optional[float] = None # Trading volume + user_name: Optional[str] = None # Display name + profile_image: Optional[str] = None # Avatar URL + verified: bool = False # Verified badge + time_period: str = "ALL" # Time period for ranking + + +class TraderHistory(BaseModel): + """Trader's recent trading history summary.""" + + total_trades: int = 0 # Total number of recent trades + total_volume: float = 0.0 # Total trading volume in USDC + avg_trade_size: float = 0.0 # Average trade size + win_rate: Optional[float] = None # Win rate if calculable + recent_markets: list[str] = Field(default_factory=list) # Recent markets traded + large_trades_count: int = 0 # Number of trades >= $5000 + recent_trades: list[dict] = Field(default_factory=list) # Recent trade details + + +class EventPosition(BaseModel): + """Whale's position in a related market under the same event.""" + + market_question: str + condition_id: str = "" + outcome: str = "" # "Yes" or "No" + size: float = 0.0 # token size held + avg_price: float = 0.0 # average entry price + current_price: float = 0.0 # current market price + current_value: float = 0.0 # current position value in USD + initial_value: float = 0.0 # cost basis + pnl: float = 0.0 # realized + unrealized PnL + side_summary: str = "" # human readable summary + + +class MarketTopTrader(BaseModel): + """Top trader on a market (by net volume).""" + + wallet: str + name: Optional[str] = None + rank: Optional[int] = None + pnl: Optional[float] = None + net_volume_usd: float = 0.0 # positive = net buyer of Yes, negative = net seller + trade_count: int = 0 + + +class WhaleTrade(BaseModel): + """Whale trade that meets detection criteria.""" + + id: str = Field(default_factory=lambda: "") + trade: TradeActivity + market_id: str + market_question: str + market_description: Optional[str] = None + market_outcomes: list[str] = Field(default_factory=list) + market_outcome_prices: list[float] = Field(default_factory=list) + detected_at: datetime = Field(default_factory=datetime.utcnow) + processed: bool = False + llm_analyzed: bool = False + + # Trader ranking info + trader_ranking: Optional[TraderRanking] = None + # Trader history info + trader_history: Optional[TraderHistory] = None + # Whale's positions across the same event + whale_event_positions: list[EventPosition] = Field(default_factory=list) + # Top traders on this market (bulls and bears) + market_top_buyers: list[MarketTopTrader] = Field(default_factory=list) + market_top_sellers: list[MarketTopTrader] = Field(default_factory=list) + + @property + def is_whale_trade(self) -> bool: + """Check if this qualifies as a whale trade.""" + return self.trade.usdc_size >= 10000 + + @property + def is_valid_price_range(self) -> bool: + """Check if trade price is in valid range (0.2-0.8).""" + return 0.2 <= self.trade.price <= 0.8 + + def format_event_positions(self) -> str: + """Format whale's event positions for LLM context.""" + if self.whale_event_positions: + info = "### Whale's Positions in Other Markets Under the Same Event\n" + info += "(Used to identify hedging or correlated bets)\n\n" + for pos in self.whale_event_positions: + pnl_str = f"PnL ${pos.pnl:+,.0f}" if pos.pnl else "" + info += ( + f"- **{pos.market_question[:60]}{'...' if len(pos.market_question) > 60 else ''}**\n" + f" {pos.side_summary} | " + f"Current Value ${pos.current_value:,.0f} | Cost Basis ${pos.initial_value:,.0f} | " + f"{pnl_str}\n" + ) + return info + return "### Whale's Positions in Other Markets Under the Same Event\n- No other related positions (single-market event or no cross-market trades)\n" + + def format_top_traders(self) -> str: + """Format market top holders for LLM context.""" + info = "### Top 5 Bulls and Bears on This Market\n" + info += "(Reflects the stance and credentials of major participants)\n" + + if self.market_top_buyers: + info += "\n**Bulls (Holding Yes Token)**:\n" + for i, t in enumerate(self.market_top_buyers, 1): + rank_str = f"Rank #{t.rank}" if t.rank else "Unranked" + pnl_str = f"PnL ${t.pnl:,.0f}" if t.pnl is not None else "" + name_str = t.name or t.wallet[:10] + "..." + info += ( + f" {i}. **{name_str}** ({rank_str}{', ' + pnl_str if pnl_str else ''}) " + f"— Position Value ${t.net_volume_usd:,.0f}\n" + ) + else: + info += "\n**Bulls**: No significant positions\n" + + if self.market_top_sellers: + info += "\n**Bears (Holding No Token)**:\n" + for i, t in enumerate(self.market_top_sellers, 1): + rank_str = f"Rank #{t.rank}" if t.rank else "Unranked" + pnl_str = f"PnL ${t.pnl:,.0f}" if t.pnl is not None else "" + name_str = t.name or t.wallet[:10] + "..." + info += ( + f" {i}. **{name_str}** ({rank_str}{', ' + pnl_str if pnl_str else ''}) " + f"— Position Value ${t.net_volume_usd:,.0f}\n" + ) + else: + info += "\n**Bears**: No significant positions\n" + + return info + + def to_llm_context(self) -> str: + """Generate context string for LLM analysis.""" + # Format trader ranking info + trader_info = "" + if self.trader_ranking: + rank_str = f"#{self.trader_ranking.rank}" if self.trader_ranking.rank else "Unranked" + pnl_str = f"${self.trader_ranking.pnl:,.2f}" if self.trader_ranking.pnl else "N/A" + vol_str = f"${self.trader_ranking.volume:,.2f}" if self.trader_ranking.volume else "N/A" + verified_str = "Verified" if self.trader_ranking.verified else "Unverified" + trader_info = f""" +### Trader Ranking (PnL Leaderboard) +- **Rank**: {rank_str} (Period: {self.trader_ranking.time_period}) +- **Cumulative PnL**: {pnl_str} +- **Volume**: {vol_str} +- **Username**: {self.trader_ranking.user_name or 'Anonymous'} +- **Verification**: {verified_str} +""" + else: + trader_info = """ +### Trader Ranking +- This trader is not on the PnL leaderboard (possibly a new user or small trader) +""" + + # Format trader history info + history_info = "" + if self.trader_history: + history_info = f""" +### Trader History +- **Recent Trades**: {self.trader_history.total_trades} +- **Recent Volume**: ${self.trader_history.total_volume:,.2f} USDC +- **Avg Trade Size**: ${self.trader_history.avg_trade_size:,.2f} USDC +- **Large Trades** (>=$5000): {self.trader_history.large_trades_count} +- **Active Markets**: {', '.join(self.trader_history.recent_markets[:5]) if self.trader_history.recent_markets else 'N/A'} +""" + # Add recent large trades details + if self.trader_history.recent_trades: + history_info += "\n**Recent Large Trade Details**:\n" + for i, t in enumerate(self.trader_history.recent_trades[:5], 1): + history_info += f" {i}. {t.get('side', 'N/A')} ${t.get('usdc_size', 0):,.2f} @ {t.get('price', 0):.4f} - {t.get('title', 'N/A')[:40]}...\n" + else: + history_info = """ +### Trader History +- Unable to retrieve this trader's trading history +""" + + return f""" +## Anomalous Trade Detection + +### Trade Information +- Direction: BUY {self.trade.outcome} Token ({'Bullish — expects the event to occur' if self.trade.outcome == 'Yes' else 'Bearish — expects the event will not occur'}) +- Trade Size: ${self.trade.usdc_size:,.2f} USDC +- Entry Price: {self.trade.price:.4f} (Odds ~{1/self.trade.price:.1f}x) +- Trade Time: {datetime.fromtimestamp(self.trade.timestamp).strftime('%Y-%m-%d %H:%M:%S')} +- Trader Wallet: {self.trade.proxy_wallet or 'Unknown'} +{trader_info}{history_info} +{self.format_event_positions()} + +{self.format_top_traders()} + +### Market Information +- Market Question: {self.market_question} +- Market Description: {self.market_description or 'N/A'} +- Possible Outcomes: {', '.join(self.market_outcomes)} +- Current Prices: {', '.join([f'{o}: {p:.4f}' for o, p in zip(self.market_outcomes, self.market_outcome_prices)])} + +### Key Analysis Points +1. This large trade (${self.trade.usdc_size:,.2f}) is **BUY {self.trade.outcome} Token**, {'indicating the trader is Bullish and expects the event to occur' if self.trade.outcome == 'Yes' else 'indicating the trader is Bearish and expects the event will not occur'} +2. Entry price {self.trade.price:.4f}, odds ~{1/self.trade.price:.1f}x +3. **Trader ranking and history are key references for assessing information asymmetry credibility** +4. **Review the whale's other positions under the same event** — opposite positions may indicate a hedging strategy +5. **Check the top bulls and bears on this market** — which side has the elite traders +""" diff --git a/src/prompts/__init__.py b/src/prompts/__init__.py new file mode 100644 index 0000000..b11efab --- /dev/null +++ b/src/prompts/__init__.py @@ -0,0 +1,4 @@ +"""Prompts module.""" +from .whale_analyzer import WhaleAnalyzerPrompts + +__all__ = ["WhaleAnalyzerPrompts"] diff --git a/src/prompts/volatility_analyzer.py b/src/prompts/volatility_analyzer.py new file mode 100644 index 0000000..bb22bae --- /dev/null +++ b/src/prompts/volatility_analyzer.py @@ -0,0 +1,222 @@ +"""Prompts for price volatility analysis - detecting leading signals.""" +from datetime import datetime, timezone + + +class VolatilityAnalyzerPrompts: + """Prompts for LLM price volatility analysis.""" + + @staticmethod + def system_prompt() -> str: + """Get the system prompt for volatility analysis.""" + current_utc = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC') + + return f"""You are a professional prediction market analyst specializing in the "price leads news" phenomenon. + +**Current real time**: {current_utc} + +**Your core task**: Determine whether a detected price anomaly "leads public news" — i.e., the price movement occurred before related news was publicly reported. + +## Background + +In prediction markets, the following pattern sometimes occurs: +1. Market price suddenly moves sharply +2. But mainstream news media has not yet reported the related event +3. Subsequently (hours or days later), the related news becomes public + +This "price leads news" phenomenon may indicate: +- Informed participants traded on information before it became public +- Market participants obtained information through social media, unofficial channels, etc. +- Pure market speculation or technical volatility + +## Your Workflow + +### Step 1: Analyze provided Web search results +- Analyze the latest news related to the market topic +- Pay special attention to news publication timestamps +- Determine if any major news can explain this price movement + +### Step 2: Analyze Twitter social media data +- Analyze provided Twitter search results +- Check for early social media discussions +- Note timing of KOL and insider posts + +### Step 3: Classify the price movement +Based on search results, classify the volatility as one of: + +1. **LEADING_SIGNAL**: Price movement clearly preceded public news + - No news found that explains the movement + - Or news publication time is significantly later than the price movement + - This is the type we care about most! + +2. **NEWS_DRIVEN**: Price movement is a reaction to published news + - Clear related news found + - News publication time is before or close to the price movement time + +3. **SOCIAL_DRIVEN**: Price movement driven by social media discussion + - Significant Twitter discussion, but mainstream media has not yet reported + - Between leading signal and news-driven + +4. **SPECULATION**: No clear information source for the movement + - No related news or discussions found + - Likely pure market speculation + +**Key principles**: +- Carefully analyze the Web search results provided +- Carefully analyze the Twitter search results +- Pay special attention to timestamps of news and discussions +- If it's a LEADING_SIGNAL, document evidence in detail""" + + @staticmethod + def analyze_volatility( + market_question: str, + price_change_percent: float, + direction: str, + start_price: float, + end_price: float, + window_seconds: int, + detected_at: str, + twitter_context: str = "", + web_search_context: str = "", + ) -> str: + """ + Get the prompt for analyzing a price volatility event. + + Args: + market_question: The market question + price_change_percent: Price change as decimal (e.g., 0.25 for 25%) + direction: "UP" or "DOWN" + start_price: Starting price + end_price: Ending price + window_seconds: Time window in seconds + detected_at: Detection timestamp + twitter_context: Twitter search results + web_search_context: Web search results from Tavily + + Returns: + Complete prompt for LLM + """ + direction_label = "UP" if direction == "UP" else "DOWN" + window_minutes = window_seconds // 60 + + web_search_section = "" + if web_search_context: + web_search_section = f""" +--- + +## Web Search Results (News & Analysis) + +The following are recent web search results related to this market. Please carefully analyze publication timestamps and content: + +{web_search_context} + +--- +""" + + twitter_section = "" + if twitter_context: + twitter_section = f""" +--- + +## Twitter Social Media Search Results + +The following are real-time Twitter discussions related to this market. Please carefully analyze timestamps and content: + +{twitter_context} + +--- +""" + + return f"""## Anomalous Price Movement Detection Report + +### Movement Details +- **Market question**: {market_question} +- **Price change**: {direction_label} {abs(price_change_percent):.1%} +- **Start price**: {start_price:.2%} +- **End price**: {end_price:.2%} +- **Time window**: Within {window_minutes} minutes +- **Detection time**: {detected_at} + +{web_search_section}{twitter_section} + +--- + +# Price Movement Verification Task + +A significant anomalous price movement has been detected. Please determine whether this is a "price leads news" signal. + +--- + +## Step 1: Web Search Results Analysis (mandatory!) + +**Please carefully analyze the Web search results provided above, focusing on:** + +1. Latest news related to "{market_question}" (focus on the past 24 hours) +2. Events or announcements that may have triggered this price movement +3. Publication timestamp of each news item + +**Web search results summary**: +(Please list key news from search results here, MUST include publication times) + +--- + +## Step 2: Twitter Social Media Analysis + +**Analyze the Twitter search results provided above:** + +1. When was the earliest related discussion? +2. What were the main topics discussed? +3. Were there any KOL or insider posts? +4. Did social media discussion precede mainstream news coverage? + +**Twitter analysis summary**: +(Please summarize key information and timeline from Twitter here) + +--- + +## Step 3: Timeline Comparison Analysis + +**Key question**: Did the price movement occur before or after news became public? + +- Price movement detection time: {detected_at} +- Earliest related news publication time found: [please fill in] +- Earliest social media discussion time found: [please fill in] + +**Timeline conclusion**: +(Did the price movement lead or lag the news?) + +--- + +## Step 4: Final Determination + +Based on the above analysis, provide your judgment in the following JSON format: + +```json +{{ + "signal_type": "LEADING_SIGNAL/NEWS_DRIVEN/SOCIAL_DRIVEN/SPECULATION", + "confidence": 0.0-1.0, + "is_leading_signal": true/false, + "news_found": true/false, + "earliest_news_time": "earliest related news publication time found, format YYYY-MM-DD HH:MM UTC, or null if none", + "earliest_social_time": "earliest social media discussion time found, format YYYY-MM-DD HH:MM UTC, or null if none", + "time_advantage_minutes": minutes price led news (if leading signal), otherwise 0, + "key_news_headlines": ["related news headline 1", "related news headline 2"], + "key_social_posts": ["key social media post summary 1", "key social media post summary 2"], + "reasoning": "brief explanation of your judgment basis", + "potential_information_source": "hypothesized information source (e.g., insider, social media leak, advance official notice, etc.)" +}} +``` + +**Judgment criteria**: +- **LEADING_SIGNAL**: At the time of price movement, web search finds no related news, or news publication time is significantly later than price movement (>=30 minutes) +- **NEWS_DRIVEN**: Clear related news found, with publication time before or close to the price movement time +- **SOCIAL_DRIVEN**: Early Twitter discussion found, but mainstream media has not yet reported +- **SPECULATION**: Neither news nor social discussion found, likely pure speculation + +**Special notes**: +- When is_leading_signal is true, detailed evidence must be provided +- time_advantage_minutes represents the time advantage of price over news +- This data will be used to build a "price leads news" research dataset + +--- + +Disclaimer: This analysis is for research purposes only and does not constitute investment advice.""" diff --git a/src/prompts/whale_analyzer.py b/src/prompts/whale_analyzer.py new file mode 100644 index 0000000..df33a26 --- /dev/null +++ b/src/prompts/whale_analyzer.py @@ -0,0 +1,315 @@ +"""Prompts for whale trade analysis.""" +from datetime import datetime, timezone +from typing import List + + +class WhaleAnalyzerPrompts: + """Prompts for LLM whale trade analysis.""" + + @staticmethod + def system_prompt() -> str: + """System prompt for whale trade analysis with tool-use.""" + current_utc = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC') + + return f"""You are a professional prediction market analyst and information asymmetry detection expert, specializing in analyzing large anomalous trades on Polymarket. + +**Current real time**: {current_utc} + +## Your Core Task + +Verify whether a flagged anomalous trade exhibits information asymmetry — i.e., whether the trader may possess information not yet reflected in market prices. + +## Data You Will Receive + +For each analysis task, you will receive the following structured data (in the user message): + +1. **Trade Details** — The whale trade that triggered the alert: amount, direction (BUY Yes or BUY No), purchase price, timestamp, trader wallet address, anomaly score +2. **Trade Interpretation** — Direction meaning (bullish/bearish), implied probability +3. **Trader Profile (JSON)** — Raw data about the trader: + - `ranking`: rank, PnL, total volume, verification status, username + - `behavior`: total trades, total volume, average trade size, large trade count and ratio, active markets + - `recent_trades`: recent trading records +6. **Whale's positions in other markets under the same event** — For detecting hedging, correlated bets, or arbitrage (real-time data from Polymarket positions API) +7. **Market Top 5 buyers and sellers** — Top 5 traders on each side with ranking, PnL, net volume (reflects smart money consensus direction) +8. **Market Information** — Market question, description, possible outcomes, current odds +9. **Historical anomaly signals** (if any) — Past anomalous trade signals detected on this market, for trend comparison + +**You must synthesize ALL of the above data in your analysis — do not neglect any dimension.** + +## Available Tools + +You can call the following tools to obtain real-time information (all results are real live data): + +- **search_web**: Search web news and analysis articles. Use for: event verification, official announcements, regulatory news, earnings, court rulings, legislative progress, etc. +- **search_twitter**: Search Twitter/X social media. Use for: real-time sentiment, KOL opinions, crypto community reactions, breaking news, etc. +- **search_telegram**: Search Telegram channels (WuBlockchain, Whale Alert, Polymarket official & news channels, etc.). Use for: crypto intelligence, token launch announcements, whale on-chain transfer alerts, and Polymarket community discussions on geopolitics, economics, politics, etc. +- **get_crypto_price**: Get real-time crypto prices (price, 24h/7d/30d change, market cap, volume, ATH). Use for: markets involving crypto price targets (e.g., "Will BTC reach $100k"). +- **get_crypto_market_overview**: Get global crypto market overview (total market cap, BTC/ETH dominance, 24h change). Use for: gauging overall crypto sentiment. +- **get_economic_data**: Get FRED macroeconomic data. Supports: fed_rate, cpi, unemployment, gdp, oil_price, wti, brent, gold, vix, sp500, yield_curve, jobless_claims, etc. Use for: Fed policy, inflation, employment, commodities, recession indicators. +- **get_stock_price**: Get stock/ETF real-time snapshot (price, change, volume). Supports: AAPL, TSLA, GS, SPY, QQQ, GLD, USO, etc. Use for: markets involving specific companies or sectors. +- **get_stock_news**: Get latest stock/company news. Use for: company events (IPO, earnings, lawsuits, M&A), CEO statements, regulatory actions. +- **get_bill_status**: Get US Congress bill status (requires congress number, bill type, and number). Use for: markets involving specific legislation (e.g., TikTok ban, crypto regulation, immigration bills). +- **get_recent_legislation**: Get recently updated US Congress bills. Use for: current legislative dynamics, political markets. +- **get_protocol_tvl**: Get DeFi protocol TVL, TVL changes (1h/24h/7d), chain distribution. Use for: token FDV markets, DeFi fundamentals, project health assessment. +- **get_token_unlocks**: Get token unlock/vesting schedules. Use for: token supply dynamics, FDV markets, predicting unlock sell pressure. +- **get_protocol_revenue**: Get DeFi protocol fees and revenue (24h/7d/30d/all-time). Use for: protocol fundamentals, comparing revenue to FDV. +- **get_wallet_transfers**: Get recent ERC-20 token transfers from an Ethereum wallet (USDC/USDT/WETH/DAI). Use for: checking if whale just received large USDC inflow (funding preparation), tracking wallet fund flows. +- **get_contract_info**: Query whether an Ethereum address is a smart contract, contract name, verification status. Use for: verifying project contract deployment, judging token launch market project progress. + +**Tool usage principles**: +- Based on market type and trade characteristics, decide which tools to call +- You may call one, multiple, or zero tools +- **You have a maximum of 3 tool-call rounds. Budget wisely: use Round 1 for broad search (web + twitter + telegram in parallel), Round 2 for targeted follow-up if needed, then produce your final analysis. Do NOT use all rounds just searching — reserve capacity for your final answer.** +- Do NOT call the same tool (e.g. search_web) more than 3 times total across all rounds +- If the first search already covers the topic well, stop searching and analyze + +**Tool collaboration and cross-verification (important)**: +- Information from different tools should be **cross-verified** — but 2-3 sources are sufficient, do not over-search +- When multiple tools return **contradictory results**, explicitly note the discrepancy and lower confidence +- Prefer breadth (web + twitter + domain-specific tool) over depth (web × 8 with slightly different keywords) + +## Polymarket Trading Mechanics + +Trade data represents taker's actual buy actions (SELL/close trades are filtered out), **no normalization applied**: +- **BUY Yes** = Buy Yes Token = **Bullish** (believes event will occur) +- **BUY No** = Buy No Token = **Bearish** (believes event will not occur) +- **Price** is taker's actual purchase price (0.0~1.0) — lower price means higher odds and more uncertainty + - Example: BUY Yes @ 0.06 = pay $0.06 per share, receive $1 if event occurs (~17x odds) + - Example: BUY No @ 0.30 = pay $0.30 per share, receive $1 if event doesn't occur (~3.3x odds) +- **Trade amount** (usdc_size) is taker's actual USDC spend +- We only monitor trades with buy price <= 0.7 (high-price buys on near-certain outcomes have no signal value) + +## Analysis Framework + +### Trader Credibility +Trader credibility (HIGH/MEDIUM/LOW/UNKNOWN) should be assessed comprehensively using all available raw data — do not rely on a single metric. Consider: +- **Ranking**: Lower rank number = more experienced participant. null means unranked +- **PnL**: Cumulative profit/loss directly reflects historical performance — high PnL is stronger evidence than high rank +- **Trading behavior**: Total trades, average trade size, large trade ratio reflect style and experience +- **Active markets**: Recent market types reflect the trader's domain expertise — is it relevant to the current market? +- **Recent trades**: Specific buy/sell directions, amounts, and prices help identify the trader's strategy pattern + +### Information Asymmetry Scoring Criteria (must be strictly followed) + +**"Information asymmetry" has a very strict definition**: The trader must possess information not yet reflected in the market — non-public, specific information (e.g., unannounced policy decisions, unreleased data, private negotiation outcomes). Simply being "a smart analyst", "experienced", or "highly ranked" does **NOT** constitute information asymmetry. + +**Score calibration benchmark (most trades should fall between 0.2-0.5)**: + +- **0.8-1.0 (Very High)**: Only when **clear evidence of non-public information** is found. Example: trade timing precisely hours before a major announcement that was completely unpredictable; or trader has known information channels (e.g., identified as a political insider). **Very few trades should reach this level.** +- **0.6-0.8 (High)**: High-ranked trader + trade timing highly aligned with an upcoming unpriced event + search reveals specific information not yet reflected in market. Multiple strong pieces of evidence must be present simultaneously. +- **0.4-0.6 (Medium)**: High-ranked trader's large trade + some information support but uncertainty about whether it's non-public. This is where **most moderately suspicious trades** should fall. +- **0.2-0.4 (Low)**: Some anomalous features but lacking information support, or trader ranking is average. **Most ordinary whale trades** should be in this range. +- **0.0-0.2 (Very Low)**: Unranked trader's routine trade, no anomalous signals. + +**Common overestimation mistakes (must avoid)**: +- Do NOT give 0.7+ just because the trader ranks high (high-ranked traders make many trades daily, the vast majority show no information asymmetry) +- Do NOT give 0.6+ just because the trade amount is large (large trades are routine for whales) +- Do NOT give high scores to short-term price prediction markets (e.g., "Bitcoin Up or Down 5 minutes") — these markets almost never involve non-public information +- Do NOT give high scores to near-expiry markets or trades with prices near 0 or 1 — this usually reflects normal market consensus +- Do NOT give high scores when all found information is public news (public information ≠ non-public information) +- Do NOT easily give high scores to large geopolitical/macro markets (e.g., Iran situation, presidential impeachment) — these markets have many participants and complex information sources; whale trades mostly reflect public analysis rather than non-public information + +**High-value scenarios to focus on**: +- **Niche markets** (daily volume < $500k) with large trades — fewer participants, larger information gap, whale signals more meaningful +- **New projects/token launches** (FDV, TGE, public sale) — project teams and early investors may have non-public information +- **Specific verifiable events** (will someone do something, will a company announce a decision) — small circle of insiders, clear information +- **Quiet markets suddenly attracting high-ranked traders with large trades** — anomalous behavior is the strongest signal + +## Event-Related Position Analysis + +Trade data includes the whale's positions in other markets under the same Event. You must analyze: +- **Hedge detection**: If the whale holds opposing positions in different markets under the same event, it may be a hedging strategy rather than a directional bet — lower information asymmetry score +- **Correlated bets**: If the whale holds same-direction positions across multiple markets under the same event (e.g., bullish on multiple related markets), this strengthens the signal +- **Arbitrage**: Price inconsistencies across markets under the same event may indicate arbitrage — this is not an information asymmetry signal + +## Market Long/Short Analysis + +Trade data includes the market's Top 5 buyers and sellers with rankings and positions. Analyze: +- **Smart money consensus**: If multiple high-ranked, high-PnL traders are on the same side, the signal is stronger +- **Counterparty analysis**: If the whale's counterparties are all low-ranked traders, the signal is more reliable; if counterparties include high-ranked traders, more caution is needed +- **Market concentration**: If one side's positions are heavily concentrated in a few large holders, the market may be more prone to sharp volatility + +## Key Principles +- Proactively use tools to gather latest information for trade verification +- When searches yield no supporting information, information asymmetry likelihood should decrease +- When confidence is low, recommend HOLD +- Whales can also be wrong or have other motivations (hedging, probing, etc.) +- Synthesize event-related positions and market long/short dynamics for comprehensive judgment +- **Time judgment**: Do NOT guess unknown event times (e.g., match start times). If you need to determine whether a trade occurred before or after an event, you MUST use tools to confirm the event time — never speculate""" + + @staticmethod + def analyze_whale_trade(trade_context: str, historical_context: str = "") -> str: + """ + Build the user prompt for analyzing a whale trade. + + Args: + trade_context: Formatted trade context from AnomalyDetector + historical_context: Historical anomaly signals (optional) + + Returns: + Complete user prompt + """ + history_section = "" + if historical_context: + history_section = f""" +--- + +{historical_context} + +--- +""" + + return f"""{trade_context} +{history_section} + +--- + +# Whale Trade Verification Task + +## Step 0: Pre-screening (must complete first) + +Before any search or analysis, determine whether this signal warrants a full report. + +**Screening criteria:** +- **Prioritize (low threshold)**: Niche markets, crypto/token launch related (FDV, TGE, public sale, protocol governance), specific verifiable events, quiet markets with sudden large trades +- **Higher threshold (need especially strong signals)**: Large geopolitical markets (war, sanctions, diplomacy), macro/Fed rate/election markets with many participants +- **Skip directly**: Sports/game results, markets with price near 0 or 1 (>=0.95 or <=0.05) + +Assess holistically based on trade amount, trader rank and profile, anomaly score, and market type. + +**If deemed not worth analyzing, output the following JSON and stop — do not proceed to subsequent steps:** +```json +{{{{"action": "SKIP", "reason": "one-line reason"}}}} +``` + +**If deemed worth analyzing, continue with the following steps.** + +--- + +## Complete the following steps: + +### 1. Information Gathering +Based on market topic, use available tools to search for relevant information: +- Latest news and developments on the market topic +- Social media discussions and sentiment +- Any events that may have triggered this trade + +### 2. Trade Signal Analysis +- Trader ranking and historical P&L performance +- Structured profile (ranking, PnL, trading behavior data, recent trades) +- Whether the trade timing is anomalous + +### 3. Event-Related Position Analysis +- Does the whale have positions in other markets under the same event? +- If opposing positions exist (e.g., holding both Yes and No, or hedging in related markets), it may be a hedge/arbitrage strategy — lower information asymmetry score +- If same-direction bets across multiple related markets, the signal is strengthened + +### 4. Market Long/Short Analysis +- Who are the Top 5 on each side? What are their rankings? +- Which side has the concentration of high-ranked, high-PnL traders? This represents smart money consensus +- What is the quality of the whale's counterparties? If counterparties also include high-ranked traders, be more cautious + +### 5. Information Gap Analysis +- Does the information found support the trade's direction? +- Has this information been fully priced by the market? +- If an information gap exists, how large is it? + +### 6. Historical Signal Comparison (if available) +- Are historical signals directionally consistent with the current signal? +- Were high-ranked traders involved? +- What are the trends in trade amounts and prices? + +### 7. Information Asymmetry Assessment + +Assess the trader's information advantage relative to public information. Core logic: +- I_public = the set of public information you can obtain through all tools +- I_trader = the set of information the trader used to make this trade decision +- Information asymmetry = I_trader - I_public +- If public information can fully explain the trade behavior → low score +- If public information cannot explain the trade behavior (trader may have additional sources, domain expertise, data speed advantage) → high score + +Output JSON assessment: + +```json +{{ + "information_asymmetry_score": 0.0-1.0, + "trader_credibility": "HIGH/MEDIUM/LOW/UNKNOWN", + "reasoning": "brief reasoning process", + "insider_evidence": "key evidence" +}} +``` + +Notes: +- information_asymmetry_score must be strictly calibrated: most trades should be 0.2-0.5, only give 0.7+ when clear evidence of information advantage is found +- Information advantage includes but is not limited to: domain expertise, data source speed difference, non-public channels, precise timing +- Trader ranking or trade size alone should NOT push information_asymmetry_score above 0.5 +- Ensure valid JSON output""" + + @staticmethod + def superforecaster_prompt(question: str, description: str, outcomes: List[str]) -> str: + """Superforecaster-style analysis prompt.""" + outcomes_str = ", ".join(outcomes) + + return f"""As a superforecaster, analyze the following prediction market: + +**Question**: {question} + +**Description**: {description} + +**Possible Outcomes**: {outcomes_str} + +Please use the following systematic approach: + +### 1. Problem Decomposition +- Break the question into smaller, more manageable parts +- Identify key components needed to answer the question + +### 2. Information Gathering +- Consider relevant quantitative data and qualitative insights +- Think about the latest relevant news and expert analysis + +### 3. Base Rate +- Use statistical baselines or historical averages as starting points +- Compare the current situation with similar historical events + +### 4. Factor Assessment +- List factors that may influence the outcome +- Assess each factor's impact, considering both positive and negative factors +- Weigh these factors using evidence + +### 5. Probabilistic Thinking +- Express predictions as probabilities, not certainties +- Assign likelihoods to different outcomes +- Acknowledge uncertainty + +Please provide probability estimates for each outcome, ensuring all probabilities sum to 100%. + +Output format: +```json +{{ + "analysis": "your detailed analysis", + "probabilities": {{ + "outcome1": 0.XX, + "outcome2": 0.XX + }}, + "confidence_level": "low/medium/high", + "key_factors": ["factor1", "factor2", "factor3"] +}} +```""" + + @staticmethod + def quick_decision_prompt(trade_summary: str) -> str: + """Quick decision prompt for time-sensitive situations.""" + return f"""Quickly analyze the following whale trade and provide a recommendation: + +{trade_summary} + +Output your decision in JSON format: +```json +{{ + "action": "BUY/SELL/HOLD", + "outcome": "the outcome to trade on", + "confidence": 0.0-1.0, + "reasoning": "one-line reason" +}} +```""" diff --git a/src/services/__init__.py b/src/services/__init__.py new file mode 100644 index 0000000..3bc2094 --- /dev/null +++ b/src/services/__init__.py @@ -0,0 +1,14 @@ +"""Services module.""" +from .market_fetcher import MarketFetcher +from .trade_monitor import TradeMonitor +from .anomaly_detector import AnomalyDetector +from .llm_analyzer import LLMAnalyzer +from .twitter_search import TwitterSearchService + +__all__ = [ + "MarketFetcher", + "TradeMonitor", + "AnomalyDetector", + "LLMAnalyzer", + "TwitterSearchService", +] diff --git a/src/services/anomaly_detector.py b/src/services/anomaly_detector.py new file mode 100644 index 0000000..3c3513c --- /dev/null +++ b/src/services/anomaly_detector.py @@ -0,0 +1,391 @@ +""" +Anomaly detection service — confidence scoring for whale trades. + +Mirrors the options flow confidence scoring from llm-trading-agent: + Base confidence: 0.50 + + Premium-to-threshold ratio: +0.20 (sqrt-scaled by market liquidity) + + Signal cleanliness (ask ratio): +0.10 + + Volume/OI equivalent (depth ratio): +0.10 + + Alert rule / cluster tier: +0.10 + +Total max = 1.0. Pre-filter threshold = 0.60 (matches options flow pipeline). +""" +import logging +import math +import time +from collections import defaultdict, deque +from datetime import datetime +from typing import Dict, List, Optional, Tuple + +from src.config import get_settings +from src.models.trade import WhaleTrade, TradeActivity, TraderHistory +from src.models.market import Market +from src.services.trader_profiler import TraderProfiler + +logger = logging.getLogger(__name__) + + +class AnomalyDetector: + """ + Confidence scoring for whale trades, mirroring options flow pipeline. + + 5-factor scoring (same structure as OptionsFlowSignalProvider._calculate_confidence): + 1. Base confidence: 0.50 + 2. Premium-to-threshold ratio: +0.20 (trade_size vs dynamic threshold) + 3. Signal cleanliness: +0.10 (conviction / price displacement) + 4. Depth ratio: +0.10 (trade_size vs market liquidity, like Volume/OI) + 5. Cluster tier: +0.10 (repeated same-direction trades, like alert_rule) + """ + + # Cluster detection + _CLUSTER_WINDOW_SECONDS = 300 # 5 minutes + _CLUSTER_MIN_COUNT = 3 + + def __init__(self): + self.settings = get_settings() + self.trader_profiler = TraderProfiler() + self._recent_trades: Dict[str, deque] = defaultdict( + lambda: deque(maxlen=50) + ) + + # ================================================================ + # Core scoring — mirrors OptionsFlowSignalProvider._calculate_confidence + # ================================================================ + + def get_anomaly_score( + self, + activity: TradeActivity, + market: Optional[Market] = None, + trader_history: Optional[TraderHistory] = None, + market_id: str = "", + ) -> Tuple[float, dict]: + """ + Calculate confidence score using options-flow-style 5-factor model. + + Returns: + (total_score, breakdown_dict) — total in [0.0, 1.0]. + """ + breakdown = {} + + # --- 1. Base confidence: 0.50 --- + breakdown["base"] = 0.50 + + # --- 2. Premium-to-threshold ratio: +0.20 max --- + # Mirrors _premium_ratio_bonus: min(0.20, (sqrt(ratio) - 1) * 0.20) + # where ratio = trade_size / dynamic_threshold + # dynamic_threshold = base_size * sqrt(market_volume / baseline_volume) + breakdown["premium_ratio"] = self._premium_ratio_bonus(activity, market) + + # --- 3. Signal cleanliness (conviction): +0.10 max --- + # Mirrors _ask_ratio_bonus: measures taker aggressiveness. + # In options: ASK-side ratio > 0.8 = full bonus. + # In Polymarket: price displacement from market mid = conviction. + breakdown["signal_clean"] = self._signal_cleanliness_bonus(activity, market) + + # --- 4. Depth ratio (like Volume/OI): +0.10 max --- + # Mirrors _volume_oi_bonus: trade_size / market_liquidity. + # High ratio = new significant positioning. + breakdown["depth_ratio"] = self._depth_ratio_bonus(activity, market) + + # --- 5. Cluster tier (like alert_rule): +0.10 max --- + # Mirrors _alert_rule_bonus: repeated same-direction activity. + # In options: RepeatedHitsAscendingFill = 0.10, RepeatedHits = 0.07. + # In Polymarket: multiple same-direction trades in 5-min window. + breakdown["cluster_tier"] = self._cluster_tier_bonus(activity, market_id) + + # --- Total --- + total = sum(breakdown.values()) + total = min(1.0, max(0.0, total)) + + return total, breakdown + + @staticmethod + def _premium_ratio_bonus(activity: TradeActivity, market: Optional[Market]) -> float: + """ + Trade size vs dynamic threshold, sqrt-scaled. + + Mirrors OptionsFlowSignalProvider._premium_ratio_bonus: + threshold = 100K * sqrt(market_cap / 40B) + bonus = min(0.20, (sqrt(premium / threshold) - 1) * 0.20) + + Polymarket mapping: + threshold = base_size * sqrt(market_volume / baseline_volume) + base_size = $5,000, baseline_volume = $1,000,000 + """ + max_bonus = 0.20 + trade_size = activity.usdc_size + + if trade_size <= 0: + return 0.0 + + # Dynamic threshold based on market volume (like market-cap scaling) + base_size = 10_000.0 + baseline_volume = 1_000_000.0 + + if market and market.volume > 0: + threshold = base_size * math.sqrt(market.volume / baseline_volume) + threshold = max(5_000.0, threshold) # floor $5K + else: + threshold = base_size + + ratio = trade_size / threshold + if ratio <= 1.0: + return 0.0 + + bonus = (math.sqrt(ratio) - 1) * max_bonus + return min(max_bonus, max(0.0, bonus)) + + @staticmethod + def _signal_cleanliness_bonus(activity: TradeActivity, market: Optional[Market]) -> float: + """ + Taker conviction / price displacement from market mid. + + Mirrors _ask_ratio_bonus logic: + ASK ratio > 0.8 → 0.10 (full), > 0.6 → 0.05 (half) + + Polymarket mapping: + A buyer paying significantly above market mid = aggressive taker (like ASK-side). + Displacement > 5% → 0.10, > 2% → 0.05. + """ + if not market or not market.outcome_prices: + return 0.0 + + # Get market mid price for the outcome the trader bought + if activity.outcome == "Yes": + market_mid = market.outcome_prices[0] + elif len(market.outcome_prices) > 1: + market_mid = market.outcome_prices[1] + else: + market_mid = 1.0 - market.outcome_prices[0] + + displacement = activity.price - market_mid + + if displacement > 0.05: + return 0.10 # strong conviction (like ASK ratio > 0.8) + if displacement > 0.02: + return 0.05 # moderate conviction (like ASK ratio > 0.6) + return 0.0 + + @staticmethod + def _depth_ratio_bonus(activity: TradeActivity, market: Optional[Market]) -> float: + """ + Trade size vs market liquidity (like Volume/OI). + + Mirrors _volume_oi_bonus: + V/OI > 3.0 → 0.10, > 1.5 → 0.07, > 1.0 → 0.03 + + Polymarket mapping: + depth_ratio = trade_size / market_liquidity + > 0.10 → 0.10, > 0.05 → 0.07, > 0.02 → 0.03 + """ + if not market or not market.liquidity or market.liquidity <= 0: + return 0.0 + + ratio = activity.usdc_size / market.liquidity + + if ratio > 0.10: + return 0.10 + if ratio > 0.05: + return 0.07 + if ratio > 0.02: + return 0.03 + return 0.0 + + def _cluster_tier_bonus(self, activity: TradeActivity, market_id: str = "") -> float: + """ + Cluster of same-direction trades in short window (like alert_rule tiers). + + Mirrors _alert_rule_bonus tier structure: + RepeatedHitsAscendingFill → 0.10 + RepeatedHits → 0.07 + SweepsFollowedByFloor → 0.05 + Single sweep → 0.03 + + Polymarket mapping: + 5+ same-direction trades in 5min → 0.10 + 3-4 trades → 0.07 + 2 trades with large volume → 0.03 + """ + key = market_id or activity.condition_id + recent = self._recent_trades.get(key) + if not recent: + return 0.0 + + now = activity.timestamp + cutoff = now - self._CLUSTER_WINDOW_SECONDS + + same_dir_count = 0 + same_dir_volume = 0.0 + for ts, side, size in recent: + if ts >= cutoff and side == activity.side: + same_dir_count += 1 + same_dir_volume += size + + if same_dir_count >= 5: + return 0.10 + if same_dir_count >= self._CLUSTER_MIN_COUNT: + return 0.07 + if same_dir_count >= 2 and same_dir_volume > 20_000: + return 0.03 + return 0.0 + + def record_trade(self, activity: TradeActivity, market_id: str): + """Record a trade for cluster detection. Call for every trade, not just whales.""" + self._recent_trades[market_id].append(( + activity.timestamp, + activity.side, + activity.usdc_size, + )) + + # ================================================================ + # Pre-filter (before LLM) + # ================================================================ + + def should_analyze( + self, + activity: TradeActivity, + market: Optional[Market] = None, + trader_history: Optional[TraderHistory] = None, + market_id: str = "", + min_score: float = 0.55, + ) -> Tuple[bool, float, dict]: + """ + Decide whether a whale trade warrants LLM analysis. + + Threshold 0.65: requires at least base (0.50) + one strong factor + to trigger LLM analysis. + """ + score, breakdown = self.get_anomaly_score( + activity, market, trader_history, market_id=market_id, + ) + return score >= min_score, score, breakdown + + # ================================================================ + # Legacy compatibility + # ================================================================ + + def is_anomalous_trade(self, activity: TradeActivity) -> bool: + """Check if a trade is anomalous based on size and price.""" + if activity.usdc_size < self.settings.min_trade_size_usd: + return False + if not (self.settings.min_price <= activity.price <= self.settings.max_price): + return False + return True + + def filter_whale_trades( + self, + trades: List[WhaleTrade], + min_score: float = 0.55, + ) -> List[WhaleTrade]: + """Filter whale trades by confidence score.""" + filtered = [] + for trade in trades: + score, _ = self.get_anomaly_score(trade.trade) + if score >= min_score: + filtered.append(trade) + return filtered + + # ================================================================ + # LLM context formatting + # ================================================================ + + def analyze_trade_context(self, whale_trade: WhaleTrade) -> dict: + """Analyze the context of a whale trade for LLM input.""" + trade = whale_trade.trade + + if trade.outcome == "Yes": + direction_meaning = f"Trader bought Yes Token @ {trade.price:.4f} — Bullish (believes event will occur)" + else: + direction_meaning = f"Trader bought No Token @ {trade.price:.4f} — Bearish (believes event will NOT occur)" + + implied_prob = trade.price + + market_state = "uncertain" + if whale_trade.market_outcome_prices: + max_price = max(whale_trade.market_outcome_prices) + if max_price > 0.7: + market_state = "leaning towards one outcome" + elif max_price < 0.6: + market_state = "highly uncertain" + + score, breakdown = self.get_anomaly_score(trade) + + return { + "trade_size_usd": trade.usdc_size, + "trade_side": trade.side, + "trade_price": trade.price, + "trade_outcome": trade.outcome, + "direction_meaning": direction_meaning, + "implied_probability": implied_prob, + "market_state": market_state, + "anomaly_score": score, + "anomaly_breakdown": breakdown, + "market_question": whale_trade.market_question, + "market_outcomes": whale_trade.market_outcomes, + "current_prices": whale_trade.market_outcome_prices, + } + + def format_for_llm(self, whale_trade: WhaleTrade) -> str: + """Format whale trade data for LLM analysis.""" + context = self.analyze_trade_context(whale_trade) + trade = whale_trade.trade + + prices_str = "" + for outcome, price in zip(context["market_outcomes"], context["current_prices"]): + prices_str += f" - {outcome}: {price:.2%}\n" + + trader_profile = self.trader_profiler.generate_profile( + wallet_address=trade.proxy_wallet or "Unknown", + ranking=whale_trade.trader_ranking, + history=whale_trade.trader_history, + ) + trader_profile_str = self.trader_profiler.format_profile_for_llm(trader_profile) + + bd = context["anomaly_breakdown"] + breakdown_str = ( + f" Base: {bd.get('base', 0):.2f} | " + f"Premium ratio: {bd.get('premium_ratio', 0):.2f} | " + f"Signal clean: {bd.get('signal_clean', 0):.2f} | " + f"Depth ratio: {bd.get('depth_ratio', 0):.2f} | " + f"Cluster tier: {bd.get('cluster_tier', 0):.2f}" + ) + + return f""" +## Whale Trade Anomaly Detection Report + +### Trade Details +- **Trade amount**: ${context['trade_size_usd']:,.2f} USDC +- **Trade direction**: BUY {context['trade_outcome']} Token ({'Bullish' if context['trade_outcome'] == 'Yes' else 'Bearish'}) +- **Buy price**: {context['trade_price']:.4f} (~{1/context['trade_price']:.1f}x odds) +- **Trade time**: {datetime.fromtimestamp(trade.timestamp).strftime('%Y-%m-%d %H:%M:%S UTC')} +- **Trader wallet**: {trade.proxy_wallet or 'Unknown'} + +### Confidence Score +- **Overall score**: {context['anomaly_score']:.2f}/1.00 +- **Score breakdown**: +{breakdown_str} + +### Trade Interpretation +- **Direction**: {context['direction_meaning']} +{trader_profile_str} + +### Market Information +- **Market question**: {context['market_question']} +- **Market description**: {whale_trade.market_description or 'N/A'} +- **Market state**: {context['market_state']} +- **Current odds**: +{prices_str} + +{whale_trade.format_event_positions()} + +{whale_trade.format_top_traders()} + +### Analysis Points +1. This is a ${context['trade_size_usd']:,.2f} large trade, direction: **BUY {context['trade_outcome']} Token** +2. {context['direction_meaning']} +3. **Focus on the Trader Profile JSON above — assess trader credibility from ranking, PnL, trading behavior, and recent trades** +4. **Analyze the whale's positions in other markets under the same event** — opposing positions may indicate hedging +5. **Reference the market's Top long/short holders** — which side has the concentration of high-ranked traders + +Please analyze the information asymmetry likelihood of this trade. +""" diff --git a/src/services/anomaly_history.py b/src/services/anomaly_history.py new file mode 100644 index 0000000..8d40ef2 --- /dev/null +++ b/src/services/anomaly_history.py @@ -0,0 +1,147 @@ +"""Anomaly history service - stores and retrieves historical anomaly signals via SQLite.""" +import logging +from typing import List, Optional + +from src.db.database import SignalDatabase +from src.models.anomaly_signal import AnomalySignal + +logger = logging.getLogger(__name__) + + +class AnomalyHistoryService: + """ + Service for storing and retrieving historical anomaly signals. + + Backend: SQLite via SignalDatabase. + All analyzed signals are stored for tracking accuracy. + Only signals with likelihood >= 0.4 are used as historical context for LLM. + """ + + # Minimum information asymmetry score to use as historical context for LLM + MIN_CONTEXT_LIKELIHOOD = 0.4 + + def __init__(self, db_path: str = "data/signals.db"): + """ + Initialize the anomaly history service. + + Args: + db_path: Path to the SQLite database file. + """ + self.db = SignalDatabase(db_path) + + def should_store_signal(self, insider_likelihood: float) -> bool: + """All analyzed signals should be stored for accuracy tracking.""" + return True + + def store_signal(self, signal: AnomalySignal) -> bool: + """ + Store an anomaly signal for accuracy tracking. + + All analyzed signals are stored regardless of likelihood. + """ + stored = self.db.insert_signal(signal) + if stored: + logger.info( + f"Stored signal for market {signal.market_id}: " + f"${signal.trade_size_usd:,.2f} {signal.trade_side} " + f"(IAS: {signal.information_asymmetry_score:.0%})" + ) + return stored + + def get_signals_for_market( + self, + market_id: str, + top_recent: int = 5, + top_likelihood: int = 5, + ) -> List[AnomalySignal]: + """ + Get historical anomaly signals for a market. + + Selects the most recent signals and highest insider likelihood signals, + then deduplicates and returns the combined list. + + Args: + market_id: The market ID + top_recent: Number of most recent signals to include + top_likelihood: Number of highest insider likelihood signals to include + + Returns: + List of AnomalySignal objects (deduplicated, sorted by trade timestamp newest first) + """ + return self.db.get_signals_for_market(market_id, top_recent, top_likelihood) + + def format_historical_signals_context( + self, + signals: List[AnomalySignal], + ) -> str: + """ + Format historical anomaly signals into a context string for LLM. + + Args: + signals: List of historical anomaly signals + + Returns: + Formatted string for LLM context + """ + if not signals: + return "" + + signal_count = len(signals) + + context = f""" +### Historical Anomaly Signals ({signal_count} total) + +**Important**: {signal_count} anomalous trades have been previously detected on this market. Please analyze these historical signals together with the current signal to provide a comprehensive information asymmetry assessment. + +""" + for i, signal in enumerate(signals, 1): + context += f""" +--- +#### Historical Signal {i} +{signal.to_context_string()} +--- +""" + + context += """ +**Comprehensive Analysis Points**: +1. Compare trade directions across all signals (historical + current) — is there a consistent trend? +2. Compare different traders' rankings and histories — where is the "smart money" flowing? +3. If multiple high-ranked traders point in the same direction, information asymmetry likelihood increases significantly +4. If signal directions conflict, analyze reasons (time changes, new information, differing judgments) +5. Consider time factor: more recent signals are more relevant +6. Observe trade amount trends: are amounts increasing? +""" + return context + + def get_all_market_ids(self) -> List[str]: + """ + Get all market IDs that have stored anomaly signals. + + Returns: + List of market IDs + """ + return self.db.get_all_market_ids() + + def get_signal_count(self, market_id: Optional[str] = None) -> int: + """ + Get the number of stored signals. + + Args: + market_id: Optional market ID to filter by + + Returns: + Number of stored signals + """ + return self.db.get_signal_count(market_id) + + def cleanup_old_signals(self, max_age_days: int = 30) -> int: + """ + Remove signals older than the specified number of days. + + Args: + max_age_days: Maximum age of signals to keep + + Returns: + Number of signals removed + """ + return self.db.cleanup_old_signals(max_age_days) diff --git a/src/services/coingecko.py b/src/services/coingecko.py new file mode 100644 index 0000000..16795af --- /dev/null +++ b/src/services/coingecko.py @@ -0,0 +1,169 @@ +"""CoinGecko API service for cryptocurrency market data.""" +import logging +from typing import Optional + +import httpx +from src.utils.http import get_client + +logger = logging.getLogger(__name__) + +COINGECKO_API = "https://api.coingecko.com/api/v3" + +# Common coin ID mapping (Polymarket markets often use ticker symbols) +TICKER_TO_ID = { + "BTC": "bitcoin", + "ETH": "ethereum", + "SOL": "solana", + "XRP": "ripple", + "DOGE": "dogecoin", + "ADA": "cardano", + "AVAX": "avalanche-2", + "DOT": "polkadot", + "MATIC": "matic-network", + "LINK": "chainlink", + "UNI": "uniswap", + "SHIB": "shiba-inu", + "LTC": "litecoin", + "BNB": "binancecoin", + "NEAR": "near", + "ARB": "arbitrum", + "OP": "optimism", + "APT": "aptos", + "SUI": "sui", + "PEPE": "pepe", +} + + +def _resolve_coin_id(query: str) -> str: + """Resolve a ticker or name to a CoinGecko coin ID.""" + q = query.strip().upper() + if q in TICKER_TO_ID: + return TICKER_TO_ID[q] + # Try lowercase as-is (CoinGecko IDs are lowercase) + return query.strip().lower() + + +class CoinGeckoService: + """ + CoinGecko API client for crypto market data. + + Free tier: 30 calls/min, no API key required. + """ + + def __init__(self): + self._client = get_client(timeout=15.0) + + def get_price(self, coin: str) -> str: + """ + Get current price, 24h change, market cap, and volume for a cryptocurrency. + + Args: + coin: Ticker symbol (BTC, ETH, SOL) or CoinGecko ID (bitcoin, ethereum) + + Returns: + Formatted price report string. + """ + coin_id = _resolve_coin_id(coin) + + try: + resp = self._client.get( + f"{COINGECKO_API}/coins/{coin_id}", + params={ + "localization": "false", + "tickers": "false", + "community_data": "false", + "developer_data": "false", + "sparkline": "false", + }, + ) + + if resp.status_code == 404: + return f"Coin '{coin}' (id: {coin_id}) not found on CoinGecko." + resp.raise_for_status() + data = resp.json() + + market = data.get("market_data", {}) + name = data.get("name", coin_id) + symbol = data.get("symbol", "").upper() + + price = market.get("current_price", {}).get("usd") + change_24h = market.get("price_change_percentage_24h") + change_7d = market.get("price_change_percentage_7d") + change_30d = market.get("price_change_percentage_30d") + high_24h = market.get("high_24h", {}).get("usd") + low_24h = market.get("low_24h", {}).get("usd") + market_cap = market.get("market_cap", {}).get("usd") + volume_24h = market.get("total_volume", {}).get("usd") + ath = market.get("ath", {}).get("usd") + ath_change = market.get("ath_change_percentage", {}).get("usd") + + lines = [ + f"--- {name} ({symbol}) Market Data ---", + f"Price: ${price:,.2f}" if price else "Price: N/A", + ] + + if high_24h and low_24h: + lines.append(f"24h Range: ${low_24h:,.2f} - ${high_24h:,.2f}") + + if change_24h is not None: + lines.append(f"24h Change: {change_24h:+.2f}%") + if change_7d is not None: + lines.append(f"7d Change: {change_7d:+.2f}%") + if change_30d is not None: + lines.append(f"30d Change: {change_30d:+.2f}%") + + if market_cap: + lines.append(f"Market Cap: ${market_cap:,.0f}") + if volume_24h: + lines.append(f"24h Volume: ${volume_24h:,.0f}") + + if ath and ath_change is not None: + lines.append(f"ATH: ${ath:,.2f} ({ath_change:+.1f}% from ATH)") + + lines.append("---") + return "\n".join(lines) + + except httpx.HTTPError as e: + msg = f"CoinGecko API error for '{coin}': {e}" + logger.error(msg) + return msg + except Exception as e: + msg = f"CoinGecko query failed for '{coin}': {e}" + logger.error(msg) + return msg + + def get_market_overview(self) -> str: + """ + Get global crypto market overview: total market cap, BTC dominance, etc. + + Returns: + Formatted global market overview string. + """ + try: + resp = self._client.get(f"{COINGECKO_API}/global") + resp.raise_for_status() + data = resp.json().get("data", {}) + + total_cap = data.get("total_market_cap", {}).get("usd", 0) + total_vol = data.get("total_volume", {}).get("usd", 0) + btc_dom = data.get("market_cap_percentage", {}).get("btc", 0) + eth_dom = data.get("market_cap_percentage", {}).get("eth", 0) + change_24h = data.get("market_cap_change_percentage_24h_usd", 0) + active_coins = data.get("active_cryptocurrencies", 0) + + lines = [ + "--- Global Crypto Market Overview ---", + f"Total Market Cap: ${total_cap:,.0f}", + f"24h Change: {change_24h:+.2f}%", + f"24h Volume: ${total_vol:,.0f}", + f"BTC Dominance: {btc_dom:.1f}%", + f"ETH Dominance: {eth_dom:.1f}%", + f"Active Coins: {active_coins:,}", + "---", + ] + return "\n".join(lines) + + except Exception as e: + msg = f"CoinGecko global query failed: {e}" + logger.error(msg) + return msg \ No newline at end of file diff --git a/src/services/congress.py b/src/services/congress.py new file mode 100644 index 0000000..eccbeff --- /dev/null +++ b/src/services/congress.py @@ -0,0 +1,205 @@ +"""Congress.gov API service for U.S. legislative data.""" +import logging +from typing import Optional + +import httpx +from src.utils.http import get_client + +logger = logging.getLogger(__name__) + +CONGRESS_API = "https://api.congress.gov/v3" + + +class CongressService: + """ + Congress.gov API client for U.S. legislative data. + + Covers: bills, votes, members, committees, nominations. + """ + + def __init__(self, api_key: str): + self.api_key = api_key + self._client = get_client(timeout=15.0) + + def is_available(self) -> bool: + return bool(self.api_key and self.api_key.strip()) + + def _get(self, path: str, params: Optional[dict] = None) -> dict: + """Make authenticated GET request.""" + params = params or {} + params["api_key"] = self.api_key + params["format"] = "json" + resp = self._client.get(f"{CONGRESS_API}{path}", params=params) + resp.raise_for_status() + return resp.json() + + def search_bills(self, query: str, limit: int = 5) -> str: + """ + Search for bills by keyword. + + Args: + query: Search keywords (e.g. 'TikTok ban', 'crypto regulation', 'immigration') + limit: Number of results (1-10) + + Returns: + Formatted report of matching bills with status. + """ + limit = max(1, min(limit, 10)) + + try: + data = self._get("/bill", params={ + "limit": limit, + "sort": "updateDate+desc", + }) + + bills = data.get("bills", []) + if not bills: + return f"No bills found on Congress.gov." + + # Filter by query keyword in title (API doesn't support text search directly) + # So we fetch recent bills and note to user + lines = [f"--- Congress.gov: Recent Bills ---"] + lines.append(f"(Showing {len(bills)} most recently updated bills)") + lines.append("") + + for i, bill in enumerate(bills, 1): + bill_type = bill.get("type", "") + number = bill.get("number", "") + title = bill.get("title", "No title") + congress = bill.get("congress", "") + update_date = bill.get("updateDate", "")[:10] + latest_action = bill.get("latestAction", {}) + action_text = latest_action.get("text", "") + action_date = latest_action.get("actionDate", "") + + bill_id = f"{bill_type} {number}" if bill_type and number else "N/A" + + lines.append(f"{i}. **{bill_id}** (Congress {congress})") + lines.append(f" Title: {title[:150]}") + lines.append(f" Updated: {update_date}") + if action_text: + lines.append(f" Latest Action ({action_date}): {action_text[:150]}") + lines.append("") + + lines.append("---") + return "\n".join(lines) + + except Exception as e: + msg = f"Congress.gov search failed: {e}" + logger.error(msg) + return msg + + def get_bill_status(self, congress: int, bill_type: str, bill_number: int) -> str: + """ + Get detailed status of a specific bill. + + Args: + congress: Congress number (e.g. 119 for current) + bill_type: Bill type (hr, s, hjres, sjres) + bill_number: Bill number + + Returns: + Formatted bill status report. + """ + bt = bill_type.strip().lower() + + try: + data = self._get(f"/bill/{congress}/{bt}/{bill_number}") + bill = data.get("bill", {}) + + if not bill: + return f"Bill {bt.upper()} {bill_number} (Congress {congress}) not found." + + title = bill.get("title", "No title") + introduced = bill.get("introducedDate", "N/A") + sponsors = bill.get("sponsors", []) + sponsor_str = ", ".join( + f"{s.get('firstName', '')} {s.get('lastName', '')} ({s.get('party', '')}-{s.get('state', '')})" + for s in sponsors[:3] + ) if sponsors else "N/A" + + latest_action = bill.get("latestAction", {}) + action_text = latest_action.get("text", "N/A") + action_date = latest_action.get("actionDate", "") + + policy_area = bill.get("policyArea", {}).get("name", "N/A") + committees_count = bill.get("committees", {}).get("count", 0) + cosponsors_count = bill.get("cosponsors", {}).get("count", 0) + actions_count = bill.get("actions", {}).get("count", 0) + + # Determine bill progress + constitutional = bill.get("constitutionalAuthorityStatementText", "") + + lines = [ + f"--- Bill Status: {bt.upper()} {bill_number} (Congress {congress}) ---", + f"Title: {title}", + f"Introduced: {introduced}", + f"Sponsor: {sponsor_str}", + f"Cosponsors: {cosponsors_count}", + f"Policy Area: {policy_area}", + f"Committees Referred: {committees_count}", + f"Total Actions: {actions_count}", + f"", + f"Latest Action ({action_date}): {action_text}", + f"---", + ] + return "\n".join(lines) + + except httpx.HTTPStatusError as e: + if e.response.status_code == 404: + return f"Bill {bt.upper()} {bill_number} (Congress {congress}) not found." + return f"Congress.gov API error: HTTP {e.response.status_code}" + except Exception as e: + msg = f"Congress.gov bill query failed: {e}" + logger.error(msg) + return msg + + def get_recent_votes(self, chamber: str = "senate", limit: int = 5) -> str: + """ + Get recent roll call votes. + + Args: + chamber: 'senate' or 'house' + limit: Number of votes (1-10) + + Returns: + Formatted report of recent votes. + """ + chamber = chamber.strip().lower() + if chamber not in ("senate", "house"): + chamber = "senate" + limit = max(1, min(limit, 10)) + + try: + # Get current congress number (119th as of 2025-2026) + congress = 119 + + data = self._get(f"/bill", params={ + "limit": limit, + "sort": "updateDate+desc", + }) + + # Use the nominations endpoint for Senate votes + # or fall back to recent bill actions + lines = [f"--- Recent Congressional Activity ({chamber.title()}) ---"] + + bills = data.get("bills", []) + for i, bill in enumerate(bills[:limit], 1): + bill_type = bill.get("type", "") + number = bill.get("number", "") + title = bill.get("title", "")[:100] + latest = bill.get("latestAction", {}) + action = latest.get("text", "")[:120] + date = latest.get("actionDate", "") + + lines.append(f"{i}. {bill_type} {number}: {title}") + lines.append(f" {date}: {action}") + lines.append("") + + lines.append("---") + return "\n".join(lines) + + except Exception as e: + msg = f"Congress.gov votes query failed: {e}" + logger.error(msg) + return msg \ No newline at end of file diff --git a/src/services/daily_briefing.py b/src/services/daily_briefing.py new file mode 100644 index 0000000..49a3b35 --- /dev/null +++ b/src/services/daily_briefing.py @@ -0,0 +1,390 @@ +"""Daily briefing service - generates daily summary of high-value signals.""" +import logging +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from datetime import datetime, timedelta +from pathlib import Path +from typing import List, Dict, Optional + +from src.config import get_settings +from src.db.database import SignalDatabase +from src.services.stats_engine import StatsEngine + +logger = logging.getLogger(__name__) + +# Directories +VOLATILITY_DIR = Path(__file__).parent.parent.parent / "price_volatility" +BRIEFINGS_DIR = Path(__file__).parent.parent.parent / "daily_briefings" + + +class DailyBriefingGenerator: + """ + Generates daily briefings summarizing high-value signals. + + Includes: + - Smart money signals with information asymmetry score >= 60% + - Price volatility alerts + - Historical signal performance stats + """ + + # Minimum information asymmetry score to include in briefing + MIN_IAS = 0.6 # 60% + + # Maximum signals to include when falling back to top-N + FALLBACK_TOP_N = 5 + + def __init__(self, db_path: str = "data/signals.db"): + """Initialize the briefing generator.""" + BRIEFINGS_DIR.mkdir(parents=True, exist_ok=True) + self.db = SignalDatabase(db_path) + self.stats_engine = StatsEngine(self.db) + + def _get_date_range(self, date: datetime) -> tuple: + """ + Get start and end timestamps for a given date. + + Args: + date: The date to get range for + + Returns: + Tuple of (start_timestamp, end_timestamp) + """ + start = datetime(date.year, date.month, date.day, 0, 0, 0) + end = start + timedelta(days=1) + return int(start.timestamp()), int(end.timestamp()) + + def _load_insider_signals(self, date: datetime) -> tuple: + """ + Load smart money signals for a specific date from the database. + + First tries to find signals with likelihood >= 60%. + If none found, falls back to the top 5 by likelihood. + + Args: + date: The date to load signals for + + Returns: + Tuple of (signals list as dicts, is_fallback bool) + """ + date_str = date.strftime("%Y-%m-%d") + + # Query all signals detected on this date + all_signals = self.db.get_all_signals(limit=500, offset=0) + day_signals = [] + for signal in all_signals: + if signal.detected_at.strftime("%Y-%m-%d") == date_str: + day_signals.append(signal) + + if not day_signals: + return [], False + + # Sort by likelihood descending + day_signals.sort(key=lambda s: s.information_asymmetry_score, reverse=True) + + # Convert to dicts for backward compat with _format_briefing + def signal_to_dict(s): + return { + "market_id": s.market_id, + "market_question": s.market_question, + "transaction_hash": s.transaction_hash, + "trade_size_usd": s.trade_size_usd, + "trade_price": s.trade_price, + "trade_outcome": s.trade_outcome, + "information_asymmetry_score": s.information_asymmetry_score, + "reasoning": s.reasoning, + "insider_evidence": s.insider_evidence, + "detected_at": s.detected_at.isoformat(), + } + + # Filter high-likelihood signals + high_likelihood = [ + signal_to_dict(s) for s in day_signals + if s.information_asymmetry_score >= self.MIN_IAS + ] + + if high_likelihood: + return high_likelihood, False + + # Fallback: top N signals by likelihood + return [signal_to_dict(s) for s in day_signals[:self.FALLBACK_TOP_N]], True + + def _load_volatility_alerts(self, date: datetime) -> List[Dict]: + """ + Load price volatility alerts for a specific date. + + Args: + date: The date to load alerts for + + Returns: + List of volatility alerts + """ + import json + alerts_file = VOLATILITY_DIR / "volatility_alerts.json" + date_str = date.strftime("%Y-%m-%d") + + if not alerts_file.exists(): + return [] + + try: + with open(alerts_file, 'r', encoding='utf-8') as f: + all_alerts = json.load(f) + + # Filter alerts for the target date + day_alerts = [ + alert for alert in all_alerts + if alert.get("detected_at", "").startswith(date_str) + ] + + # Sort by price change magnitude descending + day_alerts.sort( + key=lambda x: abs(x.get("price_change_percent", 0)), + reverse=True + ) + + return day_alerts + + except Exception as e: + logger.error(f"Error loading volatility alerts: {e}") + return [] + + def _format_briefing( + self, + date: datetime, + insider_signals: List[Dict], + volatility_alerts: List[Dict], + is_fallback: bool = False, + ) -> str: + """ + Format the daily briefing as markdown. + + Args: + date: The date of the briefing + insider_signals: List of insider signals + volatility_alerts: List of price volatility alerts + is_fallback: True if signals are fallback (none >= 60%) + + Returns: + Formatted markdown briefing + """ + date_str = date.strftime("%Y-%m-%d") + + lines = [ + f"# Daily Signal Briefing - {date_str}", + "", + f"Generated at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", + "", + ] + + # Summary stats + if is_fallback: + summary_line = f"- No signals with confidence >= 60% today; showing the top **{len(insider_signals)}** by confidence" + else: + summary_line = f"- High-confidence information asymmetry signals: **{len(insider_signals)}** (confidence >= 60%)" + + lines.extend([ + "## Today's Overview", + "", + summary_line, + f"- Abnormal price volatility events: **{len(volatility_alerts)}**", + "", + ]) + + # Insider trading signals section + if is_fallback: + section_title = "## Today's Top Anomalous Trades by Confidence" + else: + section_title = "## High-Confidence Information Asymmetry Signals" + + lines.extend([ + "---", + "", + section_title, + "", + ]) + + if insider_signals: + for i, signal in enumerate(insider_signals, 1): + likelihood = signal.get("information_asymmetry_score", 0) + market_question = signal.get("market_question", "Unknown") + trade_size = signal.get("trade_size_usd", 0) + trade_price = signal.get("trade_price", 0) + trade_outcome = signal.get("trade_outcome", "Yes") + reasoning = signal.get("reasoning", "") + insider_evidence = signal.get("insider_evidence", "") + detected_at = signal.get("detected_at", "") + + # Odds calculation + odds_str = f"{1/trade_price:.1f}x" if trade_price > 0 else "N/A" + + lines.extend([ + f"### {i}. {market_question[:80]}{'...' if len(market_question) > 80 else ''}", + "", + f"| Metric | Value |", + f"|--------|-------|", + f"| Info Asymmetry | **{likelihood:.0%}** |", + f"| Direction | BUY {trade_outcome} Token ({'Bullish' if trade_outcome == 'Yes' else 'Bearish'}) |", + f"| Entry Price | {trade_price:.4f} (Odds {odds_str}) |", + f"| Trade Size | **${trade_size:,.0f}** USDC |", + f"| Detected At | {detected_at} |", + "", + ]) + + if reasoning: + lines.extend([ + f"**Analysis**: {reasoning}", + "", + ]) + + if insider_evidence: + lines.extend([ + f"**Insider Evidence**: {insider_evidence}", + "", + ]) + + lines.append("") + else: + lines.extend([ + "*No anomalous trade signals today*", + "", + ]) + + # Volatility alerts section + lines.extend([ + "---", + "", + "## Abnormal Price Volatility", + "", + ]) + + if volatility_alerts: + lines.extend([ + "| Market | Direction | Change | Start Price | End Price | Detected At |", + "|--------|-----------|--------|-------------|-----------|-------------|", + ]) + + for alert in volatility_alerts: + market_question = alert.get("market_question", "Unknown") + # Truncate long market questions + if len(market_question) > 40: + market_question = market_question[:37] + "..." + + direction = "Down" if alert.get("direction") == "DOWN" else "Up" + price_change = abs(alert.get("price_change_percent", 0)) + start_price = alert.get("start_price", 0) + end_price = alert.get("end_price", 0) + detected_at = alert.get("detected_at", "")[:16] # Trim to minute + + lines.append( + f"| {market_question} | {direction} | {price_change:.1%} | " + f"{start_price:.2%} | {end_price:.2%} | {detected_at} |" + ) + + lines.append("") + else: + lines.extend([ + "*No abnormal price volatility today*", + "", + ]) + + # Signal performance stats section + stats_summary = self.stats_engine.format_stats_summary() + if stats_summary: + lines.extend([ + "---", + "", + stats_summary, + ]) + + # Footer + lines.extend([ + "---", + "", + "*This briefing was automatically generated by Polymarket Whale Watcher*", + ]) + + return "\n".join(lines) + + def generate_briefing(self, date: Optional[datetime] = None) -> Optional[str]: + """ + Generate daily briefing for a specific date. + + Args: + date: The date to generate briefing for (defaults to yesterday) + + Returns: + Path to the saved briefing file, or None if no signals + """ + if date is None: + # Default to yesterday + date = datetime.now() - timedelta(days=1) + + date_str = date.strftime("%Y-%m-%d") + logger.info(f"Generating daily briefing for {date_str}") + + # Load signals + insider_signals, is_fallback = self._load_insider_signals(date) + volatility_alerts = self._load_volatility_alerts(date) + + # Check if there's anything to report + if not insider_signals and not volatility_alerts: + logger.info(f"No signals for {date_str}, skipping briefing") + return None + + # Generate briefing + briefing_content = self._format_briefing(date, insider_signals, volatility_alerts, is_fallback) + + # Save to file + filename = f"briefing_{date_str}.md" + filepath = BRIEFINGS_DIR / filename + + with open(filepath, 'w', encoding='utf-8') as f: + f.write(briefing_content) + + logger.info( + f"Daily briefing saved to {filepath} " + f"({len(insider_signals)} insider signals, {len(volatility_alerts)} volatility alerts)" + ) + + # Send email notification + self._send_email(date_str, briefing_content) + + return str(filepath) + + def _send_email(self, date_str: str, content: str) -> None: + """Send briefing via email if configured.""" + settings = get_settings() + if not settings.email_enabled: + return + if not settings.email_sender or not settings.email_password: + logger.warning("Email enabled but sender/password not configured, skipping") + return + + try: + recipients = [r.strip() for r in settings.email_recipient.split(",") if r.strip()] + + msg = MIMEMultipart("alternative") + msg["Subject"] = f"Polymarket Whale Daily Briefing - {date_str}" + msg["From"] = settings.email_sender + msg["To"] = ", ".join(recipients) + + # Markdown content as plain text + text_part = MIMEText(content, "plain", "utf-8") + msg.attach(text_part) + + with smtplib.SMTP_SSL(settings.email_smtp_server, settings.email_smtp_port) as server: + server.login(settings.email_sender, settings.email_password) + server.sendmail(settings.email_sender, recipients, msg.as_string()) + + logger.info(f"Daily briefing emailed to {', '.join(recipients)}") + except Exception as e: + logger.error(f"Failed to send briefing email: {e}") + + def generate_today_briefing(self) -> Optional[str]: + """ + Generate briefing for today (useful for testing or end-of-day summary). + + Returns: + Path to the saved briefing file, or None if no signals + """ + return self.generate_briefing(datetime.now()) diff --git a/src/services/ddg_search.py b/src/services/ddg_search.py new file mode 100644 index 0000000..4e4afdb --- /dev/null +++ b/src/services/ddg_search.py @@ -0,0 +1,62 @@ +"""DuckDuckGo web search service — free, no API key required.""" +import logging + +logger = logging.getLogger(__name__) + + +class DDGSearchService: + """Web search service using duckduckgo-search (no API key needed).""" + + def __init__(self): + self._available: bool | None = None + + def is_available(self) -> bool: + if self._available is None: + try: + from duckduckgo_search import DDGS # noqa: F401 + self._available = True + except ImportError: + logger.warning( + "duckduckgo-search not installed. " + "Install with: pip install duckduckgo-search" + ) + self._available = False + return self._available + + def search(self, query: str, max_results: int = 5) -> str: + if not self.is_available(): + return "Web search unavailable: duckduckgo-search package not installed." + + try: + from duckduckgo_search import DDGS + + with DDGS() as ddgs: + results = list(ddgs.text(query, max_results=max_results)) + + if not results: + return f"No web search results found for '{query}'." + + report = [f"--- Web Search Results for '{query}' ---"] + for idx, item in enumerate(results, 1): + title = item.get("title", "No title") + url = item.get("href", "") + body = item.get("body", "")[:300] + if len(item.get("body", "")) > 300: + body += "..." + report.append(f"{idx}. **{title}**") + report.append(f" Source: {url}") + report.append(f" {body}") + report.append("") + report.append("-------------------------------------------") + return "\n".join(report) + + except Exception as e: + logger.error(f"DuckDuckGo search failed: {e}") + return f"Web search failed: {str(e)}" + + def search_for_market(self, market_question: str, max_results: int = 5) -> str: + query = market_question[:200] + result = self.search(query, max_results=max_results) + if "No web search results" not in result and "Error" not in result and "unavailable" not in result: + return "## 🔍 Web Search Results (News & Analysis)\n" + result + return f"No relevant web results found for: {market_question[:50]}..." diff --git a/src/services/defillama.py b/src/services/defillama.py new file mode 100644 index 0000000..53bfaff --- /dev/null +++ b/src/services/defillama.py @@ -0,0 +1,340 @@ +"""DeFiLlama API service for DeFi protocol data (TVL, revenue, token unlocks).""" +import logging +from typing import Optional + +import httpx +from src.utils.http import get_client + +logger = logging.getLogger(__name__) + +DEFILLAMA_API = "https://api.llama.fi" + + +def _fmt_usd(value) -> str: + """Format a dollar value with appropriate suffix.""" + if value is None: + return "N/A" + if isinstance(value, list): + return "N/A" + try: + value = float(value) + except (TypeError, ValueError): + return "N/A" + abs_val = abs(value) + if abs_val >= 1_000_000_000: + return f"${value / 1_000_000_000:,.2f}B" + if abs_val >= 1_000_000: + return f"${value / 1_000_000:,.2f}M" + if abs_val >= 1_000: + return f"${value / 1_000:,.2f}K" + return f"${value:,.2f}" + + +class DefiLlamaService: + """ + DeFiLlama API client for DeFi protocol analytics. + + Free API, no key required. Rate limits are generous. + """ + + def __init__(self): + self._client = get_client(timeout=20.0) + self._protocols_cache: Optional[list] = None + + @staticmethod + def is_available() -> bool: + """Always available - no API key needed.""" + return True + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _fetch_protocols_list(self) -> list: + """Fetch and cache the full protocols list for slug lookups.""" + if self._protocols_cache is not None: + return self._protocols_cache + try: + resp = self._client.get(f"{DEFILLAMA_API}/protocols") + resp.raise_for_status() + self._protocols_cache = resp.json() + return self._protocols_cache + except Exception as e: + logger.error(f"Failed to fetch DeFiLlama protocols list: {e}") + return [] + + def _resolve_slug(self, query: str) -> Optional[str]: + """ + Fuzzy-match a user query to a DeFiLlama protocol slug. + + Tries exact slug match, then name match, then substring match. + """ + q = query.strip().lower() + protocols = self._fetch_protocols_list() + + # 1) Exact slug match + for p in protocols: + if p.get("slug", "").lower() == q: + return p["slug"] + + # 2) Exact name match (case-insensitive) + for p in protocols: + if p.get("name", "").lower() == q: + return p["slug"] + + # 3) Substring match on slug or name – prefer shortest match (most specific) + candidates = [] + for p in protocols: + slug = p.get("slug", "").lower() + name = p.get("name", "").lower() + if q in slug or q in name: + candidates.append(p) + + if candidates: + # Sort by TVL descending so the most prominent protocol wins ties + candidates.sort(key=lambda p: p.get("tvl") or 0, reverse=True) + return candidates[0]["slug"] + + return None + + # ------------------------------------------------------------------ + # Public methods – all return formatted strings + # ------------------------------------------------------------------ + + def get_protocol_tvl(self, protocol: str) -> str: + """ + Get protocol TVL, TVL changes, and chain breakdown. + + Args: + protocol: Protocol name or slug (e.g., "aave", "Lido", "uniswap"). + + Returns: + Formatted TVL report string. + """ + slug = self._resolve_slug(protocol) + if slug is None: + return f"Protocol '{protocol}' not found on DeFiLlama." + + try: + resp = self._client.get(f"{DEFILLAMA_API}/protocol/{slug}") + if resp.status_code == 404: + return f"Protocol '{protocol}' (slug: {slug}) not found on DeFiLlama." + resp.raise_for_status() + data = resp.json() + + name = data.get("name", slug) + symbol = data.get("symbol", "") + category = data.get("category", "N/A") + # tvl field is a historical list; get current TVL from last entry or currentChainTvls + tvl_data = data.get("tvl") + if isinstance(tvl_data, list) and tvl_data: + tvl = tvl_data[-1].get("totalLiquidityUSD", 0) + elif isinstance(tvl_data, (int, float)): + tvl = tvl_data + else: + tvl = None + chain_tvls = data.get("chainTvls", {}) + + # TVL changes + change_1h = data.get("change_1h") + change_1d = data.get("change_1d") + change_7d = data.get("change_7d") + + lines = [ + f"--- {name} ({symbol}) TVL Report ---", + f"Category: {category}", + f"Total TVL: {_fmt_usd(tvl)}", + ] + + if change_1h is not None: + lines.append(f"1h Change: {change_1h:+.2f}%") + if change_1d is not None: + lines.append(f"24h Change: {change_1d:+.2f}%") + if change_7d is not None: + lines.append(f"7d Change: {change_7d:+.2f}%") + + # Chain breakdown – show top chains by TVL + if chain_tvls: + # chainTvls has sub-objects; the latest TVL per chain is the last entry + chain_summary = {} + for chain_name, chain_data in chain_tvls.items(): + # Skip aggregated keys like "staking", "borrowed", "pool2" + if "-" in chain_name or chain_name in ("staking", "borrowed", "pool2", "vesting"): + continue + if isinstance(chain_data, dict): + tvl_history = chain_data.get("tvl", []) + if tvl_history: + chain_summary[chain_name] = tvl_history[-1].get("totalLiquidityUSD", 0) + elif isinstance(chain_data, (int, float)): + chain_summary[chain_name] = chain_data + + if chain_summary: + sorted_chains = sorted(chain_summary.items(), key=lambda x: x[1], reverse=True) + lines.append("Chain Breakdown:") + for chain_name, chain_tvl in sorted_chains[:10]: + lines.append(f" {chain_name}: {_fmt_usd(chain_tvl)}") + + lines.append("---") + return "\n".join(lines) + + except httpx.HTTPError as e: + msg = f"DeFiLlama API error for '{protocol}': {e}" + logger.error(msg) + return msg + except Exception as e: + msg = f"DeFiLlama TVL query failed for '{protocol}': {e}" + logger.error(msg) + return msg + + def get_token_unlocks(self, protocol: str) -> str: + """ + Get token unlock/vesting schedule for a protocol. + + Args: + protocol: Protocol name or slug. + + Returns: + Formatted token unlock schedule string. + """ + slug = self._resolve_slug(protocol) + if slug is None: + return f"Protocol '{protocol}' not found on DeFiLlama." + + try: + resp = self._client.get(f"{DEFILLAMA_API}/api/emission/{slug}") + if resp.status_code == 404: + return f"No token unlock data for '{protocol}' on DeFiLlama." + resp.raise_for_status() + data = resp.json() + + name = data.get("name", slug) + token_price = data.get("tokenPrice", {}) + categories = data.get("categories", {}) + events = data.get("events", []) + + lines = [f"--- {name} Token Unlock Schedule ---"] + + # Token price info + if isinstance(token_price, dict): + price = token_price.get("price") + symbol = token_price.get("symbol", "") + if price: + lines.append(f"Token: {symbol.upper()} @ ${price:,.4f}") + + # Emission categories + if categories: + lines.append("Allocation Categories:") + for cat_name, cat_data in categories.items(): + if isinstance(cat_data, dict): + pct = cat_data.get("percentage") + if pct is not None: + lines.append(f" {cat_name}: {pct:.1f}%") + else: + lines.append(f" {cat_name}") + + # Upcoming events + if events: + lines.append("Upcoming Unlock Events:") + shown = 0 + for event in events[:10]: + desc = event.get("description", "Unlock") + date = event.get("date", "TBD") + amount = event.get("noOfTokens") + if amount: + lines.append(f" {date}: {desc} ({amount:,.0f} tokens)") + else: + lines.append(f" {date}: {desc}") + shown += 1 + if len(events) > 10: + lines.append(f" ... and {len(events) - 10} more events") + + if len(lines) == 1: + lines.append("No detailed unlock data available.") + + lines.append("---") + return "\n".join(lines) + + except httpx.HTTPError as e: + msg = f"DeFiLlama API error for '{protocol}' unlocks: {e}" + logger.error(msg) + return msg + except Exception as e: + msg = f"DeFiLlama unlock query failed for '{protocol}': {e}" + logger.error(msg) + return msg + + def get_protocol_revenue(self, protocol: str) -> str: + """ + Get protocol fees and revenue data. + + Args: + protocol: Protocol name or slug. + + Returns: + Formatted fees/revenue report string. + """ + slug = self._resolve_slug(protocol) + if slug is None: + return f"Protocol '{protocol}' not found on DeFiLlama." + + try: + resp = self._client.get(f"{DEFILLAMA_API}/summary/fees/{slug}") + if resp.status_code == 404: + return f"No fee/revenue data for '{protocol}' on DeFiLlama." + resp.raise_for_status() + data = resp.json() + + name = data.get("name", slug) + category = data.get("category", "N/A") + + total_24h = data.get("total24h") + total_7d = data.get("total7d") + total_30d = data.get("total30d") + total_all_time = data.get("totalAllTime") + revenue_24h = data.get("revenue24h") + revenue_7d = data.get("revenue7d") + revenue_30d = data.get("revenue30d") + + lines = [ + f"--- {name} Fees & Revenue ---", + f"Category: {category}", + ] + + # Fees + lines.append("Fees:") + if total_24h is not None: + lines.append(f" 24h Fees: {_fmt_usd(total_24h)}") + if total_7d is not None: + lines.append(f" 7d Fees: {_fmt_usd(total_7d)}") + if total_30d is not None: + lines.append(f" 30d Fees: {_fmt_usd(total_30d)}") + if total_all_time is not None: + lines.append(f" All-Time Fees: {_fmt_usd(total_all_time)}") + + # Revenue (protocol revenue, subset of fees) + has_revenue = any(v is not None for v in [revenue_24h, revenue_7d, revenue_30d]) + if has_revenue: + lines.append("Revenue (protocol share):") + if revenue_24h is not None: + lines.append(f" 24h Revenue: {_fmt_usd(revenue_24h)}") + if revenue_7d is not None: + lines.append(f" 7d Revenue: {_fmt_usd(revenue_7d)}") + if revenue_30d is not None: + lines.append(f" 30d Revenue: {_fmt_usd(revenue_30d)}") + + # Chain breakdown if available + chain_data = data.get("totalDataChartBreakdown") + if not chain_data and data.get("chains"): + lines.append(f"Available on chains: {', '.join(data['chains'][:15])}") + + lines.append("---") + return "\n".join(lines) + + except httpx.HTTPError as e: + msg = f"DeFiLlama API error for '{protocol}' revenue: {e}" + logger.error(msg) + return msg + except Exception as e: + msg = f"DeFiLlama revenue query failed for '{protocol}': {e}" + logger.error(msg) + return msg \ No newline at end of file diff --git a/src/services/etherscan.py b/src/services/etherscan.py new file mode 100644 index 0000000..afb579d --- /dev/null +++ b/src/services/etherscan.py @@ -0,0 +1,319 @@ +"""Etherscan API service for Ethereum on-chain data (wallet balances, token transfers, contracts).""" +import logging +from datetime import datetime, timezone +from typing import Optional + +import httpx +from src.utils.http import get_client + +logger = logging.getLogger(__name__) + +ETHERSCAN_API_V2 = "https://api.etherscan.io/v2/api" + +# Default chain: Polygon (137) where Polymarket operates. +# Ethereum mainnet = 1, can be overridden per-call. +DEFAULT_CHAIN_ID = 137 + +# Well-known ERC-20 token contracts on Polygon +TOKEN_CONTRACTS = { + "USDC": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", # USDC native on Polygon + "USDC.e": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", # USDC.e (bridged) on Polygon + "USDT": "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", + "WETH": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", + "DAI": "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063", +} + +# Decimals per token (used for converting raw amounts) +TOKEN_DECIMALS = { + "USDC": 6, + "USDC.e": 6, + "USDT": 6, + "WETH": 18, + "DAI": 18, +} + + +def _format_amount(raw_value: str, decimals: int) -> float: + """Convert a raw token amount string to a human-readable float.""" + try: + return int(raw_value) / (10 ** decimals) + except (ValueError, TypeError): + return 0.0 + + +def _short_address(address: str) -> str: + """Shorten an Ethereum address for display.""" + if len(address) >= 10: + return f"{address[:6]}...{address[-4:]}" + return address + + +def _ts_to_str(timestamp: str) -> str: + """Convert a unix timestamp string to a readable UTC datetime.""" + try: + dt = datetime.fromtimestamp(int(timestamp), tz=timezone.utc) + return dt.strftime("%Y-%m-%d %H:%M UTC") + except (ValueError, TypeError): + return timestamp + + +class EtherscanService: + """ + Etherscan API client for Ethereum on-chain data. + + Note: Free tier is limited to 5 calls/sec. Add delays between rapid + successive calls if needed. + """ + + def __init__(self, api_key: str = ""): + self.api_key = api_key + self._client = get_client(timeout=20.0) + + def is_available(self) -> bool: + return bool(self.api_key and self.api_key.strip()) + + def _get(self, params: dict, chain_id: int = DEFAULT_CHAIN_ID) -> dict: + """Make authenticated GET request to Etherscan V2 API.""" + params["apikey"] = self.api_key + params["chainid"] = chain_id + resp = self._client.get(ETHERSCAN_API_V2, params=params) + resp.raise_for_status() + return resp.json() + + # ------------------------------------------------------------------ + # 1. Token transfers + # ------------------------------------------------------------------ + + def get_wallet_token_transfers(self, address: str, token: str = "USDC") -> str: + """ + Get recent ERC-20 token transfers for a wallet. + + Args: + address: Ethereum wallet address. + token: Token symbol to filter on (USDC, USDT, etc.). + Pass "ALL" to show all ERC-20 transfers. + + Returns: + Formatted transfer report string for LLM consumption. + """ + addr = address.strip().lower() + token_upper = token.strip().upper() + + try: + params = { + "module": "account", + "action": "tokentx", + "address": addr, + "sort": "desc", + "page": "1", + "offset": "20", + } + + data = self._get(params) + + if data.get("status") != "1" or not data.get("result"): + message = data.get("message", "No transfers found") + return f"No ERC-20 token transfers found for {_short_address(addr)}: {message}" + + transfers = data["result"] + + # Filter by token if not "ALL" + if token_upper != "ALL": + # For USDC, match both native and bridged (USDC.e) contracts + if token_upper == "USDC": + allowed = { + TOKEN_CONTRACTS.get("USDC", "").lower(), + TOKEN_CONTRACTS.get("USDC.e", "").lower(), + } + allowed.discard("") + transfers = [ + tx for tx in transfers + if tx.get("contractAddress", "").lower() in allowed + ] + else: + contract = TOKEN_CONTRACTS.get(token_upper, "").lower() + if contract: + transfers = [ + tx for tx in transfers + if tx.get("contractAddress", "").lower() == contract + ] + else: + # Try matching by symbol in the response + transfers = [ + tx for tx in transfers + if tx.get("tokenSymbol", "").upper() == token_upper + ] + + if not transfers: + return f"No {token_upper} transfers found for {_short_address(addr)} in the last 20 token transactions." + + lines = [f"--- Token Transfers for {_short_address(addr)} ({token_upper}) ---"] + + for tx in transfers: + tx_from = tx.get("from", "").lower() + tx_to = tx.get("to", "").lower() + symbol = tx.get("tokenSymbol", "???") + decimals = int(tx.get("tokenDecimal", TOKEN_DECIMALS.get(symbol.upper(), 18))) + raw_value = tx.get("value", "0") + amount = _format_amount(raw_value, decimals) + ts = _ts_to_str(tx.get("timeStamp", "")) + tx_hash = tx.get("hash", "") + + # Determine direction + if tx_from == addr: + direction = "OUT" + counterparty = _short_address(tx_to) + elif tx_to == addr: + direction = "IN" + counterparty = _short_address(tx_from) + else: + direction = "???" + counterparty = f"{_short_address(tx_from)} -> {_short_address(tx_to)}" + + # Flag large transfers + large_flag = "" + if symbol.upper() in ("USDC", "USDT", "DAI") and amount > 10_000: + large_flag = " [LARGE]" + elif symbol.upper() == "WETH" and amount > 5: + large_flag = " [LARGE]" + + lines.append( + f" {direction} {amount:,.2f} {symbol}{large_flag} | " + f"{'to' if direction == 'OUT' else 'from'}: {counterparty} | {ts}" + ) + + lines.append("---") + return "\n".join(lines) + + except httpx.HTTPError as e: + msg = f"Etherscan API error fetching token transfers for '{address}': {e}" + logger.error(msg) + return msg + except Exception as e: + msg = f"Etherscan token transfer query failed for '{address}': {e}" + logger.error(msg) + return msg + + # ------------------------------------------------------------------ + # 2. Contract info + # ------------------------------------------------------------------ + + def get_contract_info(self, address: str) -> str: + """ + Check if an address is a smart contract and retrieve basic contract metadata. + + Args: + address: Ethereum address to inspect. + + Returns: + Formatted contract info string for LLM consumption. + """ + addr = address.strip() + + try: + # First check if ABI is available (verified contract) + abi_data = self._get({ + "module": "contract", + "action": "getabi", + "address": addr, + }) + + is_verified = abi_data.get("status") == "1" + + # Get source code info (includes contract name, compiler, etc.) + source_data = self._get({ + "module": "contract", + "action": "getsourcecode", + "address": addr, + }) + + results = source_data.get("result", []) + + lines = [f"--- Contract Info for {_short_address(addr)} ---"] + + if not results or (isinstance(results, list) and len(results) == 0): + lines.append("No contract data returned. Address may be an EOA (externally owned account).") + lines.append("---") + return "\n".join(lines) + + info = results[0] if isinstance(results, list) else results + + contract_name = info.get("ContractName", "") + compiler = info.get("CompilerVersion", "") + optimization = info.get("OptimizationUsed", "") + proxy = info.get("Proxy", "0") + implementation = info.get("Implementation", "") + + if not contract_name: + lines.append("This address does not appear to be a verified contract.") + lines.append("It may be an EOA (regular wallet) or an unverified contract.") + else: + lines.append(f"Contract Name: {contract_name}") + lines.append(f"Verified: {'Yes' if is_verified else 'No'}") + if compiler: + lines.append(f"Compiler: {compiler}") + if optimization: + lines.append(f"Optimization: {'Yes' if optimization == '1' else 'No'}") + if proxy == "1": + lines.append(f"Proxy Contract: Yes") + if implementation: + lines.append(f"Implementation: {implementation}") + + lines.append("---") + return "\n".join(lines) + + except httpx.HTTPError as e: + msg = f"Etherscan API error fetching contract info for '{address}': {e}" + logger.error(msg) + return msg + except Exception as e: + msg = f"Etherscan contract query failed for '{address}': {e}" + logger.error(msg) + return msg + + # ------------------------------------------------------------------ + # 3. ETH balance + # ------------------------------------------------------------------ + + def get_wallet_eth_balance(self, address: str) -> str: + """ + Get ETH balance for a wallet address. + + Args: + address: Ethereum wallet address. + + Returns: + Formatted ETH balance string for LLM consumption. + """ + addr = address.strip() + + try: + data = self._get({ + "module": "account", + "action": "balance", + "address": addr, + "tag": "latest", + }) + + if data.get("status") != "1": + message = data.get("message", "Unknown error") + return f"Could not fetch ETH balance for {_short_address(addr)}: {message}" + + raw_balance = data.get("result", "0") + eth_balance = _format_amount(raw_balance, 18) + + lines = [ + f"--- ETH Balance for {_short_address(addr)} ---", + f"Balance: {eth_balance:,.6f} ETH", + "---", + ] + return "\n".join(lines) + + except httpx.HTTPError as e: + msg = f"Etherscan API error fetching ETH balance for '{address}': {e}" + logger.error(msg) + return msg + except Exception as e: + msg = f"Etherscan balance query failed for '{address}': {e}" + logger.error(msg) + return msg \ No newline at end of file diff --git a/src/services/fred.py b/src/services/fred.py new file mode 100644 index 0000000..4b8e16b --- /dev/null +++ b/src/services/fred.py @@ -0,0 +1,180 @@ +"""FRED (Federal Reserve Economic Data) API service for macroeconomic indicators.""" +import logging +from typing import Optional + +import httpx +from src.utils.http import get_client + +logger = logging.getLogger(__name__) + +FRED_API = "https://api.stlouisfed.org/fred" + +# Common series IDs for prediction market analysis +SERIES_MAP = { + # Interest rates + "fed_funds_rate": "FEDFUNDS", + "fed_rate": "FEDFUNDS", + "interest_rate": "FEDFUNDS", + "10y_treasury": "DGS10", + "2y_treasury": "DGS2", + "30y_mortgage": "MORTGAGE30US", + # Inflation + "cpi": "CPIAUCSL", + "core_cpi": "CPILFESL", + "pce": "PCEPI", + "core_pce": "PCEPILFE", + "inflation": "CPIAUCSL", + # Employment + "unemployment": "UNRATE", + "unemployment_rate": "UNRATE", + "nonfarm_payrolls": "PAYEMS", + "jobs": "PAYEMS", + "initial_claims": "ICSA", + "jobless_claims": "ICSA", + # GDP + "gdp": "GDP", + "real_gdp": "GDPC1", + "gdp_growth": "A191RL1Q225SBEA", + # Markets / Financial conditions + "sp500": "SP500", + "vix": "VIXCLS", + "yield_curve": "T10Y2Y", + "financial_stress": "STLFSI2", + # Dollar + "dollar_index": "DTWEXBGS", + "usd": "DTWEXBGS", + # Oil / Commodities + "oil_price": "DCOILWTICO", + "wti": "DCOILWTICO", + "crude_oil": "DCOILWTICO", + "brent": "DCOILBRENTEU", + "gas_price": "GASREGW", + "gold": "GOLDAMGBD228NLBM", +} + + +def _resolve_series_id(query: str) -> str: + """Resolve a common name to a FRED series ID.""" + q = query.strip().lower().replace(" ", "_") + if q in SERIES_MAP: + return SERIES_MAP[q] + # If already looks like a FRED series ID (uppercase), use as-is + return query.strip().upper() + + +class FREDService: + """ + FRED API client for macroeconomic data. + + Free, unlimited usage with API key. + """ + + def __init__(self, api_key: str): + self.api_key = api_key + self._client = get_client(timeout=15.0) + + def is_available(self) -> bool: + return bool(self.api_key and self.api_key.strip()) + + def get_series(self, query: str, num_observations: int = 10) -> str: + """ + Get recent observations for an economic data series. + + Args: + query: Common name (e.g. 'fed_rate', 'cpi', 'unemployment', 'oil_price') + or a FRED series ID (e.g. 'FEDFUNDS', 'UNRATE') + num_observations: Number of recent data points to return + + Returns: + Formatted report with series info and recent values. + """ + series_id = _resolve_series_id(query) + + try: + # Get series metadata + meta_resp = self._client.get( + f"{FRED_API}/series", + params={ + "series_id": series_id, + "api_key": self.api_key, + "file_type": "json", + }, + ) + + if meta_resp.status_code == 400: + return ( + f"Series '{query}' (id: {series_id}) not found on FRED. " + f"Common names: fed_rate, cpi, unemployment, gdp, oil_price, " + f"vix, yield_curve, gold, sp500, jobless_claims" + ) + meta_resp.raise_for_status() + meta = meta_resp.json().get("seriess", [{}])[0] + + title = meta.get("title", series_id) + frequency = meta.get("frequency", "") + units = meta.get("units", "") + last_updated = meta.get("last_updated", "") + + # Get recent observations + obs_resp = self._client.get( + f"{FRED_API}/series/observations", + params={ + "series_id": series_id, + "api_key": self.api_key, + "file_type": "json", + "sort_order": "desc", + "limit": num_observations, + }, + ) + obs_resp.raise_for_status() + observations = obs_resp.json().get("observations", []) + + lines = [ + f"--- FRED: {title} ({series_id}) ---", + f"Units: {units}", + f"Frequency: {frequency}", + f"Last Updated: {last_updated}", + "", + "Recent Data:", + ] + + for obs in reversed(observations): + date = obs.get("date", "") + value = obs.get("value", ".") + if value == ".": + lines.append(f" {date}: N/A") + else: + try: + v = float(value) + lines.append(f" {date}: {v:,.2f}") + except ValueError: + lines.append(f" {date}: {value}") + + # Add trend info if enough data + valid_vals = [] + for obs in observations: + v = obs.get("value", ".") + if v != ".": + try: + valid_vals.append(float(v)) + except ValueError: + pass + + if len(valid_vals) >= 2: + latest = valid_vals[0] + prev = valid_vals[1] + change = latest - prev + pct = (change / abs(prev) * 100) if prev != 0 else 0 + lines.append(f"\nLatest vs Previous: {change:+.2f} ({pct:+.2f}%)") + + lines.append("---") + return "\n".join(lines) + + except httpx.HTTPError as e: + msg = f"FRED API error for '{query}' ({series_id}): {e}" + logger.error(msg) + return msg + except Exception as e: + msg = f"FRED query failed for '{query}': {e}" + logger.error(msg) + return msg \ No newline at end of file diff --git a/src/services/llm_analyzer.py b/src/services/llm_analyzer.py new file mode 100644 index 0000000..29ce58c --- /dev/null +++ b/src/services/llm_analyzer.py @@ -0,0 +1,466 @@ +""" +LLM analyzer service - analyzes whale trades using AI with tool-use. + +Architecture: +1. Build context (trade info + historical signals) +2. Send to LLM with tool schemas (search_twitter, search_web, etc.) +3. LLM decides which tools to call (if any) +4. Execute tool calls, return results to LLM +5. LLM produces final analysis + JSON decision + +The LLM controls which information sources to query based on the market type. +""" +import json +import logging +import re +from datetime import datetime +from typing import Optional + +from openai import OpenAI + +from src.config import get_settings +from src.models.trade import WhaleTrade +from src.models.decision import LLMDecision, TradeRecommendation, TradeAction, TraderCredibility +from src.models.anomaly_signal import AnomalySignal +from src.services.anomaly_detector import AnomalyDetector +from src.services.anomaly_history import AnomalyHistoryService +from src.services.tools import ToolRegistry +from src.prompts.whale_analyzer import WhaleAnalyzerPrompts + +logger = logging.getLogger(__name__) + +# Maximum tool-use rounds to prevent infinite loops +# 14 tools available; LLM can call multiple per round but may need +# several rounds for chain-of-investigation (search → discover → verify) +MAX_TOOL_ROUNDS = 3 + + +class LLMAnalyzer: + """Analyzes whale trades using LLM with function-calling tools.""" + + def __init__(self): + self.settings = get_settings() + + self.client = OpenAI( + base_url=self.settings.llm_base_url, + api_key=self.settings.llm_api_key, + ) + + self.anomaly_detector = AnomalyDetector() + self.prompts = WhaleAnalyzerPrompts() + self.anomaly_history = AnomalyHistoryService(self.settings.db_path) + + # Tool registry — LLM decides which tools to call + self.tool_registry = ToolRegistry( + twitter_api_key=self.settings.twitter_api_key, + tavily_api_key=self.settings.tavily_api_key, + fred_api_key=self.settings.fred_api_key, + polygon_api_key=self.settings.polygon_api_key, + congress_api_key=self.settings.congress_api_key, + etherscan_api_key=self.settings.etherscan_api_key, + serper_api_key=self.settings.serper_api_key, + telegram_api_id=self.settings.telegram_api_id, + telegram_api_hash=self.settings.telegram_api_hash, + telegram_session_string=self.settings.telegram_session_string, + telegram_channels=self.settings.telegram_channels, + ) + + self._last_historical_signal_count = 0 + + @property + def last_historical_signal_count(self) -> int: + return self._last_historical_signal_count + + # ================================================================ + # Response parsing + # ================================================================ + + # Fields that identify the final recommendation JSON (vs intermediate tool-call JSONs) + _RECOMMENDATION_FIELDS = {"information_asymmetry_score", "confidence", "trader_credibility"} + + def _extract_json_from_response(self, response: str) -> Optional[dict]: + """Extract the final recommendation JSON from LLM response text. + + When the response contains multiple JSON code blocks (e.g. an + intermediate ANALYZE decision followed by the real assessment), + prefer the block that contains recommendation-specific fields. + Falls back to the last parseable block. + """ + candidates: list[dict] = [] + for match in re.findall(r"```(?:json)?\s*([\s\S]*?)```", response): + try: + candidates.append(json.loads(match.strip())) + except json.JSONDecodeError: + continue + + if candidates: + # Prefer the block that looks like a final recommendation + for c in reversed(candidates): + if c.keys() & self._RECOMMENDATION_FIELDS: + return c + # No block has recommendation fields — return the last one + return candidates[-1] + + # Try raw JSON + try: + 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 into TradeRecommendation.""" + action_str = json_data.get("action", "HOLD").upper() + try: + action = TradeAction(action_str) + except ValueError: + action = TradeAction.HOLD + + confidence = max(0.0, min(1.0, float(json_data.get("confidence", 0.0)))) + + suggested_price = json_data.get("suggested_price") + if suggested_price is not None: + suggested_price = float(suggested_price) + + suggested_size = max(0.0, min(1.0, float(json_data.get("suggested_size_percent", 0.1)))) + + insider_likelihood = max(0.0, min(1.0, float(json_data.get("information_asymmetry_score", 0.0)))) + + credibility_str = json_data.get("trader_credibility", "UNKNOWN").upper() + try: + trader_credibility = TraderCredibility(credibility_str) + except ValueError: + trader_credibility = TraderCredibility.UNKNOWN + + 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", "")), + information_asymmetry_score=insider_likelihood, + trader_credibility=trader_credibility, + insider_evidence=str(json_data.get("insider_evidence", "")), + ) + + # ================================================================ + # Anomaly signal storage + # ================================================================ + + def _store_anomaly_signal_if_qualified( + self, + whale_trade: WhaleTrade, + decision: LLMDecision, + ) -> None: + """Store anomaly signal if information asymmetry score qualifies.""" + rec = decision.recommendation + + if not self.anomaly_history.should_store_signal(rec.information_asymmetry_score): + logger.debug( + f"Signal not stored: IAS {rec.information_asymmetry_score:.2f} " + f"below threshold" + ) + return + + signal = AnomalySignal( + id=whale_trade.id, + market_id=whale_trade.market_id, + market_question=whale_trade.market_question, + market_slug=whale_trade.trade.slug, + condition_id=whale_trade.trade.condition_id, + transaction_hash=whale_trade.trade.transaction_hash, + trade_timestamp=whale_trade.trade.timestamp, + trade_side=whale_trade.trade.side, + trade_price=whale_trade.trade.price, + trade_size_usd=whale_trade.trade.usdc_size, + trade_outcome=whale_trade.trade.outcome, + trader_wallet=whale_trade.trade.proxy_wallet, + trader_ranking=whale_trade.trader_ranking, + trader_history=whale_trade.trader_history, + information_asymmetry_score=rec.information_asymmetry_score, + reasoning=rec.reasoning, + insider_evidence=rec.insider_evidence, + detected_at=whale_trade.detected_at, + ) + + stored = self.anomaly_history.store_signal(signal) + if stored: + logger.info( + f"Stored anomaly signal: {whale_trade.market_question[:50]}... " + f"IAS={rec.information_asymmetry_score:.0%}" + ) + + # ================================================================ + # Tool-use loop + # ================================================================ + + def _execute_tool_calls(self, tool_calls) -> list[dict]: + """Execute tool calls from the LLM and return message dicts.""" + results = [] + for tc in tool_calls: + fn_name = tc.function.name + try: + fn_args = json.loads(tc.function.arguments) + except json.JSONDecodeError: + fn_args = {} + + logger.info(f"LLM requested tool: {fn_name}({fn_args})") + output = self.tool_registry.call(fn_name, **fn_args) + + results.append({ + "role": "tool", + "tool_call_id": tc.id, + "content": output, + }) + return results + + async def analyze_whale_trade(self, whale_trade: WhaleTrade) -> LLMDecision: + """ + Analyze a whale trade using LLM with tool-use. + + Flow: + 0. Pre-screening: lightweight check if signal is worth full analysis + 1. Build initial context (trade + historical signals) + 2. Send to LLM with available tool schemas + 3. If LLM requests tools → execute → return results → repeat (up to MAX_TOOL_ROUNDS) + 4. Parse final text response for JSON decision + """ + # Build context + trade_context = self.anomaly_detector.format_for_llm(whale_trade) + + historical_context = "" + historical_signals = self.anomaly_history.get_signals_for_market( + whale_trade.market_id, top_recent=5, top_likelihood=5, + ) + self._last_historical_signal_count = len(historical_signals) + if historical_signals: + historical_context = self.anomaly_history.format_historical_signals_context(historical_signals) + logger.info(f"Found {len(historical_signals)} historical anomaly signals for market") + + # Build initial messages + system_prompt = self.prompts.system_prompt() + user_prompt = self.prompts.analyze_whale_trade(trade_context, historical_context) + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ] + + # Tool schemas (empty list if no tools available) + tool_schemas = self.tool_registry.openai_tool_schemas() + + try: + analysis_text = "" + + # Tool-use loop + for round_idx in range(MAX_TOOL_ROUNDS + 1): + # Last round: no tools, force final answer + is_last_round = (round_idx == MAX_TOOL_ROUNDS) + if is_last_round: + messages.append({ + "role": "user", + "content": ( + "You have used all available tool rounds. " + "Based on all information gathered, provide your final " + "analysis and output the JSON assessment now." + ), + }) + + # Call LLM + call_kwargs = { + "model": self.settings.llm_model, + "messages": messages, + "max_tokens": 8192, + } + if tool_schemas and not is_last_round: + call_kwargs["tools"] = tool_schemas + call_kwargs["tool_choice"] = "auto" + + response = self.client.chat.completions.create(**call_kwargs) + msg = response.choices[0].message + + # If LLM wants to call tools + if msg.tool_calls: + logger.info( + f"Round {round_idx + 1}: LLM requested " + f"{len(msg.tool_calls)} tool call(s)" + ) + + # Append assistant message with tool calls + messages.append(msg.model_dump()) + + # Execute tools and append results + tool_results = self._execute_tool_calls(msg.tool_calls) + messages.extend(tool_results) + + continue # Next round — LLM processes tool results + + # No tool calls — final response + analysis_text = msg.content or "" + finish_reason = response.choices[0].finish_reason + logger.info( + f"Analysis complete after {round_idx + 1} round(s) " + f"({len(analysis_text)} chars, finish={finish_reason})" + ) + if len(analysis_text) < 200: + logger.warning(f"Suspiciously short response: {analysis_text[:200]}") + break + + # Parse JSON decision from final response + json_data = self._extract_json_from_response(analysis_text) + + if json_data: + # Check if LLM decided to skip (pre-screening in prompt) + if json_data.get("action") == "SKIP": + reason = json_data.get("reason", "not in scope") + logger.info( + f"⏭️ Pre-screening SKIP: {reason} " + f"(market: {whale_trade.market_question[:40]}...)" + ) + return LLMDecision( + whale_trade_id=whale_trade.id, + market_id=whale_trade.market_id, + analysis=f"Pre-screening: {reason}", + recommendation=TradeRecommendation( + action=TradeAction.HOLD, + outcome="", + confidence=0.0, + reasoning=f"Signal filtered: {reason}", + ), + ) + + recommendation = self._parse_recommendation(json_data) + else: + 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", + ) + + decision = LLMDecision( + whale_trade_id=whale_trade.id, + market_id=whale_trade.market_id, + analysis=analysis_text, + recommendation=recommendation, + ) + + self._store_anomaly_signal_if_qualified(whale_trade, decision) + return decision + + except Exception as e: + logger.error(f"Error in LLM analysis: {e}") + 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)}", + ), + ) + + # ================================================================ + # Report formatting (unchanged) + # ================================================================ + + def format_full_report( + self, + whale_trade: WhaleTrade, + decision: LLMDecision, + historical_signal_count: int = 0, + ) -> str: + """Format a complete analysis report.""" + trade = whale_trade.trade + rec = decision.recommendation + + 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_indicator = { + TradeAction.BUY: "🟢 BUY", + TradeAction.SELL: "🔴 SELL", + TradeAction.HOLD: "⚪ HOLD", + } + + ias = rec.information_asymmetry_score + if ias >= 0.7: + insider_indicator = f"High Information Asymmetry ({ias:.0%})" + elif ias >= 0.4: + insider_indicator = f"Medium Information Asymmetry ({ias:.0%})" + else: + insider_indicator = f"Low Information Asymmetry ({ias:.0%})" + + rank_num = whale_trade.trader_ranking.rank if whale_trade.trader_ranking and whale_trade.trader_ranking.rank else None + credibility_indicators = { + TraderCredibility.HIGH: f"High Credibility (#{rank_num})" if rank_num else "High Credibility", + TraderCredibility.MEDIUM: f"Medium Credibility (#{rank_num})" if rank_num else "Medium Credibility", + TraderCredibility.LOW: f"Low Credibility (#{rank_num})" if rank_num else "Low Credibility", + TraderCredibility.UNKNOWN: "Unknown (Unranked)", + } + credibility_str = credibility_indicators.get(rec.trader_credibility, "Unknown") + + trader_ranking_str = "" + if whale_trade.trader_ranking: + tr = whale_trade.trader_ranking + rank_str = f"#{tr.rank}" if tr.rank else "Unranked" + pnl_str = f"${tr.pnl:,.2f}" if tr.pnl else "N/A" + trader_ranking_str = f"| **Trader Rank** | {rank_str} (PnL: {pnl_str}) |" + + historical_info = "" + if historical_signal_count > 0: + historical_info = f"\n**Historical Anomaly Signals Referenced**: {historical_signal_count} (analyzed together)" + + report = f""" +{'='*70} +# Whale Trade Analysis Report +{'='*70} + +**Generated at**: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC{historical_info} + +## Trade Summary + +| Field | Details | +|-------|---------| +| **Market** | {whale_trade.market_question} | +| **Trade Size** | ${trade.usdc_size:,.2f} USDC | +| **Direction** | BUY {trade.outcome} Token ({'Bullish' if trade.outcome == 'Yes' else 'Bearish'}) | +| **Trade Price** | {trade.price:.4f} ({trade.price:.1%}) | +| **Current Odds** | {prices_str} | +| **Trade Time** | {datetime.fromtimestamp(trade.timestamp).strftime('%Y-%m-%d %H:%M:%S') if trade.timestamp else 'N/A'} | +{trader_ranking_str} + +{'='*70} + +{decision.analysis} + +{'='*70} +## Information Asymmetry Assessment +{'='*70} + +| Field | Assessment | +|-------|------------| +| **Information Asymmetry** | {insider_indicator} | +| **Trader Credibility** | {credibility_str} | + +**Key Evidence**: {rec.insider_evidence or 'No clear evidence'} + +**Reasoning**: {rec.reasoning} + +{'='*70} +Disclaimer: This report is AI-generated for informational purposes only and does not constitute investment advice. +{'='*70} +""" + return report \ No newline at end of file diff --git a/src/services/market_fetcher.py b/src/services/market_fetcher.py new file mode 100644 index 0000000..9041fc9 --- /dev/null +++ b/src/services/market_fetcher.py @@ -0,0 +1,531 @@ +"""Market fetching service - fetches trending markets from Polymarket.""" +import json +import logging +from typing import List, Optional + +import httpx + +from src.config import get_settings +from src.models.market import Market, TrendingMarket +from src.utils.http import get_client + +logger = logging.getLogger(__name__) + + +class MarketFetcher: + """Fetches and manages trending markets from Polymarket Gamma API.""" + + # Short-term price prediction markets to filter out (no signal value) + # Matches patterns like "Bitcoin Up or Down - March 27, 2:00AM-2:15AM ET" + SHORT_TERM_PRICE_KEYWORDS = [ + "up or down", # "Bitcoin Up or Down - March 27, 2:00AM" + "higher or lower", # price higher or lower + "above or below", # close above or below + "opens up or down", # "S&P 500 Opens Up or Down" + "green or red", # daily candle color + ] + + # Temperature/weather markets (no signal value) + WEATHER_KEYWORDS = [ + "highest temperature", + "lowest temperature", + "temperature in", + "weather", + "rainfall", + "°f on", + "°c on", + ] + + # Sports-related keywords to filter out + SPORTS_KEYWORDS = [ + "nba", "nfl", "mlb", "nhl", "mls", "ufc", "wwe", "pga", "atp", "wta", + "fifa", "uefa", "epl", "premier league", "la liga", "serie a", "bundesliga", + "champions league", "world cup", "olympics", "olympic", + "basketball", "football", "soccer", "baseball", "hockey", "tennis", + "golf", "boxing", "mma", "wrestling", "cricket", "rugby", "f1", "formula 1", + "nascar", "racing", "motorsport", + "game", "match", "vs", "versus", "playoff", "playoffs", "finals", + "championship", "tournament", "season", "super bowl", "world series", + "score", "points", "goals", "touchdowns", "wins", "win against", + "beat", "defeat", + "mvp", "rookie", "all-star", "draft", "trade", + "lakers", "celtics", "warriors", "bulls", "heat", "knicks", + "yankees", "dodgers", "red sox", "cubs", "mets", + "cowboys", "patriots", "chiefs", "eagles", "49ers", + "manchester", "barcelona", "real madrid", "liverpool", "chelsea", + ] + + def __init__(self): + self.settings = get_settings() + self.gamma_url = "https://gamma-api.polymarket.com" + self.markets_endpoint = f"{self.gamma_url}/markets" + self.events_endpoint = f"{self.gamma_url}/events" + self._client = get_client(timeout=30.0) + + def __del__(self): + """Cleanup HTTP client.""" + if hasattr(self, "_client"): + self._client.close() + + def _should_filter_market(self, market_data: dict) -> str: + """ + Check if a market should be filtered out. + + Returns: + Filter reason string if should be filtered, empty string if OK. + """ + question = (market_data.get("question") or "").lower() + description = (market_data.get("description") or "").lower() + slug = (market_data.get("slug") or "").lower() + text = f"{question} {description} {slug}" + + for keyword in self.SPORTS_KEYWORDS: + if keyword in text: + return "sports" + + for keyword in self.SHORT_TERM_PRICE_KEYWORDS: + if keyword in text: + return "short_term_price" + + for keyword in self.WEATHER_KEYWORDS: + if keyword in text: + return "weather" + + return "" + + def _is_sports_market(self, market_data: dict) -> bool: + """Legacy compatibility.""" + return bool(self._should_filter_market(market_data)) + + def _parse_market(self, data: dict) -> Optional[Market]: + """Parse raw market data into Market model.""" + try: + # Parse outcome prices (comes as stringified list) + outcome_prices = data.get("outcomePrices", []) + if isinstance(outcome_prices, str): + outcome_prices = json.loads(outcome_prices) + outcome_prices = [float(p) for p in outcome_prices] + + # Parse clob token IDs + clob_token_ids = data.get("clobTokenIds", []) + if isinstance(clob_token_ids, str): + clob_token_ids = json.loads(clob_token_ids) + + # Parse outcomes + outcomes = data.get("outcomes", []) + if isinstance(outcomes, str): + outcomes = json.loads(outcomes) + + return Market( + id=str(data.get("id", "")), + question=data.get("question", ""), + condition_id=data.get("conditionId"), + slug=data.get("slug"), + description=data.get("description"), + end_date=data.get("endDate"), + outcomes=outcomes, + outcome_prices=outcome_prices, + clob_token_ids=clob_token_ids, + volume=float(data.get("volume", 0) or 0), + volume_24hr=float(data.get("volume24hr", 0) or 0), + liquidity=float(data.get("liquidity", 0) or 0), + active=data.get("active", False), + closed=data.get("closed", False), + neg_risk=data.get("negRisk", False), + ) + except Exception as e: + logger.warning(f"Failed to parse market {data.get('id')}: {e}") + return None + + def get_trending_markets(self, limit: Optional[int] = None) -> List[TrendingMarket]: + """ + Fetch trending markets sorted by 24-hour volume. + + Filters out sports-related markets since they lack fundamental analysis value. + + Args: + limit: Maximum number of non-sports markets to return (defaults to settings) + + Returns: + List of TrendingMarket objects (excluding sports markets) + """ + limit = limit or self.settings.trending_markets_limit + trending_markets = [] + offset = 0 + batch_size = 100 # Fetch more to account for sports filtering + max_iterations = 10 # Safety limit to prevent infinite loops + + try: + iteration = 0 + while len(trending_markets) < limit and iteration < max_iterations: + iteration += 1 + + # Fetch active markets sorted by volume + params = { + "active": True, + "closed": False, + "archived": False, + "limit": batch_size, + "offset": offset, + "order": "volume24hr", + "ascending": False, + "enableOrderBook": True, # Only markets with CLOB enabled + } + + response = self._client.get(self.markets_endpoint, params=params) + response.raise_for_status() + + data = response.json() + if not data: + break # No more markets + + filtered_counts = {"sports": 0, "short_term_price": 0, "weather": 0} + for market_data in data: + reason = self._should_filter_market(market_data) + if reason: + filtered_counts[reason] = filtered_counts.get(reason, 0) + 1 + continue + + market = self._parse_market(market_data) + if market: + trending_market = TrendingMarket( + market=market, + volume_24hr=market.volume_24hr, + liquidity=market.liquidity, + rank=len(trending_markets) + 1, + ) + if trending_market.is_valid_for_monitoring: + trending_markets.append(trending_market) + + if len(trending_markets) >= limit: + break + + total_filtered = sum(filtered_counts.values()) + if total_filtered: + parts = [f"{k}={v}" for k, v in filtered_counts.items() if v > 0] + logger.debug( + f"Batch {iteration}: fetched {len(data)}, " + f"filtered {total_filtered} ({', '.join(parts)}), " + f"kept: {len(trending_markets)}" + ) + else: + logger.debug( + f"Batch {iteration}: fetched {len(data)}, " + f"kept: {len(trending_markets)}" + ) + + if len(data) < batch_size: + break # No more markets available + + offset += batch_size + + logger.info( + f"Fetched {len(trending_markets)} trending markets " + f"(filtered: sports, short-term price, weather)" + ) + return trending_markets + + except httpx.HTTPError as e: + logger.error(f"HTTP error fetching trending markets: {e}") + return trending_markets # Return what we have so far + except Exception as e: + logger.error(f"Error fetching trending markets: {e}") + return trending_markets + + # Keywords that identify token launch / crypto project markets + TOKEN_LAUNCH_KEYWORDS = [ + "fdv", "market cap (fdv)", "launch a token", "tge", + "listing", "airdrop", "public sale", + ] + + def _is_token_launch_market(self, market_data: dict) -> bool: + """Check if a market is related to token launches / crypto projects.""" + question = (market_data.get("question") or "").lower() + return any(kw in question for kw in self.TOKEN_LAUNCH_KEYWORDS) + + def get_token_launch_markets(self, max_scan: int = 2000) -> List[TrendingMarket]: + """ + Scan active markets for token launch / crypto project markets + that may not be in the top trending list. + + Returns: + List of TrendingMarket objects for token launch markets. + """ + token_markets = [] + offset = 0 + batch_size = 100 + seen_ids = set() + + try: + while offset < max_scan: + params = { + "active": True, "closed": False, "archived": False, + "limit": batch_size, "offset": offset, + "order": "volume24hr", "ascending": False, + "enableOrderBook": True, + } + response = self._client.get(self.markets_endpoint, params=params) + response.raise_for_status() + data = response.json() + if not data: + break + + for market_data in data: + if not self._is_token_launch_market(market_data): + continue + + market = self._parse_market(market_data) + if market and market.id not in seen_ids: + seen_ids.add(market.id) + tm = TrendingMarket( + market=market, + volume_24hr=market.volume_24hr, + liquidity=market.liquidity, + ) + if tm.is_valid_for_monitoring: + token_markets.append(tm) + + if len(data) < batch_size: + break + offset += batch_size + + logger.info(f"Found {len(token_markets)} token launch markets") + return token_markets + + except Exception as e: + logger.error(f"Error fetching token launch markets: {e}") + return token_markets + + def get_niche_markets( + self, + limit: int = 50, + min_volume_24hr: float = 5_000, + max_volume_24hr: float = 500_000, + offset_start: int = 200, + max_scan: int = 1500, + ) -> List[TrendingMarket]: + """ + Fetch niche markets (lower volume) that may have higher information + asymmetry value. Scans markets ranked beyond the top trending list. + + Args: + limit: Max number of niche markets to return + min_volume_24hr: Minimum 24h volume (filter out dead markets) + max_volume_24hr: Maximum 24h volume (filter out large/macro markets) + offset_start: Start scanning from this rank + max_scan: Stop scanning after this offset + + Returns: + List of TrendingMarket objects for niche markets. + """ + niche_markets = [] + offset = offset_start + batch_size = 100 + + try: + while offset < max_scan and len(niche_markets) < limit: + params = { + "active": True, "closed": False, "archived": False, + "limit": batch_size, "offset": offset, + "order": "volume24hr", "ascending": False, + "enableOrderBook": True, + } + response = self._client.get(self.markets_endpoint, params=params) + response.raise_for_status() + data = response.json() + if not data: + break + + for market_data in data: + vol = float(market_data.get("volume24hr", 0) or 0) + + # Volume filter: not too small (dead), not too large (macro) + if vol < min_volume_24hr or vol > max_volume_24hr: + continue + + # Apply standard filters (sports, weather, short-term price) + if self._should_filter_market(market_data): + continue + + market = self._parse_market(market_data) + if market: + tm = TrendingMarket( + market=market, + volume_24hr=market.volume_24hr, + liquidity=market.liquidity, + ) + if tm.is_valid_for_monitoring: + niche_markets.append(tm) + if len(niche_markets) >= limit: + break + + if len(data) < batch_size: + break + offset += batch_size + + logger.info( + f"Found {len(niche_markets)} niche markets " + f"(volume ${min_volume_24hr:,.0f}-${max_volume_24hr:,.0f})" + ) + return niche_markets + + except Exception as e: + logger.error(f"Error fetching niche markets: {e}") + return niche_markets + + def get_tiered_markets(self) -> dict[str, list[TrendingMarket]]: + """ + Fetch ALL active markets and classify into 3 tiers by 24h volume. + + Returns dict with keys "tier1", "tier2", "tier3", each a list of TrendingMarket. + Mirrors options flow's "passive receive all signals" approach: + scan everything, filter later. + """ + settings = self.settings + all_markets = self.get_all_current_markets() + + tiers: dict[str, list[TrendingMarket]] = {"tier1": [], "tier2": [], "tier3": []} + + for market in all_markets: + # Apply standard filters + raw = { + "question": market.question, + "description": market.description, + "slug": market.slug, + } + if self._should_filter_market(raw): + continue + + if not market.clob_token_ids or not market.active or market.closed: + continue + + vol = market.volume_24hr + + tm = TrendingMarket( + market=market, + volume_24hr=vol, + liquidity=market.liquidity, + ) + + if vol >= settings.tier1_volume_min: + tiers["tier1"].append(tm) + elif vol >= settings.tier2_volume_min: + tiers["tier2"].append(tm) + elif vol >= settings.tier3_volume_min: + tiers["tier3"].append(tm) + # vol < tier3_volume_min → dead market, skip + + # Sort each tier by volume descending + for tier in tiers.values(): + tier.sort(key=lambda t: t.volume_24hr, reverse=True) + + logger.info( + f"Tiered markets: Tier1={len(tiers['tier1'])} (>{settings.tier1_volume_min/1000:.0f}K), " + f"Tier2={len(tiers['tier2'])} (>{settings.tier2_volume_min/1000:.0f}K), " + f"Tier3={len(tiers['tier3'])} (>{settings.tier3_volume_min/1000:.0f}K)" + ) + return tiers + + def get_market_by_id(self, market_id: str) -> Optional[Market]: + """ + Fetch a single market by ID. + + Args: + market_id: The market ID + + Returns: + Market object or None + """ + try: + url = f"{self.markets_endpoint}/{market_id}" + response = self._client.get(url) + response.raise_for_status() + + data = response.json() + return self._parse_market(data) + + except httpx.HTTPError as e: + logger.error(f"HTTP error fetching market {market_id}: {e}") + return None + except Exception as e: + logger.error(f"Error fetching market {market_id}: {e}") + return None + + def get_market_by_condition_id(self, condition_id: str) -> Optional[Market]: + """ + Fetch a market by condition ID. + + Args: + condition_id: The condition ID + + Returns: + Market object or None + """ + try: + params = {"conditionId": condition_id} + response = self._client.get(self.markets_endpoint, params=params) + response.raise_for_status() + + data = response.json() + if data and len(data) > 0: + return self._parse_market(data[0]) + return None + + except httpx.HTTPError as e: + logger.error(f"HTTP error fetching market by condition {condition_id}: {e}") + return None + except Exception as e: + logger.error(f"Error fetching market by condition {condition_id}: {e}") + return None + + def get_all_current_markets(self, batch_size: int = 100) -> List[Market]: + """ + Fetch all current active markets (paginated). + + Args: + batch_size: Number of markets per request + + Returns: + List of all active Market objects + """ + all_markets = [] + offset = 0 + + while True: + try: + params = { + "active": True, + "closed": False, + "archived": False, + "limit": batch_size, + "offset": offset, + "enableOrderBook": True, + } + + response = self._client.get(self.markets_endpoint, params=params) + response.raise_for_status() + + data = response.json() + if not data: + break + + for market_data in data: + market = self._parse_market(market_data) + if market: + all_markets.append(market) + + if len(data) < batch_size: + break + + offset += batch_size + + except Exception as e: + error_str = str(e) + if "422" in error_str: + logger.debug(f"Reached end of pagination at offset {offset}") + else: + logger.error(f"Error fetching markets at offset {offset}: {e}") + break + + logger.info(f"Fetched {len(all_markets)} total active markets") + return all_markets \ No newline at end of file diff --git a/src/services/polygon.py b/src/services/polygon.py new file mode 100644 index 0000000..e808ff0 --- /dev/null +++ b/src/services/polygon.py @@ -0,0 +1,157 @@ +"""Polygon.io API service for stocks, forex, and commodities market data.""" +import logging +from typing import Optional + +import httpx +from src.utils.http import get_client + +logger = logging.getLogger(__name__) + +POLYGON_API = "https://api.polygon.io" + + +class PolygonService: + """ + Polygon.io API client for financial market data. + + Covers: stocks, options, forex, crypto, indices, commodities futures. + """ + + def __init__(self, api_key: str): + self.api_key = api_key + self._client = get_client(timeout=15.0) + + def is_available(self) -> bool: + return bool(self.api_key and self.api_key.strip()) + + def _get(self, path: str, params: Optional[dict] = None) -> dict: + """Make authenticated GET request.""" + params = params or {} + params["apiKey"] = self.api_key + resp = self._client.get(f"{POLYGON_API}{path}", params=params) + resp.raise_for_status() + return resp.json() + + def get_ticker_snapshot(self, ticker: str) -> str: + """ + Get previous day close + recent daily bars for a ticker. + + Args: + ticker: Ticker symbol (AAPL, TSLA, GS, SPY, QQQ, GLD, USO) + + Returns: + Formatted price and market data report. + """ + t = ticker.strip().upper() + + try: + # Previous close (free tier) + prev_data = self._get(f"/v2/aggs/ticker/{t}/prev") + results = prev_data.get("results", []) + + if not results: + return f"No data found for '{ticker}' on Polygon.io." + + bar = results[0] + close = bar.get("c", 0) + open_p = bar.get("o", 0) + high = bar.get("h", 0) + low = bar.get("l", 0) + volume = bar.get("v", 0) + vwap = bar.get("vw", 0) + + change = close - open_p if open_p else 0 + change_pct = (change / open_p * 100) if open_p else 0 + + lines = [ + f"--- {t} Last Trading Day (Polygon.io) ---", + f"Close: ${close:,.2f}", + f"Open: ${open_p:,.2f}", + f"High: ${high:,.2f}", + f"Low: ${low:,.2f}", + f"Change: {change:+.2f} ({change_pct:+.2f}%)", + ] + if vwap: + lines.append(f"VWAP: ${vwap:,.2f}") + if volume: + lines.append(f"Volume: {volume:,.0f}") + + # Also try to get 5-day bars for trend + try: + from datetime import date, timedelta + end = date.today() + start = end - timedelta(days=10) + range_data = self._get( + f"/v2/aggs/ticker/{t}/range/1/day/{start.isoformat()}/{end.isoformat()}", + params={"adjusted": "true", "sort": "asc", "limit": 10}, + ) + bars = range_data.get("results", []) + if len(bars) >= 2: + first_close = bars[0].get("c", 0) + last_close = bars[-1].get("c", 0) + if first_close: + week_change = ((last_close - first_close) / first_close) * 100 + lines.append(f"~{len(bars)}-day Change: {week_change:+.2f}%") + except Exception: + pass # trend data is optional + + lines.append("---") + return "\n".join(lines) + + except httpx.HTTPStatusError as e: + if e.response.status_code == 404: + return f"Ticker '{ticker}' not found on Polygon.io." + return f"Polygon API error for '{ticker}': HTTP {e.response.status_code}" + except Exception as e: + msg = f"Polygon query failed for '{ticker}': {e}" + logger.error(msg) + return msg + + def get_market_news(self, ticker: str, limit: int = 5) -> str: + """ + Get recent news articles for a ticker. + + Args: + ticker: Stock/crypto ticker (e.g. AAPL, TSLA, GS) + limit: Number of articles (1-10) + + Returns: + Formatted news report. + """ + t = ticker.strip().upper() + limit = max(1, min(limit, 10)) + + try: + data = self._get("/v2/reference/news", params={ + "ticker": t, + "limit": limit, + "order": "desc", + "sort": "published_utc", + }) + + results = data.get("results", []) + if not results: + return f"No recent news found for '{ticker}'." + + lines = [f"--- {t} Recent News (Polygon.io) ---"] + for i, article in enumerate(results, 1): + title = article.get("title", "No title") + published = article.get("published_utc", "")[:19] + source = article.get("publisher", {}).get("name", "Unknown") + desc = article.get("description", "")[:200] + if len(article.get("description", "")) > 200: + desc += "..." + + lines.append(f"{i}. **{title}**") + lines.append(f" Source: {source} | {published}") + if desc: + lines.append(f" {desc}") + lines.append("") + + lines.append("---") + return "\n".join(lines) + + except Exception as e: + msg = f"Polygon news query failed for '{ticker}': {e}" + logger.error(msg) + return msg \ No newline at end of file diff --git a/src/services/price_monitor.py b/src/services/price_monitor.py new file mode 100644 index 0000000..2cd1947 --- /dev/null +++ b/src/services/price_monitor.py @@ -0,0 +1,428 @@ +"""Price monitoring service - monitors ALL active market prices for volatility.""" +import asyncio +import json +import logging +from collections import deque +from dataclasses import dataclass, field +from datetime import datetime +from pathlib import Path +from typing import Awaitable, Callable, Dict, List, Optional + +import httpx + +from src.utils.http import get_async_client + +logger = logging.getLogger(__name__) + +# Gamma API for fetching market prices +GAMMA_API_URL = "https://gamma-api.polymarket.com/markets" + +# Storage directory for price volatility alerts +VOLATILITY_DIR = Path(__file__).parent.parent.parent / "price_volatility" + + +@dataclass +class PricePoint: + """A single price observation.""" + timestamp: int + yes_price: float + + +@dataclass +class VolatilityAlert: + """A price volatility alert.""" + market_id: str + market_question: str + start_timestamp: int + end_timestamp: int + start_price: float + end_price: float + price_change: float + price_change_percent: float + direction: str # "UP" or "DOWN" + window_seconds: int + detected_at: str = field(default_factory=lambda: datetime.utcnow().isoformat()) + + def to_dict(self) -> dict: + return { + "market_id": self.market_id, + "market_question": self.market_question, + "start_timestamp": self.start_timestamp, + "end_timestamp": self.end_timestamp, + "start_price": self.start_price, + "end_price": self.end_price, + "price_change": self.price_change, + "price_change_percent": self.price_change_percent, + "direction": self.direction, + "window_seconds": self.window_seconds, + "detected_at": self.detected_at, + } + + +class PriceMonitor: + """ + Monitors ALL active market prices for short-term volatility. + + Tracks Yes prices for all active markets and alerts when + price changes exceed threshold within the time window. + """ + + # Default configuration + DEFAULT_WINDOW_SECONDS = 300 # 5 minutes + DEFAULT_THRESHOLD = 0.10 # 10% + DEFAULT_MAX_HISTORY_SECONDS = 3600 # Keep 1 hour of history + DEFAULT_POLL_INTERVAL = 30 # Poll all markets every 30 seconds + + def __init__( + self, + window_seconds: int = DEFAULT_WINDOW_SECONDS, + threshold: float = DEFAULT_THRESHOLD, + max_history_seconds: int = DEFAULT_MAX_HISTORY_SECONDS, + poll_interval: int = DEFAULT_POLL_INTERVAL, + on_volatility_detected: Optional[Callable[["VolatilityAlert"], Awaitable[None]]] = None, + ): + """ + Initialize the price monitor. + + Args: + window_seconds: Time window for volatility detection (default 5 minutes) + threshold: Price change threshold to trigger alert (default 10%) + max_history_seconds: How long to keep price history (default 1 hour) + poll_interval: Interval for polling all markets (default 30 seconds) + on_volatility_detected: Async callback when volatility is detected + """ + self.window_seconds = window_seconds + self.threshold = threshold + self.max_history_seconds = max_history_seconds + self.poll_interval = poll_interval + + # Callback for volatility detection + self._on_volatility_detected = on_volatility_detected + + # Price history per market: market_id -> deque of PricePoints + self._price_history: Dict[str, deque] = {} + + # Market info cache: market_id -> question + self._market_info: Dict[str, str] = {} + + # Track recent alerts to avoid duplicates (market_id -> last_alert_timestamp) + self._recent_alerts: Dict[str, int] = {} + + # Minimum interval between alerts for same market (seconds) + self._alert_cooldown = 3600 # 1 hour (match window_seconds) + + # HTTP client for API calls + self._client: Optional[httpx.AsyncClient] = None + + # Control flag + self._running = False + + # Ensure storage directory exists + VOLATILITY_DIR.mkdir(parents=True, exist_ok=True) + + def record_price(self, market_id: str, market_question: str, yes_price: float) -> Optional[VolatilityAlert]: + """ + Record a price observation and check for volatility. + + Args: + market_id: The market ID + market_question: The market question text + yes_price: Current Yes price (0-1) + + Returns: + VolatilityAlert if threshold exceeded, None otherwise + """ + now = int(datetime.utcnow().timestamp()) + + # Initialize history for new markets + if market_id not in self._price_history: + self._price_history[market_id] = deque() + + history = self._price_history[market_id] + + # Add new price point + history.append(PricePoint(timestamp=now, yes_price=yes_price)) + + # Clean up old entries + cutoff = now - self.max_history_seconds + while history and history[0].timestamp < cutoff: + history.popleft() + + # Check for volatility + alert = self._check_volatility(market_id, market_question, now) + + if alert: + # Check cooldown + last_alert = self._recent_alerts.get(market_id, 0) + if now - last_alert < self._alert_cooldown: + logger.debug(f"Alert suppressed for {market_id} (cooldown)") + return None + + # Record alert + self._recent_alerts[market_id] = now + self._store_alert(alert) + + # Log warning + logger.warning( + f"🚨 PRICE VOLATILITY: {market_question[:50]}... " + f"{alert.direction} {abs(alert.price_change_percent):.1%} " + f"({alert.start_price:.2%} → {alert.end_price:.2%}) " + f"in {alert.window_seconds // 60}min" + ) + + return alert + + return None + + def _check_volatility( + self, market_id: str, market_question: str, current_time: int + ) -> Optional[VolatilityAlert]: + """ + Check if price volatility exceeds threshold within the time window. + + Args: + market_id: The market ID + market_question: The market question text + current_time: Current timestamp + + Returns: + VolatilityAlert if threshold exceeded, None otherwise + """ + history = self._price_history.get(market_id) + if not history or len(history) < 2: + return None + + current_price = history[-1].yes_price + window_start = current_time - self.window_seconds + + # Find the oldest price within the window + oldest_in_window = None + for point in history: + if point.timestamp >= window_start: + oldest_in_window = point + break + + if oldest_in_window is None: + return None + + # Calculate price change + price_change = current_price - oldest_in_window.yes_price + price_change_abs = abs(price_change) + + if price_change_abs < self.threshold: + return None + + # Create alert + return VolatilityAlert( + market_id=market_id, + market_question=market_question, + start_timestamp=oldest_in_window.timestamp, + end_timestamp=current_time, + start_price=oldest_in_window.yes_price, + end_price=current_price, + price_change=price_change, + price_change_percent=price_change, + direction="UP" if price_change > 0 else "DOWN", + window_seconds=current_time - oldest_in_window.timestamp, + ) + + def _store_alert(self, alert: VolatilityAlert) -> None: + """ + Store a volatility alert to file. + + Args: + alert: The alert to store + """ + alerts_file = VOLATILITY_DIR / "volatility_alerts.json" + + # Load existing alerts + existing_alerts = [] + if alerts_file.exists(): + try: + with open(alerts_file, 'r', encoding='utf-8') as f: + existing_alerts = json.load(f) + except Exception as e: + logger.error(f"Failed to load existing alerts: {e}") + + # Add new alert + existing_alerts.append(alert.to_dict()) + + # Save back + try: + with open(alerts_file, 'w', encoding='utf-8') as f: + json.dump(existing_alerts, f, ensure_ascii=False, indent=2) + except Exception as e: + logger.error(f"Failed to store volatility alert: {e}") + + def get_price_history(self, market_id: str) -> List[dict]: + """ + Get price history for a market. + + Args: + market_id: The market ID + + Returns: + List of price points as dicts + """ + history = self._price_history.get(market_id, deque()) + return [{"timestamp": p.timestamp, "yes_price": p.yes_price} for p in history] + + def get_all_alerts(self) -> List[dict]: + """ + Get all stored volatility alerts. + + Returns: + List of alerts as dicts + """ + alerts_file = VOLATILITY_DIR / "volatility_alerts.json" + + if not alerts_file.exists(): + return [] + + try: + with open(alerts_file, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + logger.error(f"Failed to load alerts: {e}") + return [] + + def clear_history(self, market_id: Optional[str] = None) -> None: + """ + Clear price history. + + Args: + market_id: If provided, clear only this market's history. Otherwise clear all. + """ + if market_id: + if market_id in self._price_history: + del self._price_history[market_id] + logger.info(f"Cleared price history for {market_id}") + else: + self._price_history.clear() + logger.info("Cleared all price history") + + async def _fetch_all_active_markets(self) -> List[dict]: + """ + Fetch all active markets from Gamma API. + + Returns: + List of market data dicts with id, question, and outcomePrices + """ + all_markets = [] + offset = 0 + batch_size = 100 + + try: + while True: + params = { + "active": True, + "closed": False, + "archived": False, + "limit": batch_size, + "offset": offset, + "enableOrderBook": True, + } + + response = await self._client.get(GAMMA_API_URL, params=params) + response.raise_for_status() + + data = response.json() + if not data: + break + + for market in data: + market_id = str(market.get("id", "")) + question = market.get("question", "") + outcome_prices = market.get("outcomePrices", []) + + if isinstance(outcome_prices, str): + outcome_prices = json.loads(outcome_prices) + + if market_id and outcome_prices: + all_markets.append({ + "id": market_id, + "question": question, + "yes_price": float(outcome_prices[0]) if outcome_prices else None, + }) + # Cache market info + self._market_info[market_id] = question + + if len(data) < batch_size: + break + + offset += batch_size + + except Exception as e: + logger.error(f"Error fetching active markets: {e}") + + return all_markets + + async def _poll_all_prices(self) -> List[VolatilityAlert]: + """ + Poll prices for all active markets and check for volatility. + + Returns: + List of volatility alerts triggered + """ + alerts = [] + + markets = await self._fetch_all_active_markets() + logger.debug(f"Polling prices for {len(markets)} active markets") + + for market in markets: + market_id = market["id"] + question = market["question"] + yes_price = market.get("yes_price") + + if yes_price is not None: + alert = self.record_price(market_id, question, yes_price) + if alert: + alerts.append(alert) + + return alerts + + async def run(self) -> None: + """ + Start the price monitoring loop. + + Continuously polls all active markets at the configured interval. + """ + self._running = True + self._client = get_async_client(timeout=60.0) + + logger.info( + f"Starting price monitor (interval: {self.poll_interval}s, " + f"window: {self.window_seconds}s, threshold: {self.threshold:.0%})" + ) + + try: + while self._running: + try: + alerts = await self._poll_all_prices() + if alerts: + logger.info(f"Detected {len(alerts)} volatility alerts") + + # Call callback for each alert + if self._on_volatility_detected: + for alert in alerts: + try: + await self._on_volatility_detected(alert) + except Exception as e: + logger.error(f"Error in volatility callback: {e}") + + except Exception as e: + logger.error(f"Error in price monitoring loop: {e}") + + await asyncio.sleep(self.poll_interval) + finally: + await self._client.aclose() + self._client = None + + def stop(self) -> None: + """Stop the price monitoring loop.""" + self._running = False + logger.info("Price monitor stopping...") + + def get_monitored_market_count(self) -> int: + """Get the number of markets currently being monitored.""" + return len(self._price_history) \ No newline at end of file diff --git a/src/services/resolution_tracker.py b/src/services/resolution_tracker.py new file mode 100644 index 0000000..70dcf8d --- /dev/null +++ b/src/services/resolution_tracker.py @@ -0,0 +1,115 @@ +"""Resolution tracker - checks if markets with signals have resolved and updates correctness.""" +import asyncio +import logging +from datetime import datetime +from typing import Optional + +from src.db.database import SignalDatabase +from src.services.market_fetcher import MarketFetcher + +logger = logging.getLogger(__name__) + + +class ResolutionTracker: + """Tracks market resolutions and computes signal correctness.""" + + def __init__(self, db: SignalDatabase): + self.db = db + self.market_fetcher = MarketFetcher() + + def _determine_resolved_outcome(self, market) -> Optional[str]: + """ + Determine the resolved outcome from a market. + + A market is considered resolved if closed==True and one outcome price >= 0.99. + + Returns: + The winning outcome string (e.g. "Yes" or "No"), or None if not resolved. + """ + if not market.closed: + return None + + if not market.outcomes or not market.outcome_prices: + return None + + for outcome, price in zip(market.outcomes, market.outcome_prices): + if price >= 0.99: + return outcome + + return None + + def _is_past_end_date(self, market) -> bool: + """Check if a market's end_date has passed.""" + if not market.end_date: + return True # No end date, always check + try: + end_dt = datetime.fromisoformat(market.end_date.replace("Z", "+00:00")) + return datetime.utcnow().replace(tzinfo=end_dt.tzinfo) >= end_dt + except (ValueError, TypeError): + return True + + async def check_all(self) -> dict: + """ + Check all unresolved markets for resolution. + + Returns: + Summary dict with counts. + """ + unresolved_ids = self.db.get_unresolved_market_ids() + if not unresolved_ids: + logger.debug("No unresolved markets to check") + return {"checked": 0, "resolved": 0, "signals_updated": 0} + + logger.info(f"Checking {len(unresolved_ids)} unresolved markets for resolution") + + checked = 0 + resolved = 0 + signals_updated = 0 + + for market_id in unresolved_ids: + try: + market = self.market_fetcher.get_market_by_id(market_id) + if not market: + logger.debug(f"Market {market_id} not found on API") + checked += 1 + await asyncio.sleep(0.5) + continue + + # Optimization: skip markets whose end_date hasn't passed yet + if not self._is_past_end_date(market): + checked += 1 + await asyncio.sleep(0.5) + continue + + outcome = self._determine_resolved_outcome(market) + if outcome: + updated = self.db.mark_market_resolved( + market_id=market_id, + resolved_outcome=outcome, + resolved_at=datetime.utcnow(), + ) + resolved += 1 + signals_updated += updated + logger.info( + f"Market resolved: {market.question[:50]}... " + f"outcome={outcome}, {updated} signals updated" + ) + + checked += 1 + await asyncio.sleep(0.5) # Rate limiting + + except Exception as e: + logger.error(f"Error checking market {market_id}: {e}") + checked += 1 + await asyncio.sleep(0.5) + + result = { + "checked": checked, + "resolved": resolved, + "signals_updated": signals_updated, + } + logger.info( + f"Resolution check complete: {checked} checked, " + f"{resolved} resolved, {signals_updated} signals updated" + ) + return result diff --git a/src/services/rtds_client.py b/src/services/rtds_client.py new file mode 100644 index 0000000..334adcd --- /dev/null +++ b/src/services/rtds_client.py @@ -0,0 +1,237 @@ +""" +RTDS (Real-Time Data Socket) client for Polymarket. + +Connects to wss://ws-live-data.polymarket.com and subscribes to +activity/trades for real-time trade data across ALL markets. + +Replaces the per-market HTTP polling approach with a single persistent +WebSocket connection — zero missed trades, sub-second latency. +""" +import asyncio +import json +import logging +import time +from typing import Awaitable, Callable, Optional + +import websockets +from websockets.asyncio.client import ClientConnection + +from src.config.settings import get_settings +from src.models.trade import TradeActivity + +logger = logging.getLogger(__name__) + +RTDS_URI = "wss://ws-live-data.polymarket.com" +HEARTBEAT_INTERVAL = 5 # seconds +RECONNECT_DELAYS = [1, 2, 5, 10, 30, 60] # backoff schedule + + +class RTDSClient: + """ + Persistent WebSocket client for Polymarket RTDS trade stream. + + Features: + - Auto-reconnect with exponential backoff + - Heartbeat (PING every 5s) + - Parses raw messages into TradeActivity objects + - Fires an async callback for each trade + """ + + def __init__( + self, + on_trade: Optional[Callable[[TradeActivity], Awaitable[None]]] = None, + proxy: Optional[str] = None, + ): + self._on_trade = on_trade + self._proxy = proxy + self._running = False + self._ws: Optional[ClientConnection] = None + self._trade_count = 0 + self._connect_count = 0 + + # ================================================================ + # Message parsing + # ================================================================ + + @staticmethod + def _parse_trade(payload: dict) -> Optional[TradeActivity]: + """Convert an RTDS trade payload into a TradeActivity.""" + try: + side = (payload.get("side") or "").upper() + size = float(payload.get("size", 0) or 0) + price = float(payload.get("price", 0) or 0) + usdc_size = size * price + + outcome = payload.get("outcome", "Yes") + outcome_index = int(payload.get("outcomeIndex", 0 if outcome == "Yes" else 1)) + + ts = int(payload.get("timestamp", 0) or 0) + if ts == 0: + ts = int(time.time()) + + return TradeActivity( + transaction_hash=payload.get("transactionHash", ""), + timestamp=ts, + condition_id=payload.get("conditionId", ""), + asset=payload.get("asset", ""), + side=side, + size=size, + usdc_size=usdc_size, + price=price, + outcome=outcome, + outcome_index=outcome_index, + title=payload.get("title", ""), + slug=payload.get("slug"), + event_slug=payload.get("eventSlug"), + proxy_wallet=payload.get("proxyWallet"), + name=payload.get("name") or payload.get("pseudonym"), + ) + except Exception as e: + logger.debug(f"Failed to parse RTDS trade: {e}") + return None + + # ================================================================ + # Connection lifecycle + # ================================================================ + + async def _heartbeat(self, ws: ClientConnection) -> None: + """Send PING every HEARTBEAT_INTERVAL seconds.""" + try: + while True: + await asyncio.sleep(HEARTBEAT_INTERVAL) + await ws.send("PING") + except (asyncio.CancelledError, websockets.ConnectionClosed): + pass + + async def _subscribe(self, ws: ClientConnection) -> None: + """Subscribe to the activity/trades stream.""" + msg = { + "action": "subscribe", + "subscriptions": [ + {"topic": "activity", "type": "trades", "filters": ""} + ], + } + await ws.send(json.dumps(msg)) + logger.info("Subscribed to RTDS activity/trades") + + async def _consume(self, ws: ClientConnection) -> None: + """Read messages from the WebSocket and dispatch trades.""" + async for raw in ws: + if not self._running: + break + + if raw == "PONG" or not raw.strip(): + continue + + try: + msg = json.loads(raw) + except json.JSONDecodeError: + continue + + if msg.get("topic") != "activity" or msg.get("type") != "trades": + continue + + payload = msg.get("payload") + if not payload: + continue + + activity = self._parse_trade(payload) + if not activity: + continue + + self._trade_count += 1 + + if self._on_trade: + try: + await self._on_trade(activity) + except Exception as e: + logger.error(f"Error in trade callback: {e}") + + async def _connect_and_run(self) -> None: + """Single connection attempt: connect → subscribe → consume.""" + self._connect_count += 1 + logger.info( + f"Connecting to RTDS ({self._connect_count})... " + f"(total trades so far: {self._trade_count})" + ) + + connect_kwargs = {"ping_interval": None} + if self._proxy: + connect_kwargs["proxy"] = self._proxy + logger.info(f"RTDS using proxy: {self._proxy}") + + async with websockets.connect(RTDS_URI, **connect_kwargs) as ws: + self._ws = ws + logger.info("RTDS connected") + + await self._subscribe(ws) + + hb_task = asyncio.create_task(self._heartbeat(ws)) + try: + await self._consume(ws) + finally: + hb_task.cancel() + self._ws = None + + # ================================================================ + # Public API + # ================================================================ + + async def run(self) -> None: + """ + Start the RTDS client with auto-reconnect. + + Runs forever until stop() is called. + """ + self._running = True + consecutive_failures = 0 + + while self._running: + try: + await self._connect_and_run() + # Clean disconnect (stop() called) — exit + if not self._running: + break + # Unexpected clean close — reconnect immediately + consecutive_failures = 0 + except ( + websockets.ConnectionClosed, + websockets.InvalidURI, + websockets.InvalidHandshake, + OSError, + ConnectionError, + ) as e: + if not self._running: + break + delay_idx = min(consecutive_failures, len(RECONNECT_DELAYS) - 1) + delay = RECONNECT_DELAYS[delay_idx] + consecutive_failures += 1 + logger.warning( + f"RTDS disconnected: {type(e).__name__}: {e}. " + f"Reconnecting in {delay}s (attempt {consecutive_failures})" + ) + await asyncio.sleep(delay) + except Exception as e: + if not self._running: + break + logger.error(f"Unexpected RTDS error: {e}. Reconnecting in 10s") + await asyncio.sleep(10) + + logger.info(f"RTDS client stopped (total trades received: {self._trade_count})") + + def stop(self) -> None: + """Stop the RTDS client.""" + self._running = False + if self._ws: + asyncio.ensure_future(self._ws.close()) + logger.info("RTDS client stopping...") + + @property + def trade_count(self) -> int: + """Total number of trades received since start.""" + return self._trade_count + + @property + def is_connected(self) -> bool: + """Whether the WebSocket is currently connected.""" + return self._ws is not None and self._ws.state.name == "OPEN" \ No newline at end of file diff --git a/src/services/serper_search.py b/src/services/serper_search.py new file mode 100644 index 0000000..85307bb --- /dev/null +++ b/src/services/serper_search.py @@ -0,0 +1,96 @@ +"""Serper.dev web search service.""" +import logging +from typing import Optional + +import httpx +from src.utils.http import get_client + +logger = logging.getLogger(__name__) + +SERPER_SEARCH_URL = "https://google.serper.dev/search" + + +class SerperSearchService: + """Web search service using Serper.dev API (Google Search results).""" + + def __init__(self, api_key: Optional[str] = None): + self.api_key = api_key or "" + if not self.api_key: + logger.debug("SERPER_API_KEY not set. Serper search will be disabled.") + + def is_available(self) -> bool: + return bool(self.api_key and self.api_key.strip()) + + def search(self, query: str, max_results: int = 5) -> str: + if not self.is_available(): + return "Web search unavailable: SERPER_API_KEY not configured." + + headers = { + "X-API-KEY": self.api_key, + "Content-Type": "application/json", + } + payload = {"q": query, "num": max_results} + + try: + with get_client(timeout=20) as client: + response = client.post(SERPER_SEARCH_URL, json=payload, headers=headers) + + if response.status_code != 200: + logger.error(f"Serper API error: {response.status_code} - {response.text[:200]}") + return f"Web search API Error: {response.status_code}" + + data = response.json() + + # Format results + report = [f"--- Web Search Results for '{query}' ---"] + + # Knowledge graph answer + kg = data.get("knowledgeGraph") + if kg: + title = kg.get("title", "") + desc = kg.get("description", "") + if title and desc: + report.append(f"**Summary**: {title} — {desc}\n") + + # Answer box + answer_box = data.get("answerBox") + if answer_box: + answer = answer_box.get("answer") or answer_box.get("snippet", "") + if answer: + report.append(f"**Summary**: {answer}\n") + + # Organic results + organic = data.get("organic", []) + if not organic: + return f"No web search results found for '{query}'." + + for idx, item in enumerate(organic[:max_results], 1): + title = item.get("title", "No title") + url = item.get("link", "") + snippet = item.get("snippet", "") + report.append(f"{idx}. **{title}**") + report.append(f" Source: {url}") + report.append(f" {snippet}") + report.append("") + + report.append("-------------------------------------------") + return "\n".join(report) + + except Exception as e: + logger.error(f"Serper search failed: {e}") + return f"Web search failed: {str(e)}" + + def search_for_market(self, market_question: str, max_results: int = 5) -> str: + if not self.is_available(): + return "Web search unavailable: SERPER_API_KEY not configured." + + query = market_question[:200] + result = self.search(query, max_results=max_results) + + # Propagate errors so the unified WebSearchService can fall back + if "API Error" in result or "search failed" in result: + return result + + if "No web search results" not in result and "Error" not in result: + return "## 🔍 Web Search Results (News & Analysis)\n" + result + return f"No relevant web results found for: {market_question[:50]}..." \ No newline at end of file diff --git a/src/services/stats_engine.py b/src/services/stats_engine.py new file mode 100644 index 0000000..5c74b6b --- /dev/null +++ b/src/services/stats_engine.py @@ -0,0 +1,90 @@ +"""Stats engine - computes signal performance statistics from the database.""" +import logging +from typing import List + +from src.db.database import SignalDatabase +from src.models.anomaly_signal import AnomalySignal + +logger = logging.getLogger(__name__) + + +class StatsEngine: + """Computes signal performance statistics.""" + + def __init__(self, db: SignalDatabase): + self.db = db + + def get_overview(self) -> dict: + """ + Get overall signal performance stats. + + Returns: + Dict with total_signals, resolved, correct, win_rate, avg_roi, total_theoretical_pnl. + """ + return self.db.get_stats() + + def get_stats_by_likelihood_tier(self) -> List[dict]: + """ + Get stats broken down by information_asymmetry_score tiers. + + Tiers: 0.4-0.6, 0.6-0.8, 0.8-1.0 + + Returns: + List of tier stat dicts. + """ + return self.db.get_stats_by_tier() + + def get_recent_resolved(self, limit: int = 20) -> List[AnomalySignal]: + """Get recently resolved signals.""" + return self.db.get_recent_resolved(limit) + + def get_best_worst(self, n: int = 5) -> dict: + """Get best and worst signals by ROI.""" + return self.db.get_best_worst(n) + + def format_stats_summary(self) -> str: + """ + Format a human-readable stats summary for briefings. + + Returns: + Markdown-formatted stats string. + """ + stats = self.get_overview() + tier_stats = self.get_stats_by_likelihood_tier() + + if stats["resolved"] == 0: + return "" + + lines = [ + "## Signal Performance History", + "", + f"| Metric | Value |", + f"|--------|-------|", + f"| Total Signals | {stats['total_signals']} |", + f"| Resolved | {stats['resolved']} |", + f"| Correct | {stats['correct']} |", + f"| Win Rate | **{stats['win_rate']:.1%}** |", + f"| Avg ROI | **{stats['avg_roi']:+.1%}** |", + f"| Total Theoretical PnL | **{stats['total_theoretical_pnl']:+.2f}x** |", + "", + ] + + # Tier breakdown + has_resolved_tiers = any(t["resolved"] > 0 for t in tier_stats) + if has_resolved_tiers: + lines.extend([ + "### By Signal Confidence Tier", + "", + "| Confidence Range | Signals | Resolved | Win Rate | Avg ROI |", + "|-----------------|---------|----------|----------|---------|", + ]) + for t in tier_stats: + if t["total"] > 0: + wr = f"{t['win_rate']:.0%}" if t["resolved"] > 0 else "N/A" + roi = f"{t['avg_roi']:+.1%}" if t["resolved"] > 0 else "N/A" + lines.append( + f"| {t['tier']} | {t['total']} | {t['resolved']} | {wr} | {roi} |" + ) + lines.append("") + + return "\n".join(lines) diff --git a/src/services/tavily_search.py b/src/services/tavily_search.py new file mode 100644 index 0000000..41f5b1a --- /dev/null +++ b/src/services/tavily_search.py @@ -0,0 +1,141 @@ +"""Tavily web search service - replaces Google Search for whale trade verification.""" +import logging +from typing import Optional + +import httpx +from src.utils.http import get_client + +logger = logging.getLogger(__name__) + +TAVILY_SEARCH_URL = "https://api.tavily.com/search" + + +def _format_search_results(query: str, results: list[dict]) -> str: + """Format Tavily search results as a readable report.""" + report = [f"--- Web Search Results for '{query}' ---"] + for idx, item in enumerate(results, 1): + title = item.get("title", "No title") + url = item.get("url", "") + content = item.get("content", "")[:300] + if len(item.get("content", "")) > 300: + content += "..." + report.append(f"{idx}. **{title}**") + report.append(f" Source: {url}") + report.append(f" {content}") + report.append("") + report.append("-------------------------------------------") + return "\n".join(report) + + +class TavilySearchService: + """ + Web search service using Tavily API for whale trade verification. + + Replaces Google Search grounding with explicit Tavily web search, + passing results as context to the LLM. + """ + + def __init__(self, api_key: Optional[str] = None): + self.api_key = api_key or "" + if not self.api_key: + logger.warning("TAVILY_API_KEY not set. Web search will be disabled.") + + def is_available(self) -> bool: + """Check if Tavily search is available (API key is set).""" + return bool(self.api_key and self.api_key.strip()) + + def search( + self, + query: str, + max_results: int = 5, + search_depth: str = "basic", + ) -> str: + """ + Search the web using Tavily API. + + Args: + query: Search query + max_results: Number of results to return (1-10) + search_depth: "basic" for fast search, "advanced" for deeper search + + Returns: + Formatted search results report. + """ + if not self.is_available(): + return "Web search unavailable: TAVILY_API_KEY not configured." + + max_results = max(1, min(max_results, 10)) + + payload = { + "api_key": self.api_key, + "query": query, + "search_depth": search_depth, + "max_results": max_results, + "include_answer": True, + } + + try: + with get_client(timeout=20) as client: + response = client.post(TAVILY_SEARCH_URL, json=payload) + + if response.status_code != 200: + logger.error(f"Tavily API error: {response.status_code} - {response.text[:200]}") + return f"Web search API Error: {response.status_code}" + + data = response.json() + results = data.get("results", []) + + if not results: + return f"No web search results found for '{query}'." + + report_parts = [] + + # Include AI-generated answer summary if available + answer = data.get("answer") + if answer: + report_parts.append(f"**Summary**: {answer}\n") + + report_parts.append(_format_search_results(query, results)) + return "\n".join(report_parts) + + except Exception as e: + logger.error(f"Tavily search failed: {e}") + return f"Web search failed: {str(e)}" + + def search_for_market( + self, + market_question: str, + max_results: int = 5, + ) -> str: + """ + Search the web for information relevant to a prediction market. + + Performs a deeper search for market-relevant news. + + Args: + market_question: The market question to search for + max_results: Number of results per query + + Returns: + Combined search results. + """ + if not self.is_available(): + return "Web search unavailable: TAVILY_API_KEY not configured." + + results = [] + + # Search with the market question directly + query = market_question[:200] + main_result = self.search(query, max_results=max_results, search_depth="advanced") + + # Propagate errors so the unified WebSearchService can fall back + if "API Error" in main_result or "search failed" in main_result: + return main_result + + if "No web search results" not in main_result and "Error" not in main_result: + results.append("## 🔍 Web Search Results (News & Analysis)\n" + main_result) + + if not results: + return f"No relevant web results found for: {market_question[:50]}..." + + return "\n\n".join(results) \ No newline at end of file diff --git a/src/services/telegram_search.py b/src/services/telegram_search.py new file mode 100644 index 0000000..8acb1e9 --- /dev/null +++ b/src/services/telegram_search.py @@ -0,0 +1,191 @@ +"""Telegram search service for crypto and geopolitical channel monitoring.""" +import asyncio +import logging +from datetime import datetime, timedelta +from typing import Optional, List + +try: + import nest_asyncio + nest_asyncio.apply() +except ImportError: + pass + +logger = logging.getLogger(__name__) + +# Default public channels: crypto + geopolitics/politics +# Verified 2026-03-31: all channels return text messages with meaningful content +DEFAULT_CHANNELS = [ + # Crypto / macro + "CryptoVIPSignalTA", # crypto signals + macro news + "whale_alert_io", # on-chain whale transfers + "WatcherGuru", # crypto/macro breaking news + # Geopolitics & politics + "DDGeopolitics", # geopolitical analysis (views ~20k) + "disclosetv", # US politics, breaking news (views ~50-70k) + "realDonaldTrump", # Trump's own posts + "intelslava", # Russia/Ukraine, geopolitics (views ~50k) + "TheScrollOfBenjamin", # Middle East geopolitics + "WarMonitors", # conflict breaking news (views ~14k, active) +] + + +class TelegramSearchService: + """ + Searches crypto-relevant public Telegram channels for messages. + + Uses Telethon (MTProto API) to search public channels by keyword. + Requires a one-time auth to generate a session string. + """ + + def __init__( + self, + api_id: str = "", + api_hash: str = "", + session_string: str = "", + channels: Optional[List[str]] = None, + ): + self.api_id = api_id + self.api_hash = api_hash + self.session_string = session_string + self.channels = channels or DEFAULT_CHANNELS + self._client = None + + def is_available(self) -> bool: + """Check if Telegram search is available.""" + if not (self.api_id and self.api_hash and self.session_string): + return False + try: + import telethon # noqa: F401 + return True + except ImportError: + logger.warning("telethon not installed. Telegram search disabled.") + return False + + def _get_client(self): + """Get or create the Telethon client.""" + if self._client is None: + from telethon import TelegramClient + from telethon.sessions import StringSession + self._client = TelegramClient( + StringSession(self.session_string), + int(self.api_id), + self.api_hash, + ) + return self._client + + async def _search_channel( + self, + channel: str, + query: str, + limit: int = 5, + ) -> List[dict]: + """Search a single channel for messages matching a query.""" + from telethon.errors import ( + FloodWaitError, + ChannelPrivateError, + UsernameNotOccupiedError, + UsernameInvalidError, + ) + + results = [] + try: + client = self._get_client() + async for message in client.iter_messages( + channel, + search=query, + limit=limit, + ): + if not message.text: + continue + results.append({ + "channel": channel, + "text": message.text, + "date": message.date.strftime("%Y-%m-%d %H:%M UTC") if message.date else "", + "views": message.views or 0, + "forwards": message.forwards or 0, + }) + except FloodWaitError as e: + logger.warning(f"Telegram flood wait: {e.seconds}s for channel {channel}") + except (ChannelPrivateError, UsernameNotOccupiedError, UsernameInvalidError): + logger.debug(f"Channel {channel} not accessible, skipping") + except Exception as e: + logger.error(f"Error searching Telegram channel {channel}: {e}") + return results + + async def _search_all_channels(self, query: str, limit_per_channel: int = 5) -> List[dict]: + """Search all configured channels.""" + client = self._get_client() + async with client: + all_results = [] + for channel in self.channels: + results = await self._search_channel(channel, query, limit_per_channel) + all_results.extend(results) + # Sort by views descending + all_results.sort(key=lambda x: x["views"], reverse=True) + return all_results + + def _format_report(self, query: str, messages: List[dict]) -> str: + """Format search results as a report string.""" + if not messages: + return f"No Telegram messages found for '{query}'." + + total_views = sum(m["views"] for m in messages) + lines = [ + f"--- Telegram Search Results for '{query}' ---", + f"Total Views in Sample: {total_views:,}", + f"Channels Searched: {', '.join(self.channels)}", + "Messages:", + ] + + for idx, msg in enumerate(messages): + text_preview = msg["text"][:200] + if len(msg["text"]) > 200: + text_preview += "..." + lines.append( + f'{idx + 1}. [{msg["channel"]}] ({msg["date"]}, ' + f'👁 {msg["views"]:,}): "{text_preview}"' + ) + + lines.append("-------------------------------------------") + return "\n".join(lines) + + def search_for_market(self, query: str, limit: int = 10) -> str: + """ + Search Telegram channels for messages relevant to a market. + + Args: + query: Search query + limit: Max total messages to return + + Returns: + Formatted report string + """ + if not self.is_available(): + return "Telegram search unavailable: credentials not configured or telethon not installed." + + try: + # Calculate per-channel limit + limit_per_channel = max(3, limit // len(self.channels)) + + # Run async search — handle both sync and async calling contexts + coro = self._search_all_channels(query, limit_per_channel) + try: + loop = asyncio.get_running_loop() + # Already inside an async event loop — use a new thread + import concurrent.futures + with concurrent.futures.ThreadPoolExecutor() as pool: + messages = pool.submit( + asyncio.run, coro + ).result(timeout=30) + except RuntimeError: + # No running loop — safe to use run_until_complete + loop = asyncio.get_event_loop() + messages = loop.run_until_complete(coro) + + # Trim to total limit + messages = messages[:limit] + return self._format_report(query, messages) + + except Exception as e: + logger.error(f"Telegram search failed: {e}") + return f"Telegram search failed: {str(e)}" diff --git a/src/services/tools.py b/src/services/tools.py new file mode 100644 index 0000000..b29725d --- /dev/null +++ b/src/services/tools.py @@ -0,0 +1,467 @@ +""" +Tool registry for LLM function calling. + +Each tool is a callable that the LLM can invoke on demand. +Tools are registered with their OpenAI-compatible function schema +and an executor function that performs the actual work. +""" +import json +import logging +from dataclasses import dataclass +from typing import Callable, Dict, List + +from src.services.twitter_search import TwitterSearchService +from src.services.web_search import WebSearchService +from src.services.coingecko import CoinGeckoService +from src.services.fred import FREDService +from src.services.polygon import PolygonService +from src.services.congress import CongressService +from src.services.defillama import DefiLlamaService +from src.services.etherscan import EtherscanService +from src.services.telegram_search import TelegramSearchService + +logger = logging.getLogger(__name__) + + +@dataclass +class Tool: + """A tool available to the LLM.""" + name: str + description: str + parameters: dict # JSON Schema for parameters + execute: Callable[..., str] # (kwargs) -> result string + + +class ToolRegistry: + """ + Registry of tools available for LLM function calling. + + Usage: + registry = ToolRegistry(twitter_api_key="...", tavily_api_key="...") + schemas = registry.openai_tool_schemas() # pass to LLM + result = registry.call("search_twitter", query="Bitcoin") # execute + """ + + def __init__( + self, + twitter_api_key: str, + tavily_api_key: str, + fred_api_key: str = "", + polygon_api_key: str = "", + congress_api_key: str = "", + etherscan_api_key: str = "", + serper_api_key: str = "", + telegram_api_id: str = "", + telegram_api_hash: str = "", + telegram_session_string: str = "", + telegram_channels: str = "", + ): + self._tools: Dict[str, Tool] = {} + + # -- Twitter search -- + twitter = TwitterSearchService(api_key=twitter_api_key) + if twitter.is_available(): + self._register(Tool( + name="search_twitter", + description=( + "Search Twitter/X for real-time social sentiment, KOL opinions, " + "and breaking news about a topic. Returns top and latest tweets " + "with engagement metrics. Best for: real-time sentiment, crypto " + "community reactions, political commentary, breaking news that " + "hasn't hit mainstream media yet." + ), + parameters={ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query (e.g. 'Bitcoin ETF', 'Trump indictment')", + }, + }, + "required": ["query"], + }, + execute=lambda query: twitter.search_for_market(query, limit=10), + )) + + # -- Telegram search (crypto channels) -- + telegram = TelegramSearchService( + api_id=telegram_api_id, + api_hash=telegram_api_hash, + session_string=telegram_session_string, + channels=telegram_channels.split(",") if telegram_channels.strip() else None, + ) + if telegram.is_available(): + self._register(Tool( + name="search_telegram", + description=( + "Search Telegram channels for recent messages about a topic. " + "Covers crypto (Whale Alert, WatcherGuru, CryptoVIPSignalTA) and " + "politics/geopolitics (Disclose.tv, DDGeopolitics, Intel Slava, " + "PoliticsForAll, Trump, TheScrollOfBenjamin). " + "Returns messages with view counts. " + "Best for: US politics, Trump news, approval ratings, elections, " + "geopolitics, military conflicts, Russia/Ukraine, Middle East, " + "macro economics, crypto news, whale transfers, and breaking news." + ), + parameters={ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query (e.g. 'MegaETH launch', 'Solana outage')", + }, + }, + "required": ["query"], + }, + execute=lambda query: telegram.search_for_market(query, limit=10), + )) + + # -- Web search (Tavily -> Serper -> DuckDuckGo fallback) -- + web_search = WebSearchService( + tavily_api_key=tavily_api_key, + serper_api_key=serper_api_key, + ) + if web_search.is_available(): + self._register(Tool( + name="search_web", + description=( + "Search the web for recent news articles, analysis, and factual " + "information about a topic. Returns article summaries with sources. " + "Best for: verifying events, finding official announcements, " + "regulatory news, earnings reports, court rulings, legislation status." + ), + parameters={ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query for news and analysis", + }, + }, + "required": ["query"], + }, + execute=lambda query: web_search.search_for_market(query, max_results=5), + )) + + # -- CoinGecko crypto data -- + coingecko = CoinGeckoService() + self._register(Tool( + name="get_crypto_price", + description=( + "Get real-time cryptocurrency price, 24h/7d/30d change, market cap, " + "volume, and ATH data. Accepts ticker symbols (BTC, ETH, SOL) or " + "CoinGecko IDs. Best for: any market involving crypto price targets " + "(e.g. 'Will BTC hit $100k'), crypto market conditions." + ), + parameters={ + "type": "object", + "properties": { + "coin": { + "type": "string", + "description": "Ticker symbol (BTC, ETH, SOL) or CoinGecko coin ID", + }, + }, + "required": ["coin"], + }, + execute=lambda coin: coingecko.get_price(coin), + )) + + self._register(Tool( + name="get_crypto_market_overview", + description=( + "Get global crypto market overview: total market cap, 24h change, " + "BTC/ETH dominance, trading volume. Best for: understanding overall " + "crypto market sentiment and conditions." + ), + parameters={ + "type": "object", + "properties": {}, + }, + execute=lambda: coingecko.get_market_overview(), + )) + + # -- FRED macroeconomic data -- + fred = FREDService(api_key=fred_api_key) + if fred.is_available(): + self._register(Tool( + name="get_economic_data", + description=( + "Get macroeconomic data from FRED (Federal Reserve). Supports " + "common names: fed_rate, cpi, inflation, unemployment, gdp, " + "oil_price, wti, brent, gold, vix, sp500, yield_curve, " + "jobless_claims, 10y_treasury, 2y_treasury, dollar_index. " + "Also accepts any FRED series ID (e.g. FEDFUNDS, UNRATE). " + "Returns recent data points with trend. Best for: Fed policy " + "markets, inflation bets, employment data, oil/commodity prices, " + "recession indicators." + ), + parameters={ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": ( + "Common name (fed_rate, cpi, unemployment, oil_price, " + "vix, gold, sp500) or FRED series ID" + ), + }, + }, + "required": ["query"], + }, + execute=lambda query: fred.get_series(query), + )) + + # -- Polygon.io financial data -- + polygon = PolygonService(api_key=polygon_api_key) + if polygon.is_available(): + self._register(Tool( + name="get_stock_price", + description=( + "Get real-time stock/ETF price snapshot from Polygon.io. " + "Includes price, daily change, volume, day range. " + "Examples: AAPL, TSLA, GS, META, SPY, QQQ, GLD, USO. " + "Best for: markets involving specific company events " + "(IPOs, earnings, lawsuits), sector ETFs, gold/oil ETFs." + ), + parameters={ + "type": "object", + "properties": { + "ticker": { + "type": "string", + "description": "Ticker symbol (e.g. AAPL, TSLA, GS, SPY, GLD)", + }, + }, + "required": ["ticker"], + }, + execute=lambda ticker: polygon.get_ticker_snapshot(ticker), + )) + + self._register(Tool( + name="get_stock_news", + description=( + "Get recent news articles for a stock/company from Polygon.io. " + "Returns headlines, sources, and summaries. " + "Best for: company-specific events, earnings surprises, " + "M&A rumors, regulatory actions, CEO statements." + ), + parameters={ + "type": "object", + "properties": { + "ticker": { + "type": "string", + "description": "Stock ticker (e.g. AAPL, TSLA, GS)", + }, + }, + "required": ["ticker"], + }, + execute=lambda ticker: polygon.get_market_news(ticker), + )) + + # -- Congress.gov legislative data -- + congress_svc = CongressService(api_key=congress_api_key) + if congress_svc.is_available(): + self._register(Tool( + name="get_bill_status", + description=( + "Get status of a specific U.S. Congressional bill. " + "Requires congress number (e.g. 119), bill type (hr, s, hjres, sjres), " + "and bill number. Returns sponsor, cosponsors, latest action, " + "committee referrals. Best for: markets about specific legislation " + "(TikTok ban, crypto regulation, immigration reform, tax bills)." + ), + parameters={ + "type": "object", + "properties": { + "congress": { + "type": "integer", + "description": "Congress number (119 for 2025-2026)", + }, + "bill_type": { + "type": "string", + "description": "Bill type: hr (House), s (Senate), hjres, sjres", + }, + "bill_number": { + "type": "integer", + "description": "Bill number", + }, + }, + "required": ["congress", "bill_type", "bill_number"], + }, + execute=lambda congress, bill_type, bill_number: congress_svc.get_bill_status( + congress, bill_type, bill_number + ), + )) + + self._register(Tool( + name="get_recent_legislation", + description=( + "Get recently updated U.S. Congressional bills. " + "Returns latest bills with their current status and actions. " + "Best for: understanding current legislative activity, " + "political markets about government actions, policy changes." + ), + parameters={ + "type": "object", + "properties": {}, + }, + execute=lambda: congress_svc.search_bills("", limit=5), + )) + + # -- DeFiLlama (DeFi protocol data, no API key needed) -- + defillama = DefiLlamaService() + self._register(Tool( + name="get_protocol_tvl", + description=( + "Get DeFi protocol TVL (Total Value Locked), TVL changes (1h/24h/7d), " + "and chain breakdown from DeFiLlama. Accepts protocol name or slug " + "(e.g. 'aave', 'uniswap', 'lido', 'eigenlayer'). " + "Best for: token launch FDV markets, DeFi protocol health, " + "evaluating project fundamentals before/after token launch." + ), + parameters={ + "type": "object", + "properties": { + "protocol": { + "type": "string", + "description": "Protocol name or slug (e.g. 'aave', 'uniswap', 'megaeth')", + }, + }, + "required": ["protocol"], + }, + execute=lambda protocol: defillama.get_protocol_tvl(protocol), + )) + + self._register(Tool( + name="get_token_unlocks", + description=( + "Get token unlock/vesting schedule for a DeFi protocol from DeFiLlama. " + "Shows allocation categories and upcoming unlock events. " + "Best for: understanding token supply dynamics, evaluating FDV markets, " + "predicting sell pressure from upcoming unlocks." + ), + parameters={ + "type": "object", + "properties": { + "protocol": { + "type": "string", + "description": "Protocol name or slug (e.g. 'arbitrum', 'optimism', 'eigenlayer')", + }, + }, + "required": ["protocol"], + }, + execute=lambda protocol: defillama.get_token_unlocks(protocol), + )) + + self._register(Tool( + name="get_protocol_revenue", + description=( + "Get DeFi protocol fees and revenue (24h/7d/30d/all-time) from DeFiLlama. " + "Best for: evaluating protocol fundamentals, comparing revenue vs FDV, " + "assessing if a token launch valuation is justified." + ), + parameters={ + "type": "object", + "properties": { + "protocol": { + "type": "string", + "description": "Protocol name or slug (e.g. 'aave', 'uniswap', 'gmx')", + }, + }, + "required": ["protocol"], + }, + execute=lambda protocol: defillama.get_protocol_revenue(protocol), + )) + + # -- Etherscan (on-chain data) -- + etherscan = EtherscanService(api_key=etherscan_api_key) + if etherscan.is_available(): + self._register(Tool( + name="get_wallet_transfers", + description=( + "Get recent ERC-20 token transfers (USDC, USDT, WETH, DAI) for an " + "Ethereum wallet address from Etherscan. Shows direction (IN/OUT), " + "amount, counterparty, and flags large transfers (>$10k). " + "Best for: checking if a Polymarket whale recently received large " + "USDC deposits (funding for trades), tracking wallet activity." + ), + parameters={ + "type": "object", + "properties": { + "address": { + "type": "string", + "description": "Ethereum wallet address (0x...)", + }, + "token": { + "type": "string", + "description": "Token to track: USDC, USDT, WETH, or DAI (default: USDC)", + }, + }, + "required": ["address"], + }, + execute=lambda address, token="USDC": etherscan.get_wallet_token_transfers(address, token), + )) + + self._register(Tool( + name="get_contract_info", + description=( + "Check if an Ethereum address is a smart contract, when it was created, " + "its name and verification status from Etherscan. " + "Best for: verifying if a crypto project has deployed contracts, " + "checking contract activity for token launch markets." + ), + parameters={ + "type": "object", + "properties": { + "address": { + "type": "string", + "description": "Ethereum contract address (0x...)", + }, + }, + "required": ["address"], + }, + execute=lambda address: etherscan.get_contract_info(address), + )) + + def _register(self, tool: Tool): + self._tools[tool.name] = tool + logger.info(f"Registered tool: {tool.name}") + + @property + def available_tools(self) -> List[str]: + return list(self._tools.keys()) + + def openai_tool_schemas(self) -> List[dict]: + """Return tool schemas in OpenAI function-calling format.""" + return [ + { + "type": "function", + "function": { + "name": tool.name, + "description": tool.description, + "parameters": tool.parameters, + }, + } + for tool in self._tools.values() + ] + + def call(self, name: str, **kwargs) -> str: + """ + Execute a tool by name. + + Returns the tool's string result, or an error message if the tool + is not found or execution fails. + """ + tool = self._tools.get(name) + if not tool: + msg = f"Tool '{name}' not found. Available: {self.available_tools}" + logger.error(msg) + return msg + + try: + result = tool.execute(**kwargs) + logger.info(f"Tool {name} executed successfully ({len(result)} chars)") + return result + except Exception as e: + msg = f"Tool '{name}' failed: {e}" + logger.error(msg) + return msg diff --git a/src/services/trade_monitor.py b/src/services/trade_monitor.py new file mode 100644 index 0000000..2d6077d --- /dev/null +++ b/src/services/trade_monitor.py @@ -0,0 +1,698 @@ +""" +Trade monitoring service — RTDS WebSocket architecture. + +Single WebSocket connection receives ALL trades in real-time from +Polymarket RTDS (wss://ws-live-data.polymarket.com). + +For each incoming trade: +1. Record for cluster detection (anomaly detector) +2. Dedup by transaction hash +3. Filter: whale pre-filter (price range, size, conviction) +4. Enrich: trader ranking + history → anomaly score +5. If score passes threshold → full enrichment + LLM callback + +Replaces the previous per-market HTTP polling architecture. +""" +import asyncio +import json +import logging +import math +import time as _time +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Optional, Set, Callable, Awaitable + +import httpx + +from src.config import get_settings +from src.models.market import Market, TrendingMarket +from src.models.trade import ( + TradeActivity, WhaleTrade, TraderRanking, TraderHistory, + EventPosition, MarketTopTrader, +) +from src.services.anomaly_detector import AnomalyDetector +from src.services.rtds_client import RTDSClient +from src.utils.http import get_async_client + +logger = logging.getLogger(__name__) + +# Gamma API for fetching latest market prices +GAMMA_API_URL = "https://gamma-api.polymarket.com/markets" + +# File to persist processed transaction hashes +PROCESSED_TXNS_FILE = Path(__file__).parent.parent.parent / "data" / "processed_transactions.json" + + +class TradeMonitor: + """ + Monitors Polymarket markets for large trades via RTDS WebSocket. + + Architecture: single WebSocket connection → filter → enrich → callback. + """ + + def __init__( + self, + on_whale_detected: Optional[Callable[[WhaleTrade], Awaitable[None]]] = None, + ): + self.settings = get_settings() + + # RTDS WebSocket client (created in run()) + self._rtds: Optional[RTDSClient] = None + + # HTTP client for enrichment API calls (trader ranking, history, etc.) + self.data_api_url = "https://data-api.polymarket.com" + self.leaderboard_endpoint = f"{self.data_api_url}/v1/leaderboard" + self.trades_endpoint = f"{self.data_api_url}/trades" + self._client = get_async_client( + timeout=httpx.Timeout(30.0, pool=120.0), + limits=httpx.Limits( + max_connections=50, + max_keepalive_connections=20, + keepalive_expiry=30, + ), + ) + + # Cache for trader rankings to avoid repeated API calls + self._trader_ranking_cache: Dict[str, TraderRanking] = {} + + # Markets being monitored: condition_id -> Market + # Used for enrichment (market question, description, etc.) + self._monitored_markets: Dict[str, Market] = {} + # condition_id -> market_id mapping + self._condition_to_market_id: Dict[str, str] = {} + + # Track processed transactions to avoid duplicates + self._processed_txns: Set[str] = set() + self._load_processed_txns() + + # Anomaly detector for multi-dimensional scoring + self._anomaly_detector = AnomalyDetector() + + # Callback for whale detection + self._on_whale_detected = on_whale_detected + + # Control flag + self._running = False + + # Flag to suppress alerts during initial warmup + self._warmup_complete = False + self._warmup_seconds = 10 # seconds to collect baseline before alerting + + # ================================================================ + # Persistence + # ================================================================ + + def _load_processed_txns(self): + """Load processed transaction hashes from JSON file.""" + try: + if PROCESSED_TXNS_FILE.exists(): + with open(PROCESSED_TXNS_FILE, "r") as f: + data = json.load(f) + self._processed_txns = set(data.get("transactions", [])) + logger.info(f"Loaded {len(self._processed_txns)} processed transactions from file") + except json.JSONDecodeError as e: + logger.warning(f"Corrupted JSON file, backing up and starting fresh: {e}") + if PROCESSED_TXNS_FILE.exists(): + backup_file = PROCESSED_TXNS_FILE.with_suffix('.json.bak') + PROCESSED_TXNS_FILE.rename(backup_file) + logger.info(f"Backed up corrupted file to {backup_file}") + self._processed_txns = set() + except Exception as e: + logger.warning(f"Failed to load processed transactions: {e}") + self._processed_txns = set() + + def _save_processed_txns(self): + """Save processed transaction hashes to JSON file.""" + try: + PROCESSED_TXNS_FILE.parent.mkdir(parents=True, exist_ok=True) + with open(PROCESSED_TXNS_FILE, "w") as f: + json.dump({ + "transactions": list(self._processed_txns), + "count": len(self._processed_txns), + "last_updated": datetime.now().isoformat() + }, f, indent=2) + logger.debug(f"Saved {len(self._processed_txns)} processed transactions to file") + except Exception as e: + logger.warning(f"Failed to save processed transactions: {e}") + + async def close(self): + """Cleanup resources.""" + self._save_processed_txns() + await self._client.aclose() + + # ================================================================ + # Market list management + # ================================================================ + + def set_monitored_markets(self, markets: List[TrendingMarket]): + """Update the list of markets to monitor.""" + self._monitored_markets = {} + self._condition_to_market_id = {} + for tm in markets: + m = tm.market + if m.id and m.condition_id: + self._monitored_markets[m.condition_id] = m + self._condition_to_market_id[m.condition_id] = m.id + logger.info(f"Now monitoring {len(self._monitored_markets)} markets") + + def set_tiered_markets(self, tiers: dict[str, list]) -> None: + """Set markets from tiered scan (same interface as before).""" + self._monitored_markets = {} + self._condition_to_market_id = {} + + for tier_name, markets in tiers.items(): + for tm in markets: + m = tm.market + if m.id and m.condition_id: + self._monitored_markets[m.condition_id] = m + self._condition_to_market_id[m.condition_id] = m.id + + total = len(self._monitored_markets) + tier_counts = {k: len(v) for k, v in tiers.items()} + logger.info(f"Tiered monitoring: {tier_counts}, total={total}") + + # ================================================================ + # RTDS trade handler (core of the new architecture) + # ================================================================ + + async def _on_rtds_trade(self, activity: TradeActivity) -> None: + """ + Called for every trade received from RTDS WebSocket. + + This replaces the per-market polling loop. + """ + condition_id = activity.condition_id + + # Look up market info (enrichment data) + market = self._monitored_markets.get(condition_id) + market_id = self._condition_to_market_id.get(condition_id, "") + + # Record every trade for cluster detection (even unmonitored markets) + if market_id: + self._anomaly_detector.record_trade(activity, market_id) + + # Dedup by transaction hash + outcome (same tx can have multiple fills) + dedup_key = f"{activity.transaction_hash}_{activity.outcome}_{activity.size}" + if dedup_key in self._processed_txns: + return + self._processed_txns.add(dedup_key) + + # Skip unmonitored markets + if not market: + return + + # Skip during warmup period (avoid alerting on historical trades) + if not self._warmup_complete: + return + + # Only track BUY trades (new positions) + if activity.side != "BUY": + return + + # Whale pre-filter + if not self._is_whale_trade(activity, market=market): + return + + # Handle whale (enrich + score + callback) + asyncio.create_task(self._handle_whale(activity, market_id, market)) + + # ================================================================ + # Whale detection (unchanged from original) + # ================================================================ + + def _is_whale_trade(self, activity: TradeActivity, market: Optional[Market] = None) -> bool: + """ + Multi-layer pre-filter mirroring options flow SignalFilter._check_signal. + + Filter chain (early rejection): + 1. Price range — like moneyness filter (OTM/ITM range) + 2. Direction — BUY only (like enabled direction_filters) + 3. Resolution window — like DTE filter (3-60 days sweet spot) + 4. Size — like premium filter ($250K+ minimum) + 5. Dynamic size — like dynamic_premium (base × √(vol / baseline)) + 5.5 Normalized size — like normalized_premium (usdc / √(vol)) + 6. Signal strength — like ask_ratio filter (conviction check) + """ + # --- 1. Price range --- + if not (self.settings.min_price <= activity.price <= self.settings.max_price): + return False + + # --- 2. Direction: BUY only --- + # Already enforced upstream + + # --- 3. Resolution window --- + if market and market.end_date: + try: + end_dt = datetime.fromisoformat(market.end_date.replace("Z", "+00:00")) + now_dt = datetime.utcnow().replace(tzinfo=end_dt.tzinfo) if end_dt.tzinfo else datetime.utcnow() + hours_to_resolution = max(0, (end_dt - now_dt).total_seconds() / 3600) + if hours_to_resolution < 3: + return False + if hours_to_resolution > 180 * 24: + return False + except (ValueError, TypeError): + pass + + # --- 4. Size --- + if activity.usdc_size < 3_000: + return False + + # --- 5. Dynamic size --- + base_size = 5_000.0 + baseline_volume = 1_000_000.0 + + if market and market.volume > 0: + threshold = base_size * math.sqrt(market.volume / baseline_volume) + threshold = max(3_000.0, min(threshold, 50_000.0)) + else: + threshold = base_size + + if activity.usdc_size < threshold: + return False + + # --- 5.5 Normalized size (like normalized_premium) --- + # usdc_size / √(volume) makes signals comparable across market sizes. + # A $5K trade in a $50K market is far more significant than $20K in a $10M market. + if market and market.volume > 0: + normalized = activity.usdc_size / math.sqrt(market.volume) + # Minimum normalized threshold: filters out trades that are trivial + # relative to market size (calibrated: $5K in a $1M market → 5.0) + if normalized < 1.5: + return False + + # --- 6. Signal strength --- + if market and market.outcome_prices: + if activity.outcome == "Yes": + market_mid = market.outcome_prices[0] + elif len(market.outcome_prices) > 1: + market_mid = market.outcome_prices[1] + else: + market_mid = 1.0 - market.outcome_prices[0] + + if activity.price < market_mid + 0.01: + return False + + return True + + async def _handle_whale(self, activity: TradeActivity, market_id: str, market: Market): + """ + Handle a single whale trade: + 1. Fetch trader info (ranking + history) for anomaly scoring + 2. Compute multi-dimensional anomaly score as pre-filter + 3. If score passes threshold, fetch full enrichment data and fire LLM callback + """ + try: + # Phase 1: Quick fetch — ranking + history for anomaly scoring + trader_ranking, trader_history = await asyncio.gather( + self.fetch_trader_ranking(activity.proxy_wallet), + self.fetch_trader_history(activity.proxy_wallet), + ) + + # Phase 2: Anomaly scoring + should_analyze, score, breakdown = self._anomaly_detector.should_analyze( + activity, market=market, trader_history=trader_history, + market_id=market_id, + ) + + rank_str = f"(Rank #{trader_ranking.rank})" if trader_ranking and trader_ranking.rank else "(Unranked)" + breakdown_short = " | ".join(f"{k}={v:.2f}" for k, v in breakdown.items()) + + if not should_analyze: + logger.info( + f"⚪ Whale below threshold: ${activity.usdc_size:,.2f} " + f"BUY {activity.outcome} @ {activity.price:.4f} {rank_str} " + f"score={score:.2f} [{breakdown_short}] — skipped LLM" + ) + return + + logger.info( + f"🐋 Whale trade detected! ${activity.usdc_size:,.2f} " + f"BUY {activity.outcome} @ {activity.price:.4f} {rank_str} " + f"score={score:.2f} [{breakdown_short}] on '{market.question[:50]}...'" + ) + + # Phase 3: Full enrichment + event_positions, (top_buyers, top_sellers) = await asyncio.gather( + self.fetch_whale_event_positions( + activity.proxy_wallet, + activity.event_slug, + market.condition_id or "", + ), + self.fetch_market_top_traders( + market_id, condition_id=market.condition_id or "", + outcome_prices=market.outcome_prices, + ), + ) + + whale_trade = WhaleTrade( + id=f"{market_id}_{activity.transaction_hash}", + trade=activity, + market_id=market_id, + market_question=market.question, + market_description=market.description, + market_outcomes=market.outcomes, + market_outcome_prices=market.outcome_prices, + trader_ranking=trader_ranking, + trader_history=trader_history, + whale_event_positions=event_positions, + market_top_buyers=top_buyers, + market_top_sellers=top_sellers, + ) + + # Fire callback (LLM report generation) + if self._on_whale_detected: + await self._on_whale_detected(whale_trade) + + except Exception as e: + logger.error(f"Error handling whale trade in {market_id}: {e}") + + # ================================================================ + # Enrichment API calls (unchanged — still uses HTTP) + # ================================================================ + + async def fetch_trader_ranking(self, wallet_address: str) -> Optional[TraderRanking]: + """Fetch trader ranking from the leaderboard API.""" + if not wallet_address: + return None + + if wallet_address in self._trader_ranking_cache: + return self._trader_ranking_cache[wallet_address] + + try: + params = { + "user": wallet_address, + "timePeriod": "ALL", + "orderBy": "PNL", + } + response = await self._client.get(self.leaderboard_endpoint, params=params) + response.raise_for_status() + data = response.json() + + if data and len(data) > 0: + user_data = data[0] + ranking = TraderRanking( + rank=user_data.get("rank"), + pnl=float(user_data.get("pnl", 0) or 0), + volume=float(user_data.get("vol", 0) or 0), + user_name=user_data.get("userName"), + profile_image=user_data.get("profileImage"), + verified=bool(user_data.get("verifiedBadge")), + time_period="ALL", + ) + self._trader_ranking_cache[wallet_address] = ranking + logger.debug(f"Fetched ranking for {wallet_address}: #{ranking.rank}") + return ranking + + return None + + except httpx.HTTPError as e: + logger.debug(f"HTTP error fetching ranking for {wallet_address}: {e}") + return None + except Exception as e: + logger.debug(f"Error fetching ranking for {wallet_address}: {e}") + return None + + async def fetch_trader_history(self, wallet_address: str) -> Optional[TraderHistory]: + """Fetch trader's recent trading history.""" + if not wallet_address: + return None + + try: + params = { + "user": wallet_address, + "limit": 100, + } + response = await self._client.get(self.trades_endpoint, params=params) + response.raise_for_status() + data = response.json() + + if not data: + return None + + total_trades = len(data) + total_volume = 0.0 + large_trades_count = 0 + recent_markets: Set[str] = set() + recent_trades = [] + + for trade in data: + usdc_size = float(trade.get("usdcSize", 0) or 0) + if usdc_size == 0: + size = float(trade.get("size", 0) or 0) + price = float(trade.get("price", 0) or 0) + usdc_size = size * price + + total_volume += usdc_size + + if usdc_size >= 5000: + large_trades_count += 1 + recent_trades.append({ + "side": trade.get("side", ""), + "usdc_size": usdc_size, + "price": float(trade.get("price", 0) or 0), + "title": trade.get("title", trade.get("marketTitle", "")), + "timestamp": trade.get("timestamp", 0), + }) + + title = trade.get("title", trade.get("marketTitle", "")) + if title: + recent_markets.add(title[:50]) + + avg_trade_size = total_volume / total_trades if total_trades > 0 else 0 + recent_trades.sort(key=lambda x: x["usdc_size"], reverse=True) + + return TraderHistory( + total_trades=total_trades, + total_volume=total_volume, + avg_trade_size=avg_trade_size, + large_trades_count=large_trades_count, + recent_markets=list(recent_markets)[:10], + recent_trades=recent_trades[:10], + ) + + except httpx.HTTPError as e: + logger.debug(f"HTTP error fetching history for {wallet_address}: {e}") + return None + except Exception as e: + logger.debug(f"Error fetching history for {wallet_address}: {e}") + return None + + async def fetch_whale_event_positions( + self, + wallet_address: str, + event_slug: str, + current_condition_id: str, + ) -> List[EventPosition]: + """Fetch the whale's positions across all markets in the same event.""" + if not wallet_address or not event_slug: + return [] + + try: + response = await self._client.get( + f"{self.data_api_url}/positions", + params={"user": wallet_address}, + ) + response.raise_for_status() + all_positions = response.json() + if not all_positions: + return [] + + result = [] + for pos in all_positions: + pos_event_slug = pos.get("eventSlug", "") + pos_condition_id = pos.get("conditionId", "") + + if pos_event_slug != event_slug: + continue + if pos_condition_id == current_condition_id: + continue + + size = float(pos.get("size", 0) or 0) + if size == 0: + continue + + outcome = pos.get("outcome", "Yes") + avg_price = float(pos.get("avgPrice", 0) or 0) + cur_price = float(pos.get("curPrice", 0) or 0) + current_value = float(pos.get("currentValue", 0) or 0) + initial_value = float(pos.get("initialValue", 0) or 0) + cash_pnl = float(pos.get("cashPnl", 0) or 0) + title = pos.get("title", "") + + if outcome == "Yes": + side_summary = f"Holding Yes {size:,.0f} tokens @ avg {avg_price:.2%}, current {cur_price:.2%}" + else: + side_summary = f"Holding No {size:,.0f} tokens @ avg {avg_price:.2%}, current {cur_price:.2%}" + + result.append(EventPosition( + market_question=title, + condition_id=pos_condition_id, + outcome=outcome, + size=size, + avg_price=avg_price, + current_price=cur_price, + current_value=current_value, + initial_value=initial_value, + pnl=cash_pnl, + side_summary=side_summary, + )) + + result.sort(key=lambda x: x.current_value, reverse=True) + logger.debug( + f"Found {len(result)} event positions for {wallet_address} " + f"in event '{event_slug}'" + ) + return result + + except Exception as e: + logger.warning(f"Error fetching whale event positions: {e}") + return [] + + async def fetch_market_top_traders( + self, market_id: str, condition_id: str = "", + outcome_prices: Optional[List[float]] = None, top_n: int = 5, + ) -> tuple[List[MarketTopTrader], List[MarketTopTrader]]: + """Fetch top holders (bulls and bears) for a market.""" + if not condition_id: + return [], [] + + yes_price = outcome_prices[0] if outcome_prices and len(outcome_prices) > 0 else 0.5 + no_price = outcome_prices[1] if outcome_prices and len(outcome_prices) > 1 else 0.5 + + try: + response = await self._client.get( + f"{self.data_api_url}/holders", + params={"market": condition_id, "limit": top_n}, + ) + response.raise_for_status() + data = response.json() + if not data: + return [], [] + + top_buyers = [] + top_sellers = [] + + for token_group in data: + holders = token_group.get("holders", []) + if not holders: + continue + + outcome_index = holders[0].get("outcomeIndex", 0) + token_price = yes_price if outcome_index == 0 else no_price + + for h in holders[:top_n]: + wallet = h.get("proxyWallet", "") + name = h.get("name") or h.get("pseudonym") or None + amount = float(h.get("amount", 0) or 0) + usd_value = amount * token_price + + trader = MarketTopTrader( + wallet=wallet, + name=name, + net_volume_usd=usd_value, + trade_count=0, + ) + + if outcome_index == 0: + top_buyers.append(trader) + else: + top_sellers.append(trader) + + # Fetch rankings in parallel + ranking_tasks = [] + trader_refs = [] + for t in top_buyers + top_sellers: + ranking_tasks.append(self.fetch_trader_ranking(t.wallet)) + trader_refs.append(t) + + if ranking_tasks: + rankings = await asyncio.gather(*ranking_tasks, return_exceptions=True) + for trader, ranking in zip(trader_refs, rankings): + if isinstance(ranking, TraderRanking) and ranking: + trader.rank = ranking.rank + trader.pnl = ranking.pnl + if ranking.user_name: + trader.name = ranking.user_name + + logger.debug( + f"Market {market_id}: {len(top_buyers)} top Yes holders, " + f"{len(top_sellers)} top No holders" + ) + return top_buyers, top_sellers + + except Exception as e: + logger.warning(f"Error fetching top holders for {market_id}: {e}") + return [], [] + + # ================================================================ + # Main run loop + # ================================================================ + + async def run(self): + """ + Start the RTDS-based monitoring loop. + + Architecture: + - Single RTDS WebSocket receives ALL trades in real-time + - _on_rtds_trade filters and handles each trade + - Periodic persistence of processed transactions + """ + self._running = True + + # Create RTDS client with our trade handler + proxy = self.settings.http_proxy.strip() or None + self._rtds = RTDSClient(on_trade=self._on_rtds_trade, proxy=proxy) + + logger.info( + f"Starting RTDS trade monitor " + f"({len(self._monitored_markets)} monitored markets)" + ) + + # Start warmup timer — suppress alerts for first N seconds + # to avoid firing on trades already in the RTDS pipeline + async def warmup_timer(): + await asyncio.sleep(self._warmup_seconds) + self._warmup_complete = True + logger.info( + f"Warmup complete ({self._warmup_seconds}s). " + f"Now alerting on new whale trades." + ) + + warmup_task = asyncio.create_task(warmup_timer()) + + # Periodic persistence task + async def persistence_loop(): + while self._running: + await asyncio.sleep(60) + self._save_processed_txns() + # Trim processed txns set to prevent unbounded growth + if len(self._processed_txns) > 100_000: + # Keep only the most recent 50K (approximate — set is unordered, + # but old hashes won't repeat so trimming is safe) + excess = len(self._processed_txns) - 50_000 + for _ in range(excess): + self._processed_txns.pop() + logger.info(f"Trimmed processed txns to {len(self._processed_txns)}") + + persistence_task = asyncio.create_task(persistence_loop()) + + try: + # Run RTDS client (blocks until stop) + await self._rtds.run() + finally: + warmup_task.cancel() + persistence_task.cancel() + self._save_processed_txns() + + def stop(self): + """Stop the monitoring loop.""" + self._running = False + if self._rtds: + self._rtds.stop() + logger.info("Trade monitor stopping...") + + def clear_processed_transactions(self): + """Clear the processed transactions cache.""" + count = len(self._processed_txns) + self._processed_txns.clear() + logger.info(f"Cleared {count} processed transactions from cache") \ No newline at end of file diff --git a/src/services/trader_profiler.py b/src/services/trader_profiler.py new file mode 100644 index 0000000..e6b2048 --- /dev/null +++ b/src/services/trader_profiler.py @@ -0,0 +1,81 @@ +"""Trader profiler service - generates structured trader profiles for LLM consumption.""" +import json +import logging +from typing import Optional + +from src.models.trade import TraderRanking, TraderHistory + +logger = logging.getLogger(__name__) + + +class TraderProfiler: + """ + Generates structured trader profiles for LLM consumption. + + Only organizes raw data into a clean JSON structure. + All interpretation and judgment is left to the LLM. + """ + + def generate_profile( + self, + wallet_address: str, + ranking: Optional[TraderRanking], + history: Optional[TraderHistory], + ) -> dict: + """ + Generate a structured trader profile from raw data. + + Returns: + dict with raw trader data for LLM consumption + """ + # Ranking - raw numbers only + ranking_data = { + "rank": ranking.rank if ranking else None, + "pnl": ranking.pnl if ranking else None, + "total_volume": ranking.volume if ranking else None, + "verified": ranking.verified if ranking else False, + "username": ranking.user_name if ranking else None, + } + + # Trading behavior - raw numbers only + large_trade_ratio = 0.0 + if history and history.total_trades > 0: + large_trade_ratio = history.large_trades_count / history.total_trades + + behavior_data = { + "total_trades": history.total_trades if history else 0, + "total_volume": history.total_volume if history else 0.0, + "avg_trade_size": history.avg_trade_size if history else 0.0, + "large_trades_count": history.large_trades_count if history else 0, + "large_trade_ratio": round(large_trade_ratio, 3), + "active_markets": history.recent_markets[:5] if history and history.recent_markets else [], + } + + # Recent trades - raw data + recent_trades = [] + if history and history.recent_trades: + for t in history.recent_trades[:10]: + recent_trades.append({ + "side": t.get("side", ""), + "size_usd": t.get("usdc_size", 0), + "price": t.get("price", 0), + "market": t.get("title", "")[:50], + }) + + return { + "ranking": ranking_data, + "behavior": behavior_data, + "recent_trades": recent_trades, + } + + def format_profile_for_llm(self, profile: dict) -> str: + """Format the profile dict as JSON for LLM input.""" + profile_json = json.dumps(profile, ensure_ascii=False, indent=2) + + return f""" +### Trader Profile + +```json +{profile_json} +``` +""" diff --git a/src/services/twitter_search.py b/src/services/twitter_search.py new file mode 100644 index 0000000..b403424 --- /dev/null +++ b/src/services/twitter_search.py @@ -0,0 +1,220 @@ +"""Twitter search service for whale trade verification.""" +import os +import logging +from typing import Optional, Literal + +import requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +logger = logging.getLogger(__name__) + + +# ---------- Retry-enabled HTTP GET ---------- +_shared_session: Optional[requests.Session] = None + + +def robust_get(url: str, **kwargs) -> requests.Response: + """GET request with automatic retry (3 retries, exponential backoff).""" + global _shared_session + if _shared_session is None: + _shared_session = requests.Session() + retry = Retry( + total=3, + backoff_factor=0.5, + status_forcelist=(429, 500, 502, 503, 504), + allowed_methods=["GET"], + raise_on_status=False, + ) + adapter = HTTPAdapter(max_retries=retry) + _shared_session.mount("http://", adapter) + _shared_session.mount("https://", adapter) + kwargs.setdefault("timeout", 15) + return _shared_session.get(url, **kwargs) + +# Twitter module constants and helpers (extracted to avoid langchain @tool decorator issues) +TWITTER_API_KEY = os.getenv("TWITTER_API_KEY", "NONE") +BASE_URL = "https://api.twitterapi.io/twitter" +SEARCH_ENDPOINT = f"{BASE_URL}/tweet/advanced_search" +USER_TWEETS_ENDPOINT = f"{BASE_URL}/user/last_tweets" + + +def _parse_tweet_text(tweet_data: dict) -> Optional[dict]: + """Parse and format a single tweet.""" + try: + # API returns author (not user), with userName (not username) + author = tweet_data.get("author") or tweet_data.get("user") or {} + user = author.get("userName") or author.get("username") or "unknown" + text = tweet_data.get("text", "") + likes = tweet_data.get("likeCount") or tweet_data.get("favorite_count") or 0 + retweets = tweet_data.get("retweetCount") or tweet_data.get("retweet_count") or 0 + created_at = tweet_data.get("createdAt") or tweet_data.get("created_at") or "" + engagement = int(likes) + int(retweets) + return { + "user": user, + "text": text, + "engagement": engagement, + "time": created_at, + } + except Exception: + return None + + +def _format_tweets_report(title: str, parsed_tweets: list[dict], total_engagement: int) -> str: + """Format tweets list as a report.""" + report = [f"--- {title} ---"] + report.append(f"Total Engagement in Sample: {total_engagement} (Likes+RTs)") + report.append("Top Discussions:") + for idx, item in enumerate(parsed_tweets): + text_preview = item["text"][:200] + if len(item["text"]) > 200: + text_preview += "..." + report.append(f'{idx + 1}. @{item["user"]} (🔥{item["engagement"]}): "{text_preview}"') + report.append("-------------------------------------------") + return "\n".join(report) + + +class TwitterSearchService: + """ + Twitter search service for whale trade verification. + + Uses Twitter API for social sentiment search. + """ + + def __init__(self, api_key: Optional[str] = None): + """ + Initialize Twitter search service. + + Args: + api_key: Twitter API key. If not provided, reads from TWITTER_API_KEY env var. + """ + self.api_key = api_key or os.getenv("TWITTER_API_KEY", "") + if not self.api_key: + logger.warning("TWITTER_API_KEY not set. Twitter search will be disabled.") + + def _get_headers(self) -> dict[str, str]: + """Get request headers with API key.""" + return {"X-API-Key": self.api_key} + + def is_available(self) -> bool: + """Check if Twitter search is available (API key is set).""" + return bool(self.api_key and self.api_key.strip().upper() != "NONE") + + def search_tweets( + self, + query: str, + search_mode: Literal["top", "latest"] = "top", + limit: int = 10, + ) -> str: + """ + Search Twitter for tweets matching a query. + + Args: + query: Search query (e.g., "Trump", "Bitcoin", "Fed rate") + search_mode: "top" for most relevant, "latest" for most recent + limit: Number of tweets to return (1-20) + + Returns: + Formatted report of tweets with engagement metrics. + """ + if not self.is_available(): + return "Twitter search unavailable: TWITTER_API_KEY not configured." + + limit = max(1, min(limit, 20)) + query_type = "Top" if search_mode == "top" else "Latest" + + params = { + "query": query, + "queryType": query_type, + "limit": limit, + } + + try: + response = robust_get( + SEARCH_ENDPOINT, + params=params, + headers=self._get_headers(), + timeout=15, + ) + + if response.status_code != 200: + logger.error(f"Twitter API error: {response.status_code} - {response.text[:200]}") + return f"Twitter API Error: {response.status_code}" + + data = response.json() + tweets_raw = data.get("tweets", []) + + if not tweets_raw: + return f"No recent tweets found for '{query}'." + + # Parse tweets + parsed_tweets = [] + total_engagement = 0 + + for t in tweets_raw: + p = _parse_tweet_text(t) + if p: + parsed_tweets.append(p) + total_engagement += p["engagement"] + + mode_label = "Hot" if search_mode == "top" else "Latest" + return _format_tweets_report( + f"Twitter Search Results for '{query}' [{mode_label}]", + parsed_tweets, + total_engagement, + ) + + except Exception as e: + logger.error(f"Twitter search failed: {e}") + return f"Twitter search failed: {str(e)}" + + def search_for_market( + self, + market_question: str, + limit: int = 10, + ) -> str: + """ + Search Twitter for information relevant to a prediction market. + + Combines both TOP (importance/engagement) and LATEST (timeliness) results + to balance relevance and recency. + + Args: + market_question: The market question to search for + limit: Number of tweets per search mode (will search both top and latest) + + Returns: + Combined search results from both search modes. + """ + if not self.is_available(): + return "Twitter search unavailable: TWITTER_API_KEY not configured." + + results = [] + query = market_question[:100] # Limit query length for API + + # 1. Search TOP tweets - high engagement, represents importance + top_result = self.search_tweets(query, search_mode="top", limit=limit) + if "No recent tweets" not in top_result and "Error" not in top_result: + results.append("## Hot Tweets (High Engagement / Importance)\n" + top_result) + + # 2. Search LATEST tweets - real-time info, represents timeliness + latest_result = self.search_tweets(query, search_mode="latest", limit=limit) + if "No recent tweets" not in latest_result and "Error" not in latest_result: + results.append("## Latest Tweets (Real-Time / Timeliness)\n" + latest_result) + + if not results: + return f"No relevant tweets found for: {market_question[:50]}..." + + return "\n\n".join(results) + + +# Singleton instance +_twitter_service: Optional[TwitterSearchService] = None + + +def get_twitter_service() -> TwitterSearchService: + """Get the singleton Twitter search service instance.""" + global _twitter_service + if _twitter_service is None: + _twitter_service = TwitterSearchService() + return _twitter_service diff --git a/src/services/volatility_analyzer.py b/src/services/volatility_analyzer.py new file mode 100644 index 0000000..12a19fc --- /dev/null +++ b/src/services/volatility_analyzer.py @@ -0,0 +1,372 @@ +"""Volatility analyzer service - analyzes price volatility using AI to detect leading signals.""" +import json +import logging +import re +from datetime import datetime +from pathlib import Path +from typing import Optional + +from openai import OpenAI + +from src.config import get_settings +from src.models.leading_signal import LeadingSignal, SignalType +from src.services.price_monitor import VolatilityAlert +from src.services.twitter_search import TwitterSearchService +from src.prompts.volatility_analyzer import VolatilityAnalyzerPrompts + +logger = logging.getLogger(__name__) + +# Directory for storing leading signals dataset +LEADING_SIGNALS_DIR = Path(__file__).parent.parent.parent / "leading_signals" + + +class VolatilityAnalyzer: + """ + Analyzes price volatility events using LLM to detect "price leads news" signals. + + Uses Tavily web search and Twitter to verify whether a price movement + preceded public news, building a dataset of leading signals. + """ + + def __init__(self): + self.settings = get_settings() + + # Configure OpenAI-compatible API client + self.client = OpenAI( + base_url=self.settings.llm_base_url, + api_key=self.settings.llm_api_key, + ) + + self.prompts = VolatilityAnalyzerPrompts() + self.twitter_search = TwitterSearchService(api_key=self.settings.twitter_api_key) + from src.services.web_search import WebSearchService + self.web_search = WebSearchService( + tavily_api_key=self.settings.tavily_api_key, + serper_api_key=self.settings.serper_api_key, + ) + + # Ensure storage directory exists + LEADING_SIGNALS_DIR.mkdir(parents=True, exist_ok=True) + + 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: + 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_signal_type(self, type_str: str) -> SignalType: + """Parse signal type string to enum.""" + try: + return SignalType(type_str.upper()) + except ValueError: + return SignalType.SPECULATION + + def _store_leading_signal(self, signal: LeadingSignal) -> str: + """ + Store a leading signal to the dataset. + + Args: + signal: The leading signal to store + + Returns: + Path to the stored file + """ + # Create filename with timestamp and market info + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + market_slug = re.sub(r'[^\w\s-]', '', signal.market_question)[:40] + market_slug = re.sub(r'\s+', '_', market_slug) + + filename = f"{timestamp}_{signal.signal_type.value}_{market_slug}.json" + filepath = LEADING_SIGNALS_DIR / filename + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(signal.to_dict(), f, ensure_ascii=False, indent=2) + + return str(filepath) + + def _store_all_signals_index(self, signal: LeadingSignal) -> None: + """ + Append signal to the master index file for easy querying. + + Args: + signal: The signal to append + """ + index_file = LEADING_SIGNALS_DIR / "signals_index.jsonl" + + with open(index_file, 'a', encoding='utf-8') as f: + f.write(json.dumps(signal.to_dict(), ensure_ascii=False) + "\n") + + async def analyze_volatility(self, alert: VolatilityAlert) -> Optional[LeadingSignal]: + """ + Analyze a price volatility event to determine if it's a leading signal. + + Args: + alert: The volatility alert to analyze + + Returns: + LeadingSignal if analysis successful, None otherwise + """ + logger.info( + f"Analyzing volatility: {alert.market_question[:50]}... " + f"{alert.direction} {abs(alert.price_change_percent):.1%}" + ) + + # Search web (Tavily) for news verification + web_search_context = "" + if self.web_search.is_available(): + logger.info(f"Searching web for: {alert.market_question[:50]}...") + web_result = self.web_search.search_for_market( + market_question=alert.market_question, + max_results=5, + ) + if web_result and "unavailable" not in web_result.lower(): + web_search_context = web_result + logger.info("Web search (Tavily) completed") + + # Search Twitter for social sentiment + twitter_context = "" + if self.twitter_search.is_available(): + logger.info(f"Searching Twitter for: {alert.market_question[:50]}...") + twitter_result = self.twitter_search.search_for_market( + market_question=alert.market_question, + limit=10, + ) + if twitter_result and "unavailable" not in twitter_result.lower(): + twitter_context = twitter_result + logger.info("Twitter search completed") + + # Build prompts + system_prompt = self.prompts.system_prompt() + user_prompt = self.prompts.analyze_volatility( + market_question=alert.market_question, + price_change_percent=alert.price_change_percent, + direction=alert.direction, + start_price=alert.start_price, + end_price=alert.end_price, + window_seconds=alert.window_seconds, + detected_at=alert.detected_at, + twitter_context=twitter_context, + web_search_context=web_search_context, + ) + + try: + # Call LLM API + response = self.client.chat.completions.create( + model=self.settings.llm_model, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ], + ) + + analysis_text = response.choices[0].message.content + logger.debug(f"LLM response: {analysis_text[:500]}...") + + # Extract JSON from response + json_data = self._extract_json_from_response(analysis_text) + + if not json_data: + logger.warning("Could not parse LLM response as JSON") + return None + + # Create LeadingSignal from response + signal_id = f"vol_{alert.market_id}_{int(datetime.now().timestamp())}" + + signal = LeadingSignal( + id=signal_id, + market_id=alert.market_id, + market_question=alert.market_question, + price_change_percent=alert.price_change_percent, + direction=alert.direction, + start_price=alert.start_price, + end_price=alert.end_price, + window_seconds=alert.window_seconds, + detected_at=datetime.utcnow().isoformat(), + volatility_detected_at=alert.detected_at, + signal_type=self._parse_signal_type(json_data.get("signal_type", "SPECULATION")), + confidence=float(json_data.get("confidence", 0.0)), + is_leading_signal=bool(json_data.get("is_leading_signal", False)), + news_found=bool(json_data.get("news_found", False)), + earliest_news_time=json_data.get("earliest_news_time"), + key_news_headlines=json_data.get("key_news_headlines", []), + earliest_social_time=json_data.get("earliest_social_time"), + key_social_posts=json_data.get("key_social_posts", []), + time_advantage_minutes=int(json_data.get("time_advantage_minutes", 0)), + reasoning=str(json_data.get("reasoning", "")), + potential_information_source=str(json_data.get("potential_information_source", "")), + full_analysis=analysis_text, + ) + + # Store the signal + filepath = self._store_leading_signal(signal) + self._store_all_signals_index(signal) + + # Log result + if signal.is_leading_signal: + logger.warning( + f"🚨 LEADING SIGNAL DETECTED: {alert.market_question[:50]}... " + f"Time advantage: {signal.time_advantage_minutes} minutes" + ) + else: + logger.info( + f"Volatility analyzed: {signal.signal_type.value} " + f"(confidence: {signal.confidence:.1%})" + ) + + logger.info(f"Signal stored: {filepath}") + + return signal + + except Exception as e: + logger.error(f"Error analyzing volatility: {e}") + return None + + def format_signal_report(self, signal: LeadingSignal) -> str: + """ + Format a leading signal as a readable report. + + Args: + signal: The signal to format + + Returns: + Formatted report string + """ + direction_label = "Up" if signal.direction == "UP" else "Down" + signal_type_label = { + SignalType.LEADING_SIGNAL: "Leading Signal (Price Preceded News)", + SignalType.NEWS_DRIVEN: "News-Driven", + SignalType.SOCIAL_DRIVEN: "Social-Driven", + SignalType.SPECULATION: "Speculative Volatility", + } + + news_headlines = "\n".join([f" - {h}" for h in signal.key_news_headlines]) or " None" + social_posts = "\n".join([f" - {p}" for p in signal.key_social_posts]) or " None" + + report = f""" +{'='*70} +# Price Volatility Analysis Report +{'='*70} + +**Analysis Time**: {signal.detected_at} + +## Volatility Details + +| Field | Details | +|-------|---------| +| **Market** | {signal.market_question} | +| **Price Change** | {direction_label} {abs(signal.price_change_percent):.1%} | +| **Start Price** | {signal.start_price:.2%} | +| **End Price** | {signal.end_price:.2%} | +| **Time Window** | {signal.window_seconds // 60} min | + +{'='*70} +## Analysis Results +{'='*70} + +| Field | Result | +|-------|--------| +| **Signal Type** | {signal_type_label.get(signal.signal_type, 'Unknown')} | +| **Confidence** | {signal.confidence:.1%} | +| **Is Leading Signal** | {'Yes' if signal.is_leading_signal else 'No'} | +| **Time Advantage** | {signal.time_advantage_minutes} min | + +**Earliest News Time**: {signal.earliest_news_time or 'N/A'} +**Earliest Social Time**: {signal.earliest_social_time or 'N/A'} + +## Key News +{news_headlines} + +## Key Social Posts +{social_posts} + +## Reasoning +{signal.reasoning} + +## Suspected Information Source +{signal.potential_information_source or 'Unknown'} + +{'='*70} +{signal.full_analysis} +{'='*70} +""" + return report + + def get_leading_signals_stats(self) -> dict: + """ + Get statistics about collected leading signals. + + Returns: + Dictionary with stats + """ + index_file = LEADING_SIGNALS_DIR / "signals_index.jsonl" + + if not index_file.exists(): + return { + "total_signals": 0, + "leading_signals": 0, + "news_driven": 0, + "social_driven": 0, + "speculation": 0, + } + + stats = { + "total_signals": 0, + "leading_signals": 0, + "news_driven": 0, + "social_driven": 0, + "speculation": 0, + "avg_time_advantage_minutes": 0, + } + + time_advantages = [] + + try: + with open(index_file, 'r', encoding='utf-8') as f: + for line in f: + if line.strip(): + data = json.loads(line) + stats["total_signals"] += 1 + + signal_type = data.get("signal_type", "SPECULATION") + if signal_type == "LEADING_SIGNAL": + stats["leading_signals"] += 1 + time_advantages.append(data.get("time_advantage_minutes", 0)) + elif signal_type == "NEWS_DRIVEN": + stats["news_driven"] += 1 + elif signal_type == "SOCIAL_DRIVEN": + stats["social_driven"] += 1 + else: + stats["speculation"] += 1 + + if time_advantages: + stats["avg_time_advantage_minutes"] = sum(time_advantages) / len(time_advantages) + + except Exception as e: + logger.error(f"Error reading signals index: {e}") + + return stats \ No newline at end of file diff --git a/src/services/web_search.py b/src/services/web_search.py new file mode 100644 index 0000000..bb1a547 --- /dev/null +++ b/src/services/web_search.py @@ -0,0 +1,79 @@ +"""Unified web search with fallback: Tavily -> Serper -> DuckDuckGo.""" +import logging +from typing import Optional + +from src.services.tavily_search import TavilySearchService +from src.services.serper_search import SerperSearchService +from src.services.ddg_search import DDGSearchService + +logger = logging.getLogger(__name__) + +# Responses that indicate a search engine failed (not just "no results") +_FAILURE_KEYWORDS = ("API Error", "search failed", "unavailable", "exceeds your plan") + + +def _is_failure(result: str) -> bool: + return any(kw in result for kw in _FAILURE_KEYWORDS) + + +class WebSearchService: + """ + Unified web search with automatic fallback. + + Priority: Tavily (best quality) -> Serper -> DuckDuckGo (free). + Falls back to the next engine when the current one errors. + """ + + def __init__( + self, + tavily_api_key: str = "", + serper_api_key: str = "", + ): + self._engines = [] + + tavily = TavilySearchService(api_key=tavily_api_key) + if tavily.is_available(): + self._engines.append(("Tavily", tavily)) + + serper = SerperSearchService(api_key=serper_api_key) + if serper.is_available(): + self._engines.append(("Serper", serper)) + + ddg = DDGSearchService() + if ddg.is_available(): + self._engines.append(("DuckDuckGo", ddg)) + + if self._engines: + names = [name for name, _ in self._engines] + logger.info(f"Web search engines: {' -> '.join(names)}") + else: + logger.warning("No web search engine available.") + + def is_available(self) -> bool: + return len(self._engines) > 0 + + def search(self, query: str, max_results: int = 5) -> str: + if not self._engines: + return "Web search unavailable: no search engine configured." + + for name, engine in self._engines: + result = engine.search(query, max_results=max_results) + if _is_failure(result): + logger.warning(f"{name} search failed, trying next engine...") + continue + return result + + return f"All web search engines failed for: '{query}'" + + def search_for_market(self, market_question: str, max_results: int = 5) -> str: + if not self._engines: + return "Web search unavailable: no search engine configured." + + for name, engine in self._engines: + result = engine.search_for_market(market_question, max_results=max_results) + if _is_failure(result): + logger.warning(f"{name} market search failed, trying next engine...") + continue + return result + + return f"No relevant web results found for: {market_question[:50]}..." diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..4e9de1b --- /dev/null +++ b/src/utils/__init__.py @@ -0,0 +1,4 @@ +"""Utilities module.""" +from .logger import setup_logging, get_logger + +__all__ = ["setup_logging", "get_logger"] diff --git a/src/utils/http.py b/src/utils/http.py new file mode 100644 index 0000000..6db5403 --- /dev/null +++ b/src/utils/http.py @@ -0,0 +1,32 @@ +"""Shared HTTP client factory with proxy support. + +All services that need httpx clients should import from here instead of +creating httpx.Client/AsyncClient directly. This ensures proxy settings +are applied consistently across the entire application. +""" +import httpx +from src.config.settings import get_settings + + +def get_proxy_config() -> str | None: + """Return proxy URL from settings, or None if not configured.""" + settings = get_settings() + proxy = settings.http_proxy.strip() + return proxy if proxy else None + + +def get_client(**kwargs) -> httpx.Client: + """Create a synchronous httpx.Client with proxy support.""" + proxy = get_proxy_config() + if proxy: + kwargs.setdefault("proxy", proxy) + kwargs.setdefault("timeout", 30.0) + return httpx.Client(**kwargs) + + +def get_async_client(**kwargs) -> httpx.AsyncClient: + """Create an asynchronous httpx.AsyncClient with proxy support.""" + proxy = get_proxy_config() + if proxy: + kwargs.setdefault("proxy", proxy) + return httpx.AsyncClient(**kwargs) \ No newline at end of file diff --git a/src/utils/logger.py b/src/utils/logger.py new file mode 100644 index 0000000..9566fa1 --- /dev/null +++ b/src/utils/logger.py @@ -0,0 +1,120 @@ +"""Logging utilities.""" +import logging +import sys +from datetime import datetime +from typing import Optional + +from rich.console import Console +from rich.logging import RichHandler + +from src.config import get_settings + + +def setup_logging(level: Optional[str] = None) -> None: + """ + Set up application logging with rich formatting. + + Args: + level: Log level (DEBUG, INFO, WARNING, ERROR). Defaults to settings. + """ + settings = get_settings() + log_level = level or settings.log_level + + # Create rich console + console = Console() + + # Configure root logger + logging.basicConfig( + level=log_level, + format="%(message)s", + datefmt="[%X]", + handlers=[ + RichHandler( + console=console, + rich_tracebacks=True, + show_path=False, + ) + ], + ) + + # Reduce noise from third-party libraries + logging.getLogger("httpx").setLevel(logging.WARNING) + logging.getLogger("httpcore").setLevel(logging.WARNING) + logging.getLogger("openai").setLevel(logging.WARNING) + logging.getLogger("web3").setLevel(logging.WARNING) + + +def get_logger(name: str) -> logging.Logger: + """ + Get a logger instance. + + Args: + name: Logger name (usually __name__) + + Returns: + Logger instance + """ + return logging.getLogger(name) + + +class WhaleWatcherLogger: + """Custom logger for whale watcher with formatted output.""" + + def __init__(self): + self.console = Console() + self.logger = logging.getLogger("whale_watcher") + + def whale_detected( + self, + amount: float, + side: str, + price: float, + market: str, + ) -> None: + """Log a whale trade detection.""" + self.console.print( + f"\n[bold cyan]{'='*60}[/bold cyan]\n" + f"[bold yellow]🐋 WHALE TRADE DETECTED![/bold yellow]\n" + f"[bold cyan]{'='*60}[/bold cyan]\n" + f"[green]Amount:[/green] ${amount:,.2f} USDC\n" + f"[green]Side:[/green] {side}\n" + f"[green]Price:[/green] {price:.4f}\n" + f"[green]Market:[/green] {market}\n" + f"[green]Time:[/green] {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" + f"[bold cyan]{'='*60}[/bold cyan]\n" + ) + + def report_generated(self, market: str) -> None: + """Log that a report was generated.""" + self.console.print( + f"\n[bold magenta]{'='*60}[/bold magenta]\n" + f"[bold magenta]📊 ANALYSIS REPORT GENERATED[/bold magenta]\n" + f"[bold magenta]{'='*60}[/bold magenta]\n" + f"[green]Market:[/green] {market[:50]}...\n" + f"[bold magenta]{'='*60}[/bold magenta]\n" + ) + + def monitoring_started(self, market_count: int, interval: int = 0, min_trade_size: float = 1000, min_price: float = 0.2, max_price: float = 0.8) -> None: + """Log monitoring start.""" + self.console.print( + f"\n[bold green]{'='*60}[/bold green]\n" + f"[bold green]🚀 WHALE WATCHER STARTED (RTDS WebSocket)[/bold green]\n" + f"[bold green]{'='*60}[/bold green]\n" + f"[green]Monitored Markets:[/green] {market_count}\n" + f"[green]Mode:[/green] Real-time WebSocket (zero missed trades)\n" + f"[green]Min Trade Size:[/green] ${min_trade_size:,.0f} USD\n" + f"[green]Price Range:[/green] {min_price} - {max_price}\n" + f"[bold green]{'='*60}[/bold green]\n" + ) + + def error(self, message: str) -> None: + """Log an error.""" + self.console.print(f"[bold red]❌ ERROR:[/bold red] {message}") + + def info(self, message: str) -> None: + """Log an info message.""" + self.console.print(f"[blue]ℹ️[/blue] {message}") + + def separator(self) -> None: + """Print a separator line.""" + self.console.print(f"[dim]{'─'*60}[/dim]")