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] = {} # 按时间级别进行二级分组 timeframe = selection.get('timeframe', '未知') if timeframe not in grouped_selections[time_key]: grouped_selections[time_key][timeframe] = [] # 更新选币记录的显示时间,但不修改原始时间 selection_copy = selection.copy() selection_copy['selection_time'] = beijing_time grouped_selections[time_key][timeframe].append(selection_copy) # 按时间降序排序(最新的在前面) sorted_grouped_selections = dict(sorted( grouped_selections.items(), key=lambda x: x[0], reverse=True )) # 对每个时间组内的时间级别进行排序(15m, 1h, 4h, 1d) timeframe_order = {'15m': 1, '1h': 2, '4h': 3, '1d': 4} for time_key in sorted_grouped_selections: sorted_timeframes = dict(sorted( sorted_grouped_selections[time_key].items(), key=lambda x: timeframe_order.get(x[0], 99) )) sorted_grouped_selections[time_key] = sorted_timeframes return templates.TemplateResponse("dashboard_table.html", { "request": request, "grouped_selections": sorted_grouped_selections, "update_time": latest_update_time.split(' ')[1][:5] if latest_update_time else "--:--", "total_signals": len(selections), "long_signals": len([s for s in selections if s.get('signal_type') == 'LONG']), "short_signals": len([s for s in selections if s.get('signal_type') == 'SHORT']), "total_count": len(selections), "current_limit": limit }) 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.get("/api/refresh") async def refresh_data(): """刷新数据API - 仅刷新显示数据,不执行选币""" try: # 清除缓存 cache_data.clear() # 获取现有数据进行显示 selections = engine.get_latest_selections(50, 0) return JSONResponse({ "success": True, "message": f"数据刷新完成,当前有{len(selections)}个选币记录", "count": len(selections) }) except Exception as e: return JSONResponse({ "success": False, "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)