- Add anomaly signal model and history tracking - Implement backtest framework with detailed reports - Update whale analyzer prompts for better analysis - Improve LLM analyzer with enhanced features - Remove deprecated report history service - Add numerous whale activity reports 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
670 lines
25 KiB
Python
670 lines
25 KiB
Python
"""Backtest script - analyze historical trades for a specific user."""
|
||
import asyncio
|
||
import json
|
||
import logging
|
||
import os
|
||
import re
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
|
||
import httpx
|
||
from google import genai
|
||
from google.genai import types
|
||
from dotenv import load_dotenv
|
||
|
||
# Load environment variables
|
||
load_dotenv()
|
||
|
||
# Configure logging
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||
)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Settings from .env
|
||
MIN_TRADE_SIZE_USD = float(os.getenv("MIN_TRADE_SIZE_USD", 5000))
|
||
MIN_PRICE = float(os.getenv("MIN_PRICE", 0))
|
||
MAX_PRICE = float(os.getenv("MAX_PRICE", 0.9))
|
||
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
|
||
LLM_MODEL = os.getenv("LLM_MODEL", "gemini-2.0-flash")
|
||
|
||
# Target user
|
||
TARGET_USER = "0x31a56e9e690c621ed21de08cb559e9524cdb8ed9"
|
||
|
||
# API endpoints
|
||
DATA_API_URL = "https://data-api.polymarket.com"
|
||
GAMMA_API_URL = "https://gamma-api.polymarket.com"
|
||
|
||
# Reports directory
|
||
REPORTS_DIR = Path(__file__).parent / "backtest_reports"
|
||
REPORTS_DIR.mkdir(exist_ok=True)
|
||
|
||
|
||
class BacktestAnalyzer:
|
||
"""Analyzer for backtesting historical trades."""
|
||
|
||
def __init__(self):
|
||
self.client = httpx.AsyncClient(timeout=30.0)
|
||
os.environ["GOOGLE_API_KEY"] = GEMINI_API_KEY
|
||
self.genai_client = genai.Client()
|
||
|
||
async def close(self):
|
||
await self.client.aclose()
|
||
|
||
async def fetch_user_trades(self, wallet_address: str, limit: int = 500) -> list:
|
||
"""Fetch all trades for a user."""
|
||
try:
|
||
params = {
|
||
"user": wallet_address,
|
||
"limit": limit,
|
||
}
|
||
response = await self.client.get(f"{DATA_API_URL}/trades", params=params)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
except Exception as e:
|
||
logger.error(f"Error fetching trades: {e}")
|
||
return []
|
||
|
||
async def fetch_market_info(self, condition_id: str) -> Optional[dict]:
|
||
"""Fetch market information."""
|
||
try:
|
||
response = await self.client.get(f"{GAMMA_API_URL}/markets/{condition_id}")
|
||
response.raise_for_status()
|
||
return response.json()
|
||
except Exception as e:
|
||
logger.debug(f"Error fetching market info for {condition_id}: {e}")
|
||
return None
|
||
|
||
async def fetch_trader_ranking(self, wallet_address: str) -> Optional[dict]:
|
||
"""Fetch trader ranking from leaderboard."""
|
||
try:
|
||
params = {
|
||
"user": wallet_address,
|
||
"timePeriod": "ALL",
|
||
"orderBy": "PNL",
|
||
}
|
||
response = await self.client.get(f"{DATA_API_URL}/v1/leaderboard", params=params)
|
||
response.raise_for_status()
|
||
data = response.json()
|
||
if data and len(data) > 0:
|
||
return data[0]
|
||
return None
|
||
except Exception as e:
|
||
logger.debug(f"Error fetching ranking: {e}")
|
||
return None
|
||
|
||
def filter_trades(self, trades: list) -> list:
|
||
"""Filter trades by criteria."""
|
||
filtered = []
|
||
for trade in trades:
|
||
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
|
||
|
||
price = float(trade.get("price", 0) or 0)
|
||
|
||
if usdc_size >= MIN_TRADE_SIZE_USD and MIN_PRICE <= price <= MAX_PRICE:
|
||
trade["_usdc_size"] = usdc_size
|
||
filtered.append(trade)
|
||
|
||
# Sort by timestamp (newest first)
|
||
filtered.sort(key=lambda x: x.get("timestamp", 0), reverse=True)
|
||
return filtered
|
||
|
||
def build_trade_context(self, trade: dict, market_info: Optional[dict], prior_trades: list = None) -> str:
|
||
"""Build trade context for LLM analysis with prior trades history."""
|
||
usdc_size = trade.get("_usdc_size", 0)
|
||
side = trade.get("side", "")
|
||
price = float(trade.get("price", 0) or 0)
|
||
outcome = trade.get("outcome", "")
|
||
timestamp = trade.get("timestamp", 0)
|
||
proxy_wallet = trade.get("proxyWallet", trade.get("maker", ""))
|
||
|
||
# Market info
|
||
market_question = trade.get("title", trade.get("marketTitle", "Unknown"))
|
||
market_description = ""
|
||
outcomes = []
|
||
outcome_prices = []
|
||
|
||
if market_info:
|
||
market_question = market_info.get("question", market_question)
|
||
market_description = market_info.get("description", "")
|
||
outcomes = market_info.get("outcomes", [])
|
||
outcome_prices = market_info.get("outcomePrices", [])
|
||
|
||
# Format prices
|
||
prices_str = ""
|
||
if outcomes and outcome_prices:
|
||
try:
|
||
prices_str = ", ".join([f"{o}: {float(p):.4f}" for o, p in zip(outcomes, outcome_prices)])
|
||
except:
|
||
prices_str = "N/A"
|
||
|
||
time_str = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S') if timestamp else 'N/A'
|
||
trade_date = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d') if timestamp else 'N/A'
|
||
|
||
# Build prior trades history
|
||
prior_trades_str = ""
|
||
if prior_trades:
|
||
prior_trades_str = "\n### 该交易者之前的交易记录(本次交易之前)\n"
|
||
for i, pt in enumerate(prior_trades, 1):
|
||
pt_time = datetime.fromtimestamp(pt.get("timestamp", 0)).strftime('%Y-%m-%d %H:%M:%S') if pt.get("timestamp") else 'N/A'
|
||
pt_usdc = pt.get("_usdc_size", 0)
|
||
pt_side = pt.get("side", "")
|
||
pt_price = float(pt.get("price", 0) or 0)
|
||
pt_outcome = pt.get("outcome", "")
|
||
pt_market = pt.get("title", pt.get("marketTitle", "Unknown"))
|
||
prior_trades_str += f" {i}. [{pt_time}] {pt_side} ${pt_usdc:,.2f} @ {pt_price:.4f} - {pt_outcome} - {pt_market[:50]}\n"
|
||
else:
|
||
prior_trades_str = "\n### 该交易者之前的交易记录\n- 无之前的交易记录\n"
|
||
|
||
return f"""
|
||
## 异常交易检测
|
||
|
||
### 交易信息
|
||
- 交易金额: ${usdc_size:,.2f} USDC
|
||
- 交易方向: {side}
|
||
- 交易价格: {price:.4f}
|
||
- 交易结果: {outcome}
|
||
- 交易时间: {time_str}
|
||
- 交易者钱包: {proxy_wallet}
|
||
|
||
### 交易者信息
|
||
- 无排行榜排名信息(未知)
|
||
- 无盈亏记录(未知)
|
||
{prior_trades_str}
|
||
### 市场信息
|
||
- 市场问题: {market_question}
|
||
- 市场描述: {market_description or 'N/A'}
|
||
- 可能结果: {', '.join(outcomes) if outcomes else 'N/A'}
|
||
- 当前价格: {prices_str or 'N/A'}
|
||
|
||
### 重要提示
|
||
- **搜索新闻时,只能使用 {trade_date} 及之前的新闻**
|
||
- 不要使用交易发生之后的任何信息
|
||
|
||
### 分析要点
|
||
1. 这笔大额交易 (${usdc_size:,.2f}) 表明交易者对 "{outcome}" 结果有很强的信心
|
||
2. 交易价格 {price:.4f} 说明市场尚未形成明确共识
|
||
3. 交易方向为 {side},可能暗示内部信息或深度分析结论
|
||
4. 请分析交易者之前的交易记录,判断其交易模式和专业程度
|
||
"""
|
||
|
||
def get_system_prompt(self, trade_date: str) -> str:
|
||
"""Get system prompt with Google Search enabled."""
|
||
return f"""你是一位专业的预测市场分析师和内幕交易识别专家,专门分析 Polymarket 上的大额异常交易。
|
||
|
||
**你的核心任务**:验证一笔"疑似异常交易"是否真的是"内幕交易"(即交易者掌握了市场尚未反映的信息)。
|
||
|
||
## 严格时间限制(必须遵守!)
|
||
**交易发生时间**: {trade_date}
|
||
|
||
### 绝对禁止:
|
||
- ❌ 绝对禁止引用或提及任何 2026 年的新闻或事件
|
||
- ❌ 绝对禁止虚构、编造、推测任何新闻事件
|
||
- ❌ 绝对禁止假设交易之后发生了什么
|
||
- ❌ 绝对禁止使用"后续事件"、"之后发生"等表述
|
||
|
||
### 必须遵守:
|
||
- ✅ 只能搜索和使用 2025 年 12 月及之前的真实新闻
|
||
- ✅ 搜索关键词必须包含 "December 2025" 或 "2025"
|
||
- ✅ 只能基于 2025 年 12 月的信息进行分析
|
||
- ✅ 如果搜索不到相关新闻,必须如实说明"未找到相关新闻"
|
||
|
||
### 分析视角:
|
||
你必须假装现在是 2026 年 1 月 3 日,你不知道交易之后会发生什么。
|
||
你只能基于 2025 年 12 月及之前的公开信息来分析这笔交易。
|
||
|
||
## 你的工作流程
|
||
|
||
### 第一步:接收疑似异常交易信号
|
||
你会收到一笔被系统标记为"疑似异常"的交易,包含:
|
||
- 交易金额($5,000+的大额交易)
|
||
- 交易方向(BUY/SELL)和价格
|
||
- 交易者之前的交易记录(如有)
|
||
|
||
### 第二步:获取市场信息
|
||
你会同时收到该交易对应的市场信息:
|
||
- 市场问题(预测的事件)
|
||
- 市场描述
|
||
- 当前各结果的价格/概率
|
||
|
||
### 第三步:使用 Google Search 验证(关键步骤!)
|
||
**你必须使用 Google 搜索来验证这笔交易是否基于真实信息:**
|
||
- 搜索委内瑞拉相关的 2025 年 12 月新闻
|
||
- 搜索关键词示例:
|
||
- "Venezuela Maduro December 2025"
|
||
- "委内瑞拉 马杜罗 2025年12月"
|
||
- "Venezuela political news December 2025"
|
||
- "Maduro opposition December 2025"
|
||
- 查找是否有可能影响马杜罗政权的重要信息
|
||
- **只报告你真实搜索到的新闻,不要编造任何内容!**
|
||
|
||
### 第四步:综合判断并生成报告
|
||
结合所有信息,判断:
|
||
- 这笔交易是"真正的内幕交易"还是"普通大额交易"
|
||
- 给出内幕交易可能性评分(0-100%)
|
||
- 提供跟单建议(BUY/SELL/HOLD)
|
||
|
||
## 内幕交易识别框架
|
||
|
||
1. **交易者历史行为分析**:
|
||
- 查看交易者之前的交易记录
|
||
- 分析其交易模式(是否有规律性加仓、是否集中在特定领域)
|
||
- 判断其专业程度
|
||
|
||
2. **信息验证**:
|
||
- 搜索是否有支持该交易方向的 2025 年 12 月新闻
|
||
- 判断市场是否已经反映了这些信息
|
||
- 评估信息的时效性和可靠性
|
||
|
||
3. **综合判断标准**:
|
||
- 有历史交易记录 + 有最新未反映信息 = 可能是内幕交易 (0.6+)
|
||
- 有历史记录但无明显信息 = 可能基于深度分析 (0.4-0.6)
|
||
- 无历史记录 + 无信息 = 普通投机交易 (<0.4)
|
||
|
||
**重要原则**:
|
||
- **务必使用 Google Search!** 不要仅依赖你的历史知识
|
||
- **只搜索 2025 年 12 月的新闻!**
|
||
- **只报告真实搜索到的新闻,绝对不能虚构!**
|
||
- 如果搜索不到支持信息,内幕交易可能性应该降低
|
||
- 信心不足时建议观望(HOLD)"""
|
||
|
||
def get_analysis_prompt(self, trade_context: str, trade_date: str) -> str:
|
||
"""Get analysis prompt with Google Search instructions."""
|
||
return f"""{trade_context}
|
||
|
||
---
|
||
|
||
# 鲸鱼交易验证报告
|
||
|
||
你收到了一笔**疑似异常交易信号**,请按照以下步骤验证这是否是"真正的内幕交易"。
|
||
|
||
**重要:只搜索 2025 年 12 月的委内瑞拉新闻!不要虚构任何内容!**
|
||
|
||
---
|
||
|
||
## 第一步:Google 搜索验证(必须执行!)
|
||
|
||
**请立即使用 Google Search 搜索以下内容:**
|
||
|
||
1. 搜索委内瑞拉 2025 年 12 月的政治新闻
|
||
2. 搜索马杜罗政权 2025 年 12 月的动态
|
||
3. 搜索委内瑞拉反对派 2025 年 12 月的活动
|
||
|
||
**必须使用的搜索关键词**(包含 December 2025):
|
||
- "Venezuela Maduro December 2025"
|
||
- "Venezuela opposition December 2025"
|
||
- "Maduro government December 2025"
|
||
- "Venezuela political crisis December 2025"
|
||
|
||
**搜索结果摘要**:
|
||
(请在此列出你**真实搜索到**的 2025 年 12 月新闻,包括:
|
||
- 新闻标题
|
||
- 来源网站
|
||
- 发布日期(必须是 2025 年 12 月或之前)
|
||
- 主要内容摘要
|
||
|
||
**严格要求**:
|
||
- ❌ 绝对不能编造任何新闻
|
||
- ❌ 绝对不能引用任何 2026 年的新闻或事件
|
||
- ❌ 绝对不能推测交易之后发生了什么
|
||
- ✅ 如果搜索不到 2025 年 12 月的相关新闻,请如实说明"未找到相关新闻")
|
||
|
||
---
|
||
|
||
## 第二步:交易者历史行为分析
|
||
|
||
### 2.1 交易记录分析
|
||
- 查看交易者之前的交易记录
|
||
- 分析其交易模式(加仓频率、金额变化、价格变化)
|
||
- 判断其是否在有系统性地建仓
|
||
|
||
### 2.2 交易时机分析
|
||
- 这笔交易发生的时间点是否异常?
|
||
- **结合 2025 年 12 月的搜索结果**:是否有 2025 年 12 月的新闻可能触发了这笔交易?
|
||
- 基于 2025 年 12 月的公开信息,交易者是否可能掌握了市场尚未反映的信息?
|
||
|
||
---
|
||
|
||
## 第三步:市场信息验证(基于 2025 年 12 月信息)
|
||
|
||
### 3.1 当前市场状态
|
||
- 基于 2025 年 12 月的公开信息,市场价格是否合理?
|
||
- 交易价格与市场预期的关系如何?
|
||
|
||
### 3.2 信息差分析(只基于 2025 年 12 月信息)
|
||
- **关键问题**:2025 年 12 月的新闻是否支持这笔交易的方向?
|
||
- 这些信息是否已被市场完全定价?
|
||
- 基于 2025 年 12 月的公开信息,是否存在信息差?
|
||
|
||
---
|
||
|
||
## 第四步:内幕交易判定(基于 2025 年 12 月信息)
|
||
|
||
### 4.1 内幕交易可能性评估
|
||
**只基于 2025 年 12 月及之前的公开信息**,判断这笔交易是:
|
||
- **可能的内幕交易**:交易者可能掌握了 2025 年 12 月公开信息之外的信息
|
||
- **深度分析交易**:交易者基于 2025 年 12 月公开信息的深度分析
|
||
- **普通投机交易**:没有明显信息优势
|
||
|
||
### 4.2 关键证据
|
||
列出支持你判断的关键证据(**只能来自 2025 年 12 月的搜索结果和交易历史,禁止引用 2026 年信息**)
|
||
|
||
---
|
||
|
||
## 第五步:跟单风险提示
|
||
|
||
- 鲸鱼也可能犯错或有其他动机(对冲、试探等)
|
||
- 基于 2025 年 12 月的公开信息,市场可能已经部分反映了相关预期
|
||
- 搜索结果可能不完整
|
||
- **注意:我们不知道交易之后会发生什么,只能基于 2025 年 12 月的信息做判断**
|
||
|
||
---
|
||
|
||
## 第六步:最终决策
|
||
|
||
**重要提醒**:你的决策必须只基于 2025 年 12 月及之前的公开信息,不能假设或引用任何 2026 年的事件。
|
||
|
||
基于以上分析,给出你的交易建议,并用以下JSON格式输出决策:
|
||
|
||
```json
|
||
{{
|
||
"action": "BUY/SELL/HOLD",
|
||
"outcome": "你建议交易的结果选项",
|
||
"confidence": 0.0-1.0之间的数字,
|
||
"insider_trading_likelihood": 0.0-1.0之间的数字(内幕交易可能性评估),
|
||
"trader_credibility": "HIGH/MEDIUM/LOW/UNKNOWN",
|
||
"suggested_price": 建议的交易价格,
|
||
"suggested_size_percent": 0.0-1.0之间的数字(建议使用资金的比例),
|
||
"reasoning": "简要说明你的推理过程",
|
||
"insider_evidence": "支持内幕交易判断的关键证据"
|
||
}}
|
||
```
|
||
|
||
注意:
|
||
- action为HOLD时,outcome可以为空字符串
|
||
- confidence低于0.6时应该选择HOLD
|
||
- suggested_size_percent不应超过0.2(20%的资金)
|
||
- 请确保输出的是有效的JSON格式
|
||
|
||
---
|
||
|
||
**免责声明**:本报告仅供参考,不构成投资建议。预测市场具有高风险,请用户基于自身判断谨慎决策。"""
|
||
|
||
def _extract_json_from_response(self, response: str) -> Optional[dict]:
|
||
"""Extract JSON from LLM response."""
|
||
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:
|
||
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
|
||
|
||
async def analyze_trade(self, trade: dict, market_info: Optional[dict], prior_trades: list = None) -> tuple[str, Optional[dict]]:
|
||
"""Analyze a single trade with LLM (with Google Search and prior trades history)."""
|
||
timestamp = trade.get("timestamp", 0)
|
||
trade_date = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d') if timestamp else 'N/A'
|
||
|
||
trade_context = self.build_trade_context(trade, market_info, prior_trades)
|
||
system_prompt = self.get_system_prompt(trade_date)
|
||
user_prompt = self.get_analysis_prompt(trade_context, trade_date)
|
||
full_prompt = f"{system_prompt}\n\n---\n\n{user_prompt}"
|
||
|
||
try:
|
||
# Call Gemini API WITH Google Search tool enabled
|
||
response = self.genai_client.models.generate_content(
|
||
model=LLM_MODEL,
|
||
contents=full_prompt,
|
||
config=types.GenerateContentConfig(
|
||
tools=[types.Tool(google_search=types.GoogleSearch())],
|
||
),
|
||
)
|
||
|
||
# Log grounding metadata to verify Google Search was used
|
||
if hasattr(response, 'candidates') and response.candidates:
|
||
candidate = response.candidates[0]
|
||
if hasattr(candidate, 'grounding_metadata') and candidate.grounding_metadata:
|
||
gm = candidate.grounding_metadata
|
||
logger.info(f"Grounding metadata found: {type(gm)}")
|
||
if hasattr(gm, 'search_entry_point'):
|
||
logger.info(f"Search entry point: {gm.search_entry_point}")
|
||
if hasattr(gm, 'grounding_chunks') and gm.grounding_chunks:
|
||
logger.info(f"Grounding chunks count: {len(gm.grounding_chunks)}")
|
||
for i, chunk in enumerate(gm.grounding_chunks[:5]):
|
||
if hasattr(chunk, 'web') and chunk.web:
|
||
logger.info(f" Chunk {i+1}: {chunk.web.title} - {chunk.web.uri}")
|
||
else:
|
||
logger.warning("No grounding metadata - Google Search may not have been used")
|
||
|
||
analysis_text = response.text
|
||
json_data = self._extract_json_from_response(analysis_text)
|
||
return analysis_text, json_data
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error calling LLM: {e}")
|
||
return f"Error: {str(e)}", None
|
||
|
||
def format_report(self, trade: dict, market_info: Optional[dict], analysis_text: str, json_data: Optional[dict]) -> str:
|
||
"""Format the full report."""
|
||
usdc_size = trade.get("_usdc_size", 0)
|
||
side = trade.get("side", "")
|
||
price = float(trade.get("price", 0) or 0)
|
||
outcome = trade.get("outcome", "")
|
||
timestamp = trade.get("timestamp", 0)
|
||
market_question = trade.get("title", trade.get("marketTitle", "Unknown"))
|
||
|
||
if market_info:
|
||
market_question = market_info.get("question", market_question)
|
||
|
||
# Format prices
|
||
prices_str = ""
|
||
if market_info:
|
||
outcomes = market_info.get("outcomes", [])
|
||
outcome_prices = market_info.get("outcomePrices", [])
|
||
if outcomes and outcome_prices:
|
||
try:
|
||
prices_str = " | ".join([f"{o}: {float(p):.1%}" for o, p in zip(outcomes, outcome_prices)])
|
||
except:
|
||
prices_str = "N/A"
|
||
|
||
# Parse recommendation
|
||
action = "HOLD"
|
||
confidence = 0.0
|
||
reasoning = ""
|
||
insider_likelihood = 0.0
|
||
trader_credibility = "UNKNOWN"
|
||
insider_evidence = ""
|
||
|
||
if json_data:
|
||
action = json_data.get("action", "HOLD")
|
||
confidence = float(json_data.get("confidence", 0))
|
||
reasoning = json_data.get("reasoning", "")
|
||
insider_likelihood = float(json_data.get("insider_trading_likelihood", 0))
|
||
trader_credibility = json_data.get("trader_credibility", "UNKNOWN")
|
||
insider_evidence = json_data.get("insider_evidence", "")
|
||
|
||
# Action indicator
|
||
action_indicators = {
|
||
"BUY": "🟢 BUY",
|
||
"SELL": "🔴 SELL",
|
||
"HOLD": "⚪ HOLD",
|
||
}
|
||
|
||
# Insider indicator
|
||
if insider_likelihood >= 0.7:
|
||
insider_indicator = f"🔴 高度可疑 ({insider_likelihood:.0%})"
|
||
elif insider_likelihood >= 0.4:
|
||
insider_indicator = f"🟡 中等可能 ({insider_likelihood:.0%})"
|
||
else:
|
||
insider_indicator = f"🟢 普通交易 ({insider_likelihood:.0%})"
|
||
|
||
# Credibility indicator
|
||
credibility_indicators = {
|
||
"HIGH": "🏆 高可信度 (前100名)",
|
||
"MEDIUM": "⭐ 中等可信度 (100-500名)",
|
||
"LOW": "📉 低可信度 (500名+)",
|
||
"UNKNOWN": "❓ 未知 (未上榜)",
|
||
}
|
||
|
||
time_str = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S') if timestamp else 'N/A'
|
||
|
||
report = f"""
|
||
{'='*70}
|
||
# 🐋 鲸鱼交易回测分析报告
|
||
{'='*70}
|
||
|
||
**生成时间**: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC
|
||
**回测模式**: 无 Google Search
|
||
|
||
## 交易摘要
|
||
|
||
| 项目 | 详情 |
|
||
|------|------|
|
||
| **市场** | {market_question} |
|
||
| **交易金额** | ${usdc_size:,.2f} USDC |
|
||
| **交易方向** | {side} |
|
||
| **交易价格** | {price:.4f} ({price:.1%}) |
|
||
| **交易结果** | {outcome} |
|
||
| **当前赔率** | {prices_str or 'N/A'} |
|
||
| **交易时间** | {time_str} |
|
||
|
||
{'='*70}
|
||
|
||
{analysis_text}
|
||
|
||
{'='*70}
|
||
## 🔍 内幕交易评估
|
||
{'='*70}
|
||
|
||
| 项目 | 评估 |
|
||
|------|------|
|
||
| **内幕交易可能性** | {insider_indicator} |
|
||
| **交易者可信度** | {credibility_indicators.get(trader_credibility, '❓ 未知')} |
|
||
|
||
**关键证据**: {insider_evidence or '无明确证据'}
|
||
|
||
{'='*70}
|
||
## 📊 决策摘要
|
||
{'='*70}
|
||
|
||
| 项目 | 建议 |
|
||
|------|------|
|
||
| **操作建议** | {action_indicators.get(action, '⚪ HOLD')} |
|
||
| **目标结果** | {json_data.get('outcome', 'N/A') if json_data else 'N/A'} |
|
||
| **信心程度** | {confidence:.1%} |
|
||
| **建议仓位** | {json_data.get('suggested_size_percent', 0):.1%} if json_data else 'N/A' |
|
||
| **建议价格** | {json_data.get('suggested_price', 'Market') if json_data else 'Market'} |
|
||
|
||
**决策理由**: {reasoning}
|
||
|
||
{'='*70}
|
||
⚠️ 免责声明:本报告由AI生成,仅供参考,不构成投资建议。
|
||
预测市场具有高风险,请基于自身判断谨慎决策。
|
||
{'='*70}
|
||
"""
|
||
return report
|
||
|
||
def save_report(self, trade: dict, report: str) -> str:
|
||
"""Save report to file."""
|
||
usdc_size = trade.get("_usdc_size", 0)
|
||
side = trade.get("side", "")
|
||
timestamp = trade.get("timestamp", 0)
|
||
market_title = trade.get("title", trade.get("marketTitle", "Unknown"))
|
||
|
||
# Clean title for filename
|
||
clean_title = re.sub(r'[^\w\s-]', '', market_title)[:50].strip().replace(' ', '_')
|
||
|
||
time_str = datetime.fromtimestamp(timestamp).strftime('%Y%m%d_%H%M%S') if timestamp else 'unknown'
|
||
filename = f"{time_str}_{side}_{int(usdc_size)}USD_{clean_title}.md"
|
||
filepath = REPORTS_DIR / filename
|
||
|
||
with open(filepath, 'w', encoding='utf-8') as f:
|
||
f.write(report)
|
||
|
||
return str(filepath)
|
||
|
||
|
||
async def main():
|
||
"""Main backtest function."""
|
||
analyzer = BacktestAnalyzer()
|
||
|
||
try:
|
||
logger.info(f"Starting backtest for user: {TARGET_USER}")
|
||
logger.info(f"Filter criteria: MIN_SIZE=${MIN_TRADE_SIZE_USD}, PRICE_RANGE={MIN_PRICE}-{MAX_PRICE}")
|
||
|
||
# Fetch all trades for user
|
||
logger.info("Fetching user trades...")
|
||
all_trades = await analyzer.fetch_user_trades(TARGET_USER)
|
||
logger.info(f"Found {len(all_trades)} total trades")
|
||
|
||
# Filter trades
|
||
filtered_trades = analyzer.filter_trades(all_trades)
|
||
logger.info(f"Found {len(filtered_trades)} trades matching criteria")
|
||
|
||
if not filtered_trades:
|
||
logger.info("No trades found matching criteria")
|
||
return
|
||
|
||
# Sort by timestamp (oldest first) for correct chronological order
|
||
filtered_trades.sort(key=lambda x: x.get("timestamp", 0))
|
||
|
||
# Only analyze the latest trade (the $7,215 one at 2026-01-03 10:58:25)
|
||
# Use previous trades as history
|
||
target_trade = filtered_trades[-1] # The latest trade
|
||
prior_trades = filtered_trades[:-1] # All trades before the target
|
||
|
||
usdc_size = target_trade.get("_usdc_size", 0)
|
||
side = target_trade.get("side", "")
|
||
market_title = target_trade.get("title", target_trade.get("marketTitle", "Unknown"))
|
||
timestamp = target_trade.get("timestamp", 0)
|
||
trade_time = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S') if timestamp else 'N/A'
|
||
|
||
logger.info(f"\nAnalyzing target trade: {side} ${usdc_size:,.2f} - {market_title[:50]}")
|
||
logger.info(f"Trade time: {trade_time}")
|
||
logger.info(f"Prior trades count: {len(prior_trades)}")
|
||
|
||
for i, pt in enumerate(prior_trades, 1):
|
||
pt_time = datetime.fromtimestamp(pt.get("timestamp", 0)).strftime('%Y-%m-%d %H:%M:%S') if pt.get("timestamp") else 'N/A'
|
||
pt_usdc = pt.get("_usdc_size", 0)
|
||
pt_side = pt.get("side", "")
|
||
logger.info(f" Prior trade {i}: [{pt_time}] {pt_side} ${pt_usdc:,.2f}")
|
||
|
||
# Fetch market info
|
||
condition_id = target_trade.get("conditionId", "")
|
||
market_info = await analyzer.fetch_market_info(condition_id) if condition_id else None
|
||
|
||
# Analyze with LLM (with Google Search and prior trades history)
|
||
logger.info("\nCalling LLM with Google Search enabled...")
|
||
analysis_text, json_data = await analyzer.analyze_trade(target_trade, market_info, prior_trades)
|
||
|
||
# Format and save report
|
||
report = analyzer.format_report(target_trade, market_info, analysis_text, json_data)
|
||
filepath = analyzer.save_report(target_trade, report)
|
||
logger.info(f"Report saved: {filepath}")
|
||
|
||
logger.info(f"\nBacktest complete! {len(filtered_trades)} reports generated in {REPORTS_DIR}")
|
||
|
||
finally:
|
||
await analyzer.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(main())
|