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)