├── logoduolai.png ├── public ├── logoduolai.png └── index.html ├── frontend ├── logoduolai.png └── index.html ├── .gitignore ├── backend ├── requirements.txt └── main.py ├── vercel.json ├── start.sh ├── README.md └── api └── markets.py /logoduolai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duolaAmengweb3/opinionWhale/HEAD/logoduolai.png -------------------------------------------------------------------------------- /public/logoduolai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duolaAmengweb3/opinionWhale/HEAD/public/logoduolai.png -------------------------------------------------------------------------------- /frontend/logoduolai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duolaAmengweb3/opinionWhale/HEAD/frontend/logoduolai.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | .env* 3 | venv/ 4 | __pycache__/ 5 | *.pyc 6 | .DS_Store 7 | node_modules/ 8 | .vscode/ 9 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi>=0.104.0 2 | uvicorn>=0.24.0 3 | pydantic>=2.0.0 4 | opinion-clob-sdk>=0.4.2 5 | requests>=2.31.0 6 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "api/markets.py", 6 | "use": "@vercel/python", 7 | "config": { 8 | "maxDuration": 60 9 | } 10 | }, 11 | { 12 | "src": "public/**", 13 | "use": "@vercel/static" 14 | } 15 | ], 16 | "routes": [ 17 | { 18 | "src": "/api/markets", 19 | "dest": "/api/markets.py" 20 | }, 21 | { 22 | "src": "/(.*)", 23 | "dest": "/public/$1" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Opinion Whale Tracker 启动脚本 4 | 5 | echo "=================================" 6 | echo "Opinion Whale Tracker" 7 | echo "=================================" 8 | 9 | # 检查虚拟环境 10 | if [ ! -d "venv" ]; then 11 | echo "Creating virtual environment..." 12 | python3 -m venv venv 13 | fi 14 | 15 | # 激活虚拟环境 16 | source venv/bin/activate 17 | 18 | # 安装依赖 19 | echo "Installing dependencies..." 20 | pip install -q -r backend/requirements.txt 21 | 22 | # 启动后端服务 (后台运行) 23 | echo "Starting backend server on port 8000..." 24 | cd backend 25 | uvicorn main:app --host 0.0.0.0 --port 8000 --lifespan off & 26 | BACKEND_PID=$! 27 | cd .. 28 | 29 | # 等待后端启动 30 | sleep 3 31 | 32 | # 启动前端服务 33 | echo "Starting frontend server on port 8080..." 34 | cd frontend 35 | python3 -m http.server 8080 & 36 | FRONTEND_PID=$! 37 | cd .. 38 | 39 | echo "" 40 | echo "=================================" 41 | echo "Services started!" 42 | echo "=================================" 43 | echo "Backend API: http://localhost:8000" 44 | echo "Frontend UI: http://localhost:8080" 45 | echo "API Docs: http://localhost:8000/docs" 46 | echo "=================================" 47 | echo "" 48 | echo "Press Ctrl+C to stop all services" 49 | 50 | # 等待用户中断 51 | trap "kill $BACKEND_PID $FRONTEND_PID 2>/dev/null; exit" SIGINT SIGTERM 52 | 53 | wait 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Opinion Whale Tracker 2 | 3 | 面向预测市场的“巨鲸监控”与市场可视化项目,包含 Python FastAPI 后端与简单前端页面。后端聚合 Opinion CLOB API 的市场与订单簿数据,提炼买卖墙与大额委托,前端展示基础信息。 4 | 5 | ## 技术栈 6 | 7 | - 后端:`FastAPI` + `pydantic`,结构清晰、性能优良 8 | - SDK:`opinion_clob_sdk`(封装对 Opinion 代理网关的访问) 9 | - 运行环境:Python 3.10+ 10 | - 前端:静态页面(`frontend/` 与 `public/`),通过浏览器访问后端 API 11 | 12 | ## 目录结构 13 | 14 | - `backend/`:后端服务入口与业务逻辑(`main.py`) 15 | - `api/`:无框架的简易 HTTP 处理器示例(`markets.py`) 16 | - `frontend/`、`public/`:静态资源与示例页面 17 | - `requirements.txt`:后端依赖清单(位于 `backend/`) 18 | 19 | ## 环境变量 20 | 21 | - `OPINION_API_KEY`:后端访问 Opinion 代理网关所需的 API Key(必填) 22 | 23 | 为保证安全,代码中不再包含任何密钥或令牌。请在本地或部署环境以环境变量方式注入,不要将 `.env*` 文件提交到仓库。 24 | 25 | ## 安装与运行 26 | 27 | 1. 创建虚拟环境并安装依赖: 28 | ```bash 29 | python3 -m venv .venv 30 | source .venv/bin/activate 31 | pip install -r backend/requirements.txt 32 | ``` 33 | 2. 设置环境变量: 34 | ```bash 35 | export OPINION_API_KEY="" 36 | ``` 37 | 3. 启动后端: 38 | ```bash 39 | python backend/main.py 40 | # 或使用 uvicorn: 41 | uvicorn backend.main:app --host 0.0.0.0 --port 8000 42 | ``` 43 | 44 | 服务启动后,访问: 45 | 46 | - `GET /`:健康检查 47 | - `GET /api/markets`:聚合市场与巨鲸信息 48 | - `GET /api/markets/{market_id}`:单市场详情 49 | - `GET /api/whales?threshold=500`:按阈值过滤的大额委托/买卖墙 50 | - `GET /api/orderbook/{token_id}`:订单簿(bids/asks) 51 | - `GET /api/stats`:总览统计 52 | 53 | 示例请求: 54 | 55 | ```bash 56 | curl http://localhost:8000/api/markets 57 | curl "http://localhost:8000/api/whales?threshold=1000" 58 | ``` 59 | 60 | ## 关键实现 61 | 62 | - 市场聚合:分页拉取并合并市场列表,兼容二元与分类市场结构 63 | - 价格/订单簿:对 `yes_token_id` 拉取当前价格与订单簿,计算买卖墙价值 64 | - 巨鲸检测:基于可配置阈值(默认 500 USD)识别买卖侧的大额挂单 65 | - 缓存与刷新:定时刷新缓存,避免频繁调用上游 API 66 | 67 | 后端核心入口:`backend/main.py`。 68 | 69 | ## 安全与合规 70 | 71 | - 绝不在仓库中保存密钥、令牌或私人配置 72 | - `.gitignore` 已忽略常见敏感与本地文件(`venv/`、`.env*`、`__pycache__/` 等) 73 | - 若未设置 `OPINION_API_KEY`,后端将返回空数据而非抛出敏感信息 74 | 75 | ## 部署建议 76 | 77 | - 将 `OPINION_API_KEY` 作为托管平台的环境变量注入(如 Vercel/Render/Railway 等) 78 | - 使用反向代理或边缘缓存降低上游 API 压力和时延 79 | - 配置健康检查与日志收集,监控刷新任务与调用错误率 80 | 81 | ## 开发提示 82 | 83 | - 添加更多数据源:可在 `process_market` 与 `detect_whales` 中扩展策略 84 | - 指标与告警:接入 Prometheus/Grafana,针对买卖墙变动做告警 85 | - 前端可视化:将 `whales` 列表与 `orderbook` 转为图形展示,提高可读性 86 | 87 | ## 许可 88 | 89 | 本项目用于技术演示与学习目的,请在遵守上游 API 使用条款的前提下进行二次开发。 90 | 91 | -------------------------------------------------------------------------------- /api/markets.py: -------------------------------------------------------------------------------- 1 | from http.server import BaseHTTPRequestHandler 2 | import json 3 | import traceback 4 | import requests 5 | from datetime import datetime 6 | import os 7 | 8 | # Configuration 9 | API_KEY = os.environ.get("OPINION_API_KEY", "") 10 | API_HOST = "https://proxy.opinion.trade:8443" 11 | WHALE_THRESHOLD = 500 12 | 13 | def api_request(endpoint, params=None): 14 | """Direct API request to Opinion""" 15 | headers = {"Content-Type": "application/json"} 16 | if API_KEY: 17 | headers["x-api-key"] = API_KEY 18 | url = f"{API_HOST}{endpoint}" 19 | try: 20 | resp = requests.get(url, headers=headers, params=params, timeout=30) 21 | return resp.json() 22 | except Exception as e: 23 | return {"error": str(e)} 24 | 25 | def fetch_markets_data(): 26 | errors = [] 27 | all_markets = [] 28 | 29 | # Try to get markets using direct API call 30 | for page in range(1, 4): 31 | try: 32 | data = api_request("/markets", {"page": page, "limit": 20}) 33 | 34 | if "error" in data: 35 | errors.append(f"Page {page}: {data['error']}") 36 | break 37 | 38 | # Try different response structures 39 | markets_list = None 40 | if isinstance(data, dict): 41 | if "result" in data and data["result"]: 42 | if isinstance(data["result"], dict) and "list" in data["result"]: 43 | markets_list = data["result"]["list"] 44 | elif isinstance(data["result"], list): 45 | markets_list = data["result"] 46 | elif "data" in data: 47 | if isinstance(data["data"], list): 48 | markets_list = data["data"] 49 | elif isinstance(data["data"], dict) and "list" in data["data"]: 50 | markets_list = data["data"]["list"] 51 | elif "markets" in data: 52 | markets_list = data["markets"] 53 | elif "list" in data: 54 | markets_list = data["list"] 55 | elif isinstance(data, list): 56 | markets_list = data 57 | 58 | if not markets_list: 59 | errors.append(f"Page {page}: Unknown response structure: {json.dumps(data)[:500]}") 60 | break 61 | 62 | all_markets.extend(markets_list) 63 | 64 | except Exception as e: 65 | errors.append(f"Page {page}: {str(e)}") 66 | break 67 | 68 | if not all_markets: 69 | return { 70 | "error": "No markets fetched", 71 | "errors": errors, 72 | "markets": [], 73 | "whales": [], 74 | "total_volume": 0, 75 | "whale_count": 0, 76 | "updated_at": datetime.now().isoformat() 77 | } 78 | 79 | processed_markets = [] 80 | whales = [] 81 | 82 | for m in all_markets[:20]: 83 | try: 84 | # Handle both dict and object-like responses 85 | if isinstance(m, dict): 86 | market_id = m.get("market_id") or m.get("id") 87 | market_title = m.get("market_title") or m.get("title") or m.get("question") or "" 88 | yes_token_id = m.get("yes_token_id") or m.get("yesTokenId") 89 | volume = float(m.get("volume", 0) or 0) 90 | status = m.get("status_enum") or m.get("status") or "Active" 91 | else: 92 | market_id = getattr(m, "market_id", None) or getattr(m, "id", None) 93 | market_title = getattr(m, "market_title", "") or getattr(m, "title", "") 94 | yes_token_id = getattr(m, "yes_token_id", None) 95 | volume = float(getattr(m, "volume", 0) or 0) 96 | status = getattr(m, "status_enum", "Active") or "Active" 97 | 98 | if not market_id: 99 | continue 100 | 101 | outcomes = [] 102 | 103 | if yes_token_id: 104 | token_id = yes_token_id 105 | price = 0 106 | bid_depth = 0 107 | ask_depth = 0 108 | 109 | # Get price 110 | try: 111 | price_data = api_request("/price", {"token_id": token_id}) 112 | if price_data and "result" in price_data and price_data["result"]: 113 | price = float(price_data["result"].get("price", 0) or 0) 114 | except: 115 | pass 116 | 117 | # Get orderbook 118 | try: 119 | ob_data = api_request("/orderbook", {"token_id": token_id}) 120 | if ob_data and "result" in ob_data and ob_data["result"]: 121 | bids = ob_data["result"].get("bids", []) or [] 122 | asks = ob_data["result"].get("asks", []) or [] 123 | for b in bids: 124 | bid_depth += float(b.get("size", 0) or 0) 125 | for a in asks: 126 | ask_depth += float(a.get("size", 0) or 0) 127 | except: 128 | pass 129 | 130 | bid_value = bid_depth * price if price > 0 else 0 131 | ask_value = ask_depth * (1 - price) if 0 < price < 1 else ask_depth * 0.5 132 | 133 | outcomes.append({ 134 | "title": "YES", 135 | "token_id": str(token_id)[:20] + "...", 136 | "price": price, 137 | "bid_depth": bid_value, 138 | "ask_depth": ask_value 139 | }) 140 | 141 | if bid_value >= WHALE_THRESHOLD: 142 | whales.append({ 143 | "market_id": market_id, 144 | "market_title": market_title, 145 | "outcome": "YES", 146 | "side": "BUY", 147 | "price": price, 148 | "size": bid_depth, 149 | "value": bid_value 150 | }) 151 | if ask_value >= WHALE_THRESHOLD: 152 | whales.append({ 153 | "market_id": market_id, 154 | "market_title": market_title, 155 | "outcome": "YES", 156 | "side": "SELL", 157 | "price": price, 158 | "size": ask_depth, 159 | "value": ask_value 160 | }) 161 | 162 | if outcomes: 163 | processed_markets.append({ 164 | "market_id": market_id, 165 | "title": market_title, 166 | "volume": volume, 167 | "status": status, 168 | "outcomes": outcomes 169 | }) 170 | except Exception as e: 171 | errors.append(f"Market processing: {str(e)}") 172 | continue 173 | 174 | whales.sort(key=lambda x: x["value"], reverse=True) 175 | 176 | return { 177 | "markets": processed_markets, 178 | "whales": whales, 179 | "total_volume": sum(m["volume"] for m in processed_markets), 180 | "whale_count": len(whales), 181 | "updated_at": datetime.now().isoformat(), 182 | "debug": { 183 | "total_fetched": len(all_markets), 184 | "processed": len(processed_markets), 185 | "errors": errors if errors else None 186 | } 187 | } 188 | 189 | 190 | class handler(BaseHTTPRequestHandler): 191 | def do_GET(self): 192 | try: 193 | data = fetch_markets_data() 194 | 195 | self.send_response(200) 196 | self.send_header('Content-Type', 'application/json') 197 | self.send_header('Access-Control-Allow-Origin', '*') 198 | self.send_header('Cache-Control', 'public, max-age=30') 199 | self.end_headers() 200 | 201 | self.wfile.write(json.dumps(data).encode()) 202 | except Exception as e: 203 | self.send_response(500) 204 | self.send_header('Content-Type', 'application/json') 205 | self.send_header('Access-Control-Allow-Origin', '*') 206 | self.end_headers() 207 | 208 | error_data = { 209 | "error": str(e), 210 | "traceback": traceback.format_exc() 211 | } 212 | self.wfile.write(json.dumps(error_data).encode()) 213 | 214 | def do_OPTIONS(self): 215 | self.send_response(200) 216 | self.send_header('Access-Control-Allow-Origin', '*') 217 | self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') 218 | self.send_header('Access-Control-Allow-Headers', 'Content-Type') 219 | self.end_headers() 220 | -------------------------------------------------------------------------------- /backend/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Opinion Whale Tracker Backend 4 | FastAPI 后端服务 - 提供市场数据和巨鲸监控 API 5 | """ 6 | 7 | import asyncio 8 | import os 9 | from fastapi import FastAPI, HTTPException 10 | from fastapi.middleware.cors import CORSMiddleware 11 | from fastapi.responses import JSONResponse 12 | from pydantic import BaseModel 13 | from typing import List, Optional 14 | import json 15 | from datetime import datetime 16 | 17 | from opinion_clob_sdk import Client 18 | 19 | # Configuration 20 | API_KEY = os.environ.get("OPINION_API_KEY", "") 21 | WHALE_THRESHOLD = 500 22 | 23 | # Initialize FastAPI 24 | app = FastAPI( 25 | title="Opinion Whale Tracker API", 26 | description="巨鲸监控平台后端 API", 27 | version="1.0.0" 28 | ) 29 | 30 | # CORS 31 | app.add_middleware( 32 | CORSMiddleware, 33 | allow_origins=["*"], 34 | allow_credentials=True, 35 | allow_methods=["*"], 36 | allow_headers=["*"], 37 | ) 38 | 39 | # Data Models 40 | class Outcome(BaseModel): 41 | title: str 42 | token_id: str 43 | price: float 44 | bid_depth: float 45 | ask_depth: float 46 | 47 | class Market(BaseModel): 48 | market_id: int 49 | title: str 50 | volume: float 51 | status: str 52 | outcomes: List[Outcome] 53 | 54 | class WhaleOrder(BaseModel): 55 | market_id: int 56 | market_title: str 57 | outcome: str 58 | side: str 59 | price: float 60 | size: float 61 | value: float 62 | 63 | class MarketsResponse(BaseModel): 64 | markets: List[Market] 65 | whales: List[WhaleOrder] 66 | total_volume: float 67 | whale_count: int 68 | updated_at: str 69 | 70 | # Global cache 71 | cache = { 72 | "data": None, 73 | "updated_at": None 74 | } 75 | 76 | def get_opinion_client(): 77 | return Client( 78 | host='https://proxy.opinion.trade:8443', 79 | apikey=API_KEY, 80 | chain_id=56, 81 | rpc_url='https://bsc-dataseed.binance.org', 82 | private_key='0x0000000000000000000000000000000000000000000000000000000000000001', 83 | multi_sig_addr='0x2f0c9ba178c669b8a21173bd83b267bd9200ad6d' 84 | ) 85 | 86 | def fetch_all_markets(): 87 | """获取所有市场数据""" 88 | client = get_opinion_client() 89 | all_markets = [] 90 | 91 | # 获取市场列表 92 | for page in range(1, 8): 93 | try: 94 | markets = client.get_markets(page=page, limit=20) 95 | if not markets.result.list: 96 | break 97 | all_markets.extend(markets.result.list) 98 | except Exception as e: 99 | print(f"Error fetching page {page}: {e}") 100 | break 101 | 102 | return all_markets 103 | 104 | def process_market(client, market) -> Optional[Market]: 105 | """处理单个市场,获取详细信息""" 106 | try: 107 | outcomes = [] 108 | 109 | # 检查是否是二元市场 110 | if market.yes_token_id: 111 | # 二元市场 112 | token_id = market.yes_token_id 113 | price = 0 114 | bid_depth = 0 115 | ask_depth = 0 116 | 117 | try: 118 | price_data = client.get_latest_price(token_id=token_id) 119 | if price_data.result.price: 120 | price = float(price_data.result.price) 121 | except: 122 | pass 123 | 124 | try: 125 | ob = client.get_orderbook(token_id=token_id) 126 | if ob.result.bids: 127 | bid_depth = sum(float(b.size) for b in ob.result.bids) 128 | if ob.result.asks: 129 | ask_depth = sum(float(a.size) for a in ob.result.asks) 130 | except: 131 | pass 132 | 133 | outcomes.append(Outcome( 134 | title="YES", 135 | token_id=token_id[:20] + "...", 136 | price=price, 137 | bid_depth=bid_depth * price, 138 | ask_depth=ask_depth * (1 - price) if price < 1 else ask_depth * 0.5 139 | )) 140 | 141 | else: 142 | # 分类市场 - 获取子市场 143 | try: 144 | cat = client.get_categorical_market(market_id=market.market_id) 145 | if cat.result.data and cat.result.data.child_markets: 146 | for cm in cat.result.data.child_markets: 147 | token_id = cm.yes_token_id 148 | price = 0 149 | bid_depth = 0 150 | ask_depth = 0 151 | 152 | if token_id: 153 | try: 154 | price_data = client.get_latest_price(token_id=token_id) 155 | if price_data.result.price: 156 | price = float(price_data.result.price) 157 | except: 158 | pass 159 | 160 | try: 161 | ob = client.get_orderbook(token_id=token_id) 162 | if ob.result.bids: 163 | bid_depth = sum(float(b.price) * float(b.size) for b in ob.result.bids) 164 | if ob.result.asks: 165 | ask_depth = sum(float(a.price) * float(a.size) for a in ob.result.asks) 166 | except: 167 | pass 168 | 169 | outcomes.append(Outcome( 170 | title=cm.market_title, 171 | token_id=token_id[:20] + "..." if token_id else "", 172 | price=price, 173 | bid_depth=bid_depth, 174 | ask_depth=ask_depth 175 | )) 176 | except Exception as e: 177 | print(f"Error processing categorical market {market.market_id}: {e}") 178 | 179 | if not outcomes: 180 | return None 181 | 182 | return Market( 183 | market_id=market.market_id, 184 | title=market.market_title, 185 | volume=float(market.volume) if market.volume else 0, 186 | status=market.status_enum or "Active", 187 | outcomes=outcomes 188 | ) 189 | 190 | except Exception as e: 191 | print(f"Error processing market {market.market_id}: {e}") 192 | return None 193 | 194 | def detect_whales(markets: List[Market], threshold: float = WHALE_THRESHOLD) -> List[WhaleOrder]: 195 | """检测大单""" 196 | whales = [] 197 | 198 | for market in markets: 199 | for outcome in market.outcomes: 200 | # 检测买墙 201 | if outcome.bid_depth >= threshold: 202 | whales.append(WhaleOrder( 203 | market_id=market.market_id, 204 | market_title=market.title, 205 | outcome=outcome.title, 206 | side="BUY", 207 | price=outcome.price, 208 | size=outcome.bid_depth / outcome.price if outcome.price > 0 else 0, 209 | value=outcome.bid_depth 210 | )) 211 | 212 | # 检测卖墙 213 | if outcome.ask_depth >= threshold: 214 | whales.append(WhaleOrder( 215 | market_id=market.market_id, 216 | market_title=market.title, 217 | outcome=outcome.title, 218 | side="SELL", 219 | price=outcome.price, 220 | size=outcome.ask_depth / (1 - outcome.price) if outcome.price < 1 else outcome.ask_depth, 221 | value=outcome.ask_depth 222 | )) 223 | 224 | return sorted(whales, key=lambda x: x.value, reverse=True) 225 | 226 | async def refresh_data(): 227 | print(f"[{datetime.now()}] Refreshing data...") 228 | 229 | if not API_KEY: 230 | cache["data"] = MarketsResponse( 231 | markets=[], 232 | whales=[], 233 | total_volume=0, 234 | whale_count=0, 235 | updated_at=datetime.now().isoformat() 236 | ) 237 | cache["updated_at"] = datetime.now() 238 | print("OPINION_API_KEY not set, served empty dataset") 239 | return 240 | 241 | client = get_opinion_client() 242 | raw_markets = fetch_all_markets() 243 | 244 | markets = [] 245 | for m in raw_markets[:50]: # 限制前50个市场以加快速度 246 | processed = process_market(client, m) 247 | if processed: 248 | markets.append(processed) 249 | 250 | whales = detect_whales(markets) 251 | total_volume = sum(m.volume for m in markets) 252 | 253 | cache["data"] = MarketsResponse( 254 | markets=markets, 255 | whales=whales, 256 | total_volume=total_volume, 257 | whale_count=len(whales), 258 | updated_at=datetime.now().isoformat() 259 | ) 260 | cache["updated_at"] = datetime.now() 261 | 262 | print(f"[{datetime.now()}] Data refreshed: {len(markets)} markets, {len(whales)} whales") 263 | 264 | # API Endpoints 265 | @app.get("/") 266 | async def root(): 267 | return {"message": "Opinion Whale Tracker API", "status": "running"} 268 | 269 | @app.get("/api/markets", response_model=MarketsResponse) 270 | async def get_markets(): 271 | """获取所有市场数据和巨鲸信息""" 272 | if cache["data"] is None: 273 | await refresh_data() 274 | 275 | return cache["data"] 276 | 277 | @app.get("/api/markets/{market_id}") 278 | async def get_market(market_id: int): 279 | """获取单个市场详情""" 280 | if cache["data"] is None: 281 | await refresh_data() 282 | 283 | for market in cache["data"].markets: 284 | if market.market_id == market_id: 285 | return market 286 | 287 | raise HTTPException(status_code=404, detail="Market not found") 288 | 289 | @app.get("/api/whales") 290 | async def get_whales(threshold: int = WHALE_THRESHOLD): 291 | """获取大单列表""" 292 | if cache["data"] is None: 293 | await refresh_data() 294 | 295 | whales = [w for w in cache["data"].whales if w.value >= threshold] 296 | return {"whales": whales, "count": len(whales)} 297 | 298 | @app.get("/api/orderbook/{token_id}") 299 | async def get_orderbook(token_id: str): 300 | """获取订单簿""" 301 | try: 302 | client = get_opinion_client() 303 | ob = client.get_orderbook(token_id=token_id) 304 | 305 | bids = [{"price": float(b.price), "size": float(b.size)} for b in (ob.result.bids or [])] 306 | asks = [{"price": float(a.price), "size": float(a.size)} for a in (ob.result.asks or [])] 307 | 308 | return { 309 | "bids": bids, 310 | "asks": asks, 311 | "bid_count": len(bids), 312 | "ask_count": len(asks) 313 | } 314 | except Exception as e: 315 | raise HTTPException(status_code=500, detail=str(e)) 316 | 317 | @app.post("/api/refresh") 318 | async def force_refresh(): 319 | """强制刷新数据""" 320 | await refresh_data() 321 | return {"message": "Data refreshed", "updated_at": cache["updated_at"].isoformat()} 322 | 323 | @app.get("/api/stats") 324 | async def get_stats(): 325 | """获取统计数据""" 326 | if cache["data"] is None: 327 | await refresh_data() 328 | 329 | data = cache["data"] 330 | return { 331 | "total_markets": len(data.markets), 332 | "total_volume": data.total_volume, 333 | "whale_count": data.whale_count, 334 | "top_markets": [ 335 | {"id": m.market_id, "title": m.title, "volume": m.volume} 336 | for m in sorted(data.markets, key=lambda x: x.volume, reverse=True)[:5] 337 | ], 338 | "updated_at": data.updated_at 339 | } 340 | 341 | # Background task to refresh data periodically 342 | @app.on_event("startup") 343 | async def startup_event(): 344 | asyncio.create_task(refresh_data()) 345 | 346 | # 启动定时刷新任务 347 | async def periodic_refresh(): 348 | while True: 349 | await asyncio.sleep(60) # 每60秒刷新一次 350 | try: 351 | await refresh_data() 352 | except Exception as e: 353 | print(f"Error in periodic refresh: {e}") 354 | 355 | if API_KEY: 356 | asyncio.create_task(periodic_refresh()) 357 | 358 | if __name__ == "__main__": 359 | import uvicorn 360 | import os 361 | port = int(os.environ.get("PORT", 8000)) 362 | uvicorn.run(app, host="0.0.0.0", port=port) 363 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | OpinionWhale 7 | 8 | 9 | 32 | 61 | 62 | 63 | 64 |
65 |
66 |
67 | Logo 68 |

OpinionWhale

69 |
70 | 71 |
72 | 73 | 85 | 86 |
87 | 88 |
89 | 90 | 实时 91 |
92 | -- 93 | 96 |
97 |
98 |
99 | 100 |
101 | 102 |
103 |
104 |
活跃市场
105 |
--
106 |
107 |
108 |
总交易量
109 |
--
110 |
111 |
112 |
巨鲸订单
113 |
--
114 |
115 |
116 |
最大买墙
117 |
--
118 |
119 |
120 | 121 | 122 |
123 | 126 | 129 | 132 |
133 | 134 | 135 |
136 |
137 | 138 |
139 |
140 |
141 | 巨鲸动态 142 | 0 143 |
144 | 150 |
151 |
152 |
加载中...
153 |
154 |
155 | 156 | 157 |
158 | 159 |
160 |
161 | 买墙排行 TOP 5 162 |
163 |
164 |
加载中...
165 |
166 |
167 | 168 | 169 |
170 |
171 | 卖墙排行 TOP 5 172 |
173 |
174 |
加载中...
175 |
176 |
177 |
178 |
179 |
180 | 181 | 182 | 219 | 220 | 221 | 234 |
235 | 236 | 237 | 246 | 247 | 612 | 613 | 614 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | OpinionWhale 7 | 8 | 9 | 32 | 61 | 62 | 63 | 64 |
65 |
66 |
67 | Logo 68 |

OpinionWhale

69 |
70 | 71 |
72 | 73 | 85 | 86 |
87 | 88 |
89 | 90 | 实时 91 |
92 | -- 93 | 96 |
97 |
98 |
99 | 100 |
101 | 102 |
103 |
104 |
活跃市场
105 |
--
106 |
107 |
108 |
总交易量
109 |
--
110 |
111 |
112 |
巨鲸订单
113 |
--
114 |
115 |
116 |
最大买墙
117 |
--
118 |
119 |
120 | 121 | 122 |
123 | 126 | 129 | 132 |
133 | 134 | 135 |
136 |
137 | 138 |
139 |
140 |
141 | 巨鲸动态 142 | 0 143 |
144 | 150 |
151 |
152 |
加载中...
153 |
154 |
155 | 156 | 157 |
158 | 159 |
160 |
161 | 买墙排行 TOP 5 162 |
163 |
164 |
加载中...
165 |
166 |
167 | 168 | 169 |
170 |
171 | 卖墙排行 TOP 5 172 |
173 |
174 |
加载中...
175 |
176 |
177 |
178 |
179 |
180 | 181 | 182 | 219 | 220 | 221 | 234 |
235 | 236 | 237 | 246 | 247 | 615 | 616 | 617 | --------------------------------------------------------------------------------