218 lines
7.1 KiB
Python
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) |