diff --git a/requirements.txt b/requirements.txt index 73c2d48..821719a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ openai==1.58.1 # HTTP client for notifications requests==2.31.0 +aiohttp>=3.9.0 # WebSocket client for realtime data websockets>=12.0 diff --git a/web/api.py b/web/api.py index 864d22d..e9e4dc1 100644 --- a/web/api.py +++ b/web/api.py @@ -3,9 +3,11 @@ FastAPI Web Service - 多周期交易状态展示 API """ import json import asyncio +import urllib.request +import ssl from datetime import datetime from pathlib import Path -from typing import Dict, Any, List +from typing import Dict, Any, List, Optional from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.staticfiles import StaticFiles @@ -15,8 +17,48 @@ from fastapi.responses import FileResponse STATE_FILE = Path(__file__).parent.parent / 'output' / 'paper_trading_state.json' SIGNAL_FILE = Path(__file__).parent.parent / 'output' / 'latest_signal.json' +# Binance API +BINANCE_PRICE_URL = "https://fapi.binance.com/fapi/v1/ticker/price?symbol=BTCUSDT" + app = FastAPI(title="Trading Dashboard", version="2.0.0") +# 全局价格缓存 +_current_price: float = 0.0 +_price_update_time: datetime = None + + +async def fetch_binance_price() -> Optional[float]: + """从 Binance 获取实时价格(使用标准库)""" + global _current_price, _price_update_time + try: + # 使用线程池执行同步请求,避免阻塞事件循环 + loop = asyncio.get_event_loop() + price = await loop.run_in_executor(None, _fetch_price_sync) + if price: + _current_price = price + _price_update_time = datetime.now() + return _current_price + except Exception as e: + print(f"Error fetching Binance price: {type(e).__name__}: {e}") + return _current_price if _current_price > 0 else None + + +def _fetch_price_sync() -> Optional[float]: + """同步获取价格""" + try: + # 创建 SSL 上下文 + ctx = ssl.create_default_context() + req = urllib.request.Request( + BINANCE_PRICE_URL, + headers={'User-Agent': 'Mozilla/5.0'} + ) + with urllib.request.urlopen(req, timeout=5, context=ctx) as response: + data = json.loads(response.read().decode('utf-8')) + return float(data['price']) + except Exception as e: + print(f"Sync fetch error: {type(e).__name__}: {e}") + return None + class ConnectionManager: def __init__(self): @@ -262,6 +304,9 @@ async def websocket_endpoint(websocket: WebSocket): await manager.connect(websocket) try: + # 获取初始实时价格 + current_price = await fetch_binance_price() + # 发送初始状态 state = load_trading_state() signal = load_latest_signal() @@ -269,15 +314,30 @@ async def websocket_endpoint(websocket: WebSocket): 'type': 'init', 'state': state, 'signal': signal, + 'current_price': current_price, }) # 持续推送更新 last_state_mtime = STATE_FILE.stat().st_mtime if STATE_FILE.exists() else 0 last_signal_mtime = SIGNAL_FILE.stat().st_mtime if SIGNAL_FILE.exists() else 0 + last_price = current_price + price_update_counter = 0 while True: await asyncio.sleep(1) + price_update_counter += 1 + # 每秒获取实时价格并推送 + current_price = await fetch_binance_price() + if current_price and current_price != last_price: + last_price = current_price + await websocket.send_json({ + 'type': 'price_update', + 'current_price': current_price, + 'timestamp': datetime.now().isoformat(), + }) + + # 检查状态文件更新 current_state_mtime = STATE_FILE.stat().st_mtime if STATE_FILE.exists() else 0 current_signal_mtime = SIGNAL_FILE.stat().st_mtime if SIGNAL_FILE.exists() else 0 diff --git a/web/static/index.html b/web/static/index.html index ed1281b..ccb667b 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -283,10 +283,25 @@ ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'init') { - // 先更新信号获取当前价格,再更新状态 + // 先更新价格,再更新信号和状态 + if (data.current_price) { + currentPrice = data.current_price; + document.getElementById('current-price').textContent = `$${currentPrice.toLocaleString('en-US', {minimumFractionDigits: 2})}`; + } updateSignal(data.signal); updateState(data.state); } + else if (data.type === 'price_update') { + // 实时价格更新 + if (data.current_price) { + currentPrice = data.current_price; + document.getElementById('current-price').textContent = `$${currentPrice.toLocaleString('en-US', {minimumFractionDigits: 2})}`; + // 重新计算 PnL + if (lastState) { + updateState(lastState); + } + } + } else if (data.type === 'state_update') { updateState(data.state); }