2025-12-16 00:04:02 +08:00
|
|
|
|
# filename: main.py
|
|
|
|
|
|
|
|
|
|
|
|
import subprocess
|
|
|
|
|
|
import os
|
|
|
|
|
|
import logging
|
|
|
|
|
|
from collections import deque
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
2026-01-26 01:21:46 +08:00
|
|
|
|
from contextlib import asynccontextmanager
|
2025-11-21 16:08:03 +08:00
|
|
|
|
from fastapi import FastAPI, HTTPException, Query
|
|
|
|
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
|
|
from fastapi.responses import FileResponse
|
|
|
|
|
|
|
|
|
|
|
|
# 引入调度器
|
|
|
|
|
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
|
|
|
|
from apscheduler.triggers.cron import CronTrigger
|
|
|
|
|
|
|
|
|
|
|
|
# 复用现有manager
|
|
|
|
|
|
from core.manager import StrategyManager
|
|
|
|
|
|
|
|
|
|
|
|
# ================== 初始化 ==================
|
|
|
|
|
|
app = FastAPI(title="策略控制台")
|
|
|
|
|
|
manager = StrategyManager()
|
|
|
|
|
|
|
|
|
|
|
|
# 初始化调度器
|
|
|
|
|
|
scheduler = AsyncIOScheduler()
|
|
|
|
|
|
|
|
|
|
|
|
# 配置日志
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-16 00:04:02 +08:00
|
|
|
|
# ================== 新增:获取 Git 版本信息 ==================
|
|
|
|
|
|
def get_git_commit_info():
|
|
|
|
|
|
"""
|
|
|
|
|
|
[智能版] 获取 Git 仓库的最新提交信息。
|
|
|
|
|
|
此函数会自动从当前文件位置向上查找项目根目录(.git 文件夹所在位置)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 1. 获取 main.py 所在的目录
|
|
|
|
|
|
script_dir = Path(__file__).resolve().parent
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 推断出项目根目录 (即 main.py 所在目录的上一级)
|
|
|
|
|
|
project_root = script_dir.parent
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 检查项目根目录下是否存在 .git 文件夹
|
|
|
|
|
|
git_dir = project_root / ".git"
|
|
|
|
|
|
if not git_dir.is_dir():
|
|
|
|
|
|
return "Git repo not found in parent directory"
|
|
|
|
|
|
|
|
|
|
|
|
# 4. 在推断出的项目根目录下执行 git 命令
|
|
|
|
|
|
# 使用 cwd (current working directory) 参数是关键
|
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
|
['git', 'log', '-1', '--pretty=format:%h - %s (%cr)'],
|
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
|
|
text=True,
|
|
|
|
|
|
check=True,
|
|
|
|
|
|
encoding='utf-8',
|
|
|
|
|
|
cwd=project_root # <--- 在这里指定命令的执行目录
|
|
|
|
|
|
)
|
|
|
|
|
|
return result.stdout.strip()
|
|
|
|
|
|
except (FileNotFoundError, subprocess.CalledProcessError) as e:
|
|
|
|
|
|
logger.warning(f"无法获取 Git 提交信息: {e}")
|
|
|
|
|
|
return "获取 Git 信息失败"
|
|
|
|
|
|
|
2026-01-26 01:21:46 +08:00
|
|
|
|
# ================== 定时任务逻辑 ==================
|
2025-11-21 16:08:03 +08:00
|
|
|
|
|
|
|
|
|
|
def scheduled_restart_task():
|
|
|
|
|
|
"""
|
|
|
|
|
|
定时任务:重启所有正在运行的策略
|
|
|
|
|
|
"""
|
|
|
|
|
|
logger.info("⏰ [定时任务] 触发自动重启流程...")
|
|
|
|
|
|
|
|
|
|
|
|
# 获取当前所有策略状态
|
|
|
|
|
|
status = manager.get_status()
|
|
|
|
|
|
running_strategies = [
|
|
|
|
|
|
name for name, info in status['strategies'].items()
|
|
|
|
|
|
if info['status'] == 'running'
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
if not running_strategies:
|
|
|
|
|
|
logger.info("⏰ [定时任务] 当前无运行中的策略,无需重启")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"⏰ [定时任务] 即将重启以下策略: {running_strategies}")
|
|
|
|
|
|
|
|
|
|
|
|
for name in running_strategies:
|
|
|
|
|
|
try:
|
|
|
|
|
|
manager.restart_strategy(name)
|
|
|
|
|
|
logger.info(f"✅ [定时任务] {name} 重启成功")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"❌ [定时任务] {name} 重启失败: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
logger.info("⏰ [定时任务] 自动重启流程结束")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-26 01:21:46 +08:00
|
|
|
|
def scheduled_whitelist_auto_start():
|
|
|
|
|
|
"""
|
|
|
|
|
|
定时任务:白名单自动启动
|
|
|
|
|
|
仅在 08:58 执行
|
|
|
|
|
|
"""
|
|
|
|
|
|
logger.info("⏰ [白名单定时任务] 触发白名单自动启动...")
|
|
|
|
|
|
|
|
|
|
|
|
results = manager.auto_start_whitelist_strategies()
|
|
|
|
|
|
|
|
|
|
|
|
if not results:
|
|
|
|
|
|
logger.info("⏰ [白名单定时任务] 今天已执行过或无需启动")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
success_count = sum(1 for v in results.values() if v)
|
|
|
|
|
|
fail_count = len(results) - success_count
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"⏰ [白名单定时任务] 完成: 成功 {success_count}, 失败 {fail_count}")
|
|
|
|
|
|
|
|
|
|
|
|
for name, success in results.items():
|
|
|
|
|
|
if success:
|
|
|
|
|
|
logger.info(f"✅ [白名单定时任务] {name} 启动成功")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.error(f"❌ [白名单定时任务] {name} 启动失败")
|
|
|
|
|
|
|
2025-11-21 16:08:03 +08:00
|
|
|
|
|
2026-01-26 01:21:46 +08:00
|
|
|
|
# ================== FastAPI 生命周期事件 (使用 lifespan 替代废弃的 on_event) ==================
|
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
|
|
async def lifespan(app: FastAPI):
|
|
|
|
|
|
"""
|
|
|
|
|
|
FastAPI 生命周期管理器,替代废弃的 @app.on_event 装饰器
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 原有重启任务 (08:58, 20:58)
|
2025-11-21 16:08:03 +08:00
|
|
|
|
scheduler.add_job(
|
|
|
|
|
|
scheduled_restart_task,
|
2025-11-29 16:35:02 +08:00
|
|
|
|
CronTrigger(hour=8, minute=58),
|
2025-11-21 16:08:03 +08:00
|
|
|
|
id="restart_morning",
|
|
|
|
|
|
replace_existing=True
|
|
|
|
|
|
)
|
|
|
|
|
|
scheduler.add_job(
|
|
|
|
|
|
scheduled_restart_task,
|
2025-11-29 16:35:02 +08:00
|
|
|
|
CronTrigger(hour=20, minute=58),
|
2025-11-21 16:08:03 +08:00
|
|
|
|
id="restart_evening",
|
|
|
|
|
|
replace_existing=True
|
|
|
|
|
|
)
|
2026-01-26 01:21:46 +08:00
|
|
|
|
|
|
|
|
|
|
# 新增:白名单自动启动任务(仅 08:58)
|
|
|
|
|
|
scheduler.add_job(
|
|
|
|
|
|
scheduled_whitelist_auto_start,
|
|
|
|
|
|
CronTrigger(hour=8, minute=58),
|
|
|
|
|
|
id="whitelist_auto_start",
|
|
|
|
|
|
replace_existing=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-11-21 16:08:03 +08:00
|
|
|
|
scheduler.start()
|
2026-01-26 01:21:46 +08:00
|
|
|
|
logger.info("📅 定时任务调度器已启动")
|
|
|
|
|
|
logger.info(" - 重启任务: 08:58, 20:58")
|
|
|
|
|
|
logger.info(" - 白名单自动启动: 08:58")
|
2025-11-21 16:08:03 +08:00
|
|
|
|
|
2026-01-26 01:21:46 +08:00
|
|
|
|
yield
|
2025-11-21 16:08:03 +08:00
|
|
|
|
|
2026-01-26 01:21:46 +08:00
|
|
|
|
# 应用关闭时执行
|
2025-11-21 16:08:03 +08:00
|
|
|
|
scheduler.shutdown()
|
2026-01-26 01:21:46 +08:00
|
|
|
|
logger.info("📅 定时任务调度器已关闭")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ================== 初始化 ==================
|
|
|
|
|
|
app = FastAPI(title="策略控制台", lifespan=lifespan)
|
2025-11-21 16:08:03 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-12-16 00:04:02 +08:00
|
|
|
|
# ================== API 路由 ==================
|
2025-11-21 16:08:03 +08:00
|
|
|
|
|
|
|
|
|
|
@app.get("/api/status")
|
|
|
|
|
|
def get_status():
|
2025-12-16 00:04:02 +08:00
|
|
|
|
# [核心修改] 在返回状态的同时,注入 Git 版本信息
|
|
|
|
|
|
status_data = manager.get_status()
|
|
|
|
|
|
status_data['git_info'] = get_git_commit_info()
|
|
|
|
|
|
return status_data
|
2025-11-21 16:08:03 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/api/strategy/{name}/start")
|
|
|
|
|
|
def start_strategy(name: str):
|
|
|
|
|
|
if manager.start_strategy(name):
|
|
|
|
|
|
return {"success": True}
|
|
|
|
|
|
raise HTTPException(400, "启动失败")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/api/strategy/{name}/stop")
|
|
|
|
|
|
def stop_strategy(name: str):
|
|
|
|
|
|
if manager.stop_strategy(name):
|
|
|
|
|
|
return {"success": True}
|
|
|
|
|
|
raise HTTPException(400, "停止失败")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/api/strategy/{name}/restart")
|
|
|
|
|
|
def restart_strategy(name: str):
|
|
|
|
|
|
if manager.restart_strategy(name):
|
|
|
|
|
|
return {"success": True}
|
|
|
|
|
|
raise HTTPException(400, "重启失败,请检查日志")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/api/logs/{name}")
|
|
|
|
|
|
def get_logs(name: str, lines: int = Query(50, le=500)):
|
|
|
|
|
|
try:
|
|
|
|
|
|
if '_' not in name:
|
|
|
|
|
|
return {"lines": []}
|
|
|
|
|
|
strategy_name, symbol = name.split('_', 1)
|
|
|
|
|
|
log_file = Path(f"logs/{strategy_name}/{symbol}.log")
|
|
|
|
|
|
|
|
|
|
|
|
if not log_file.exists():
|
|
|
|
|
|
return {"lines": ["日志文件尚未生成"]}
|
|
|
|
|
|
|
|
|
|
|
|
with open(log_file, 'r', encoding='utf-8', errors='replace') as f:
|
|
|
|
|
|
last_lines = deque(f, maxlen=lines)
|
|
|
|
|
|
return {"lines": [line.rstrip() for line in last_lines]}
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise HTTPException(500, f"读取日志失败: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-26 01:21:46 +08:00
|
|
|
|
# ================== 白名单管理 API ==================
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/api/whitelist")
|
|
|
|
|
|
def get_whitelist():
|
|
|
|
|
|
"""获取白名单列表"""
|
|
|
|
|
|
whitelist = manager.whitelist_manager.get_all()
|
|
|
|
|
|
auto_start_status = manager.whitelist_manager.get_auto_start_status()
|
|
|
|
|
|
return {
|
|
|
|
|
|
"whitelist": whitelist,
|
|
|
|
|
|
"auto_start_status": auto_start_status
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/api/whitelist/{name}/add")
|
|
|
|
|
|
def add_to_whitelist(name: str):
|
|
|
|
|
|
"""添加策略到白名单"""
|
|
|
|
|
|
if manager.add_to_whitelist(name):
|
|
|
|
|
|
return {"success": True, "message": f"已添加到白名单: {name}"}
|
|
|
|
|
|
raise HTTPException(400, f"添加失败,策略可能已存在: {name}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/api/whitelist/{name}/remove")
|
|
|
|
|
|
def remove_from_whitelist(name: str):
|
|
|
|
|
|
"""从白名单移除策略"""
|
|
|
|
|
|
if manager.remove_from_whitelist(name):
|
|
|
|
|
|
return {"success": True, "message": f"已从白名单移除: {name}"}
|
|
|
|
|
|
raise HTTPException(400, f"移除失败,策略可能不在白名单中: {name}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/api/whitelist/{name}/enable")
|
|
|
|
|
|
def enable_in_whitelist(name: str):
|
|
|
|
|
|
"""启用白名单中的策略"""
|
|
|
|
|
|
if manager.set_whitelist_enabled(name, True):
|
|
|
|
|
|
return {"success": True, "message": f"已启用: {name}"}
|
|
|
|
|
|
raise HTTPException(400, f"操作失败,策略可能不在白名单中: {name}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/api/whitelist/{name}/disable")
|
|
|
|
|
|
def disable_in_whitelist(name: str):
|
|
|
|
|
|
"""禁用白名单中的策略"""
|
|
|
|
|
|
if manager.set_whitelist_enabled(name, False):
|
|
|
|
|
|
return {"success": True, "message": f"已禁用: {name}"}
|
|
|
|
|
|
raise HTTPException(400, f"操作失败,策略可能不在白名单中: {name}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/api/whitelist/auto-start")
|
|
|
|
|
|
def trigger_auto_start():
|
|
|
|
|
|
"""手动触发白名单自动启动(用于测试)"""
|
|
|
|
|
|
results = manager.auto_start_whitelist_strategies()
|
|
|
|
|
|
return {
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"results": results,
|
|
|
|
|
|
"count": len(results),
|
|
|
|
|
|
"success_count": sum(1 for v in results.values() if v),
|
|
|
|
|
|
"fail_count": sum(1 for v in results.values() if not v)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ================== 静态文件挂载 ==================
|
|
|
|
|
|
# 服务前端构建文件
|
2025-11-21 16:08:03 +08:00
|
|
|
|
app.mount("/static", StaticFiles(directory="frontend/dist"), name="static")
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/")
|
|
|
|
|
|
def serve_frontend():
|
|
|
|
|
|
return FileResponse("frontend/dist/index.html")
|