trading.ai/web_app.py
2025-08-17 21:27:42 +08:00

255 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)