255 lines
8.6 KiB
Python
255 lines
8.6 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] = {}
|
||
|
||
# 按时间级别进行二级分组
|
||
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) |