trading.ai/web_app.py
2025-08-14 10:06:19 +08:00

218 lines
7.1 KiB
Python

from fastapi import FastAPI, Request, HTTPException
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, JSONResponse
import uvicorn
import os
from coin_selection_engine import CoinSelectionEngine
from datetime import datetime, timezone, timedelta
from database import utc_to_beijing
import asyncio
from functools import lru_cache
import time
# 东八区时区
BEIJING_TZ = timezone(timedelta(hours=8))
def get_beijing_time():
"""获取当前东八区时间用于显示"""
return datetime.now(BEIJING_TZ)
app = FastAPI(title="加密货币选币系统", version="1.0.0")
# 设置模板和静态文件目录
templates = Jinja2Templates(directory="templates")
# 创建静态文件目录
os.makedirs("static", exist_ok=True)
app.mount("/static", StaticFiles(directory="static"), name="static")
# 全局变量
engine = CoinSelectionEngine()
# 缓存配置
CACHE_DURATION = 60 # 缓存60秒
cache_data = {
'selections': {'data': None, 'timestamp': 0},
'stats': {'data': None, 'timestamp': 0}
}
def get_cached_data(cache_key, fetch_func, *args, **kwargs):
"""获取缓存数据或重新获取"""
current_time = time.time()
cache_entry = cache_data.get(cache_key, {'data': None, 'timestamp': 0})
# 检查缓存是否过期
if current_time - cache_entry['timestamp'] > CACHE_DURATION or cache_entry['data'] is None:
cache_entry['data'] = fetch_func(*args, **kwargs)
cache_entry['timestamp'] = current_time
cache_data[cache_key] = cache_entry
return cache_entry['data']
@app.get("/", response_class=HTMLResponse)
async def dashboard(request: Request, limit: int = 20, offset: int = 0):
"""主页面 - 支持分页"""
try:
# 使用缓存获取数据
selections = get_cached_data(
f'selections_{limit}_{offset}',
lambda: engine.get_latest_selections(limit + 5, offset)
)
# 按年月日时分分组选币结果,转换时间为东八区显示
grouped_selections = {}
latest_update_time = None
for selection in selections:
# 将UTC时间转换为东八区时间显示
utc_time = selection['selection_time']
beijing_time = utc_to_beijing(utc_time)
# 跟踪最新的更新时间
if latest_update_time is None or utc_time > latest_update_time:
latest_update_time = beijing_time
# 提取年月日时分部分 (YYYY-MM-DD HH:MM)
time_key = beijing_time[:16] # "YYYY-MM-DD HH:MM"
if time_key not in grouped_selections:
grouped_selections[time_key] = []
# 更新选币记录的显示时间,但不修改原始时间
selection_copy = selection.copy()
selection_copy['selection_time'] = beijing_time
grouped_selections[time_key].append(selection_copy)
# 按时间降序排序(最新的在前面)
sorted_grouped_selections = dict(sorted(
grouped_selections.items(),
key=lambda x: x[0],
reverse=True
))
return templates.TemplateResponse("dashboard.html", {
"request": request,
"grouped_selections": sorted_grouped_selections,
"last_update": latest_update_time + " CST" if latest_update_time else "暂无数据",
"total_count": len(selections),
"current_limit": limit,
"current_offset": offset
})
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/selections")
async def get_selections(limit: int = 20, offset: int = 0):
"""获取选币结果API - 支持分页"""
try:
# 限制每页数据量
limit = min(limit, 100)
# 使用缓存
selections = get_cached_data(
f'selections_{limit}_{offset}',
lambda: engine.get_latest_selections(limit, offset)
)
return JSONResponse({
"status": "success",
"data": selections,
"count": len(selections),
"limit": limit,
"offset": offset
})
except Exception as e:
return JSONResponse({
"status": "error",
"message": str(e)
}, status_code=500)
@app.post("/api/run_selection")
async def run_selection():
"""执行选币API"""
try:
# 清除相关缓存
cache_data.clear()
# 异步执行选币
selected_coins = engine.run_coin_selection()
return JSONResponse({
"status": "success",
"message": f"选币完成,共选出{len(selected_coins)}个币种",
"count": len(selected_coins)
})
except Exception as e:
return JSONResponse({
"status": "error",
"message": str(e)
}, status_code=500)
@app.put("/api/selections/{selection_id}/status")
async def update_selection_status(selection_id: int, status: str, exit_price: float = None):
"""更新选币状态API"""
try:
# 清除相关缓存
for key in list(cache_data.keys()):
if 'selections' in key or 'stats' in key:
del cache_data[key]
engine.update_selection_status(selection_id, status, exit_price)
return JSONResponse({
"status": "success",
"message": f"更新选币{selection_id}状态为{status}"
})
except Exception as e:
return JSONResponse({
"status": "error",
"message": str(e)
}, status_code=500)
@app.get("/api/stats")
async def get_stats():
"""获取统计信息API"""
try:
# 使用缓存获取统计数据
stats = get_cached_data('stats', lambda: _get_fresh_stats())
return JSONResponse({
"status": "success",
"data": stats
})
except Exception as e:
return JSONResponse({
"status": "error",
"message": str(e)
}, status_code=500)
def _get_fresh_stats():
"""获取新鲜的统计数据"""
conn = engine.db.get_connection()
cursor = conn.cursor()
# 总选币数
cursor.execute("SELECT COUNT(*) FROM coin_selections")
total_selections = cursor.fetchone()[0]
# 活跃选币数
cursor.execute("SELECT COUNT(*) FROM coin_selections WHERE status = 'active'")
active_selections = cursor.fetchone()[0]
# 完成选币数
cursor.execute("SELECT COUNT(*) FROM coin_selections WHERE status = 'completed'")
completed_selections = cursor.fetchone()[0]
# 平均收益率
cursor.execute("SELECT AVG(pnl_percentage) FROM coin_selections WHERE pnl_percentage IS NOT NULL")
avg_pnl = cursor.fetchone()[0] or 0
conn.close()
return {
"total_selections": total_selections,
"active_selections": active_selections,
"completed_selections": completed_selections,
"avg_pnl": round(avg_pnl, 2)
}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)