mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 17:54:15 +08:00
监控引擎 (backend): - MonitorRuleEngine 统一规则引擎,支持策略/信号/价格/行情四种类型 - JSONL 追加存储 (alert_store.py),支持分页 + 清理 - alert/monitor_rules CRUD API + seed 演示数据 - POST /api/kline/instruments/names 批量股票名称查询 - SSE strategy_alert 事件触发前端通知 前端通知体系: - AlertToast 自定义弹窗 (Framer Motion + AnimatePresence) - Web Audio API 合成音效 (12种预设,无需音频文件) - 侧边栏角标 (monitorBadge.ts) localStorage 持久化未读数 - pendingSeen 机制解决 markSeen/setCurrentTotal 竞态 - 系统设置页: 通知开关 + 最大条数 + 音效选择 - 菜单设置页: 角标数字开关 监控中心 (Monitor.tsx): - 双栏布局: 左侧实时告警列表 + 右侧规则管理 - RulesList 股票代码后显示中文名称 - RuleEditor 完整规则编辑器组件 - 告警支持查看详情 (StockPreviewDialog) Dashboard: - MonitorWidget: Top 10 实时告警卡片 Dev 页面: - 一键填充演示告警 + 规则 + 分钟探测 - 可视化 SSE 事件流查看 其他: - v0.1.19 → v0.1.28 (VERSION 从 pyproject.toml 读取) - dev.ps1/dev.sh 添加 --host 0.0.0.0 (局域网访问) - 修复 Screener.tsx presets.length 可选链 - README 截图表格更新 (6张) + 监控章节重写 - 删除 MinuteDataProbe 页面 (合并至 Dev)
219 lines
7.8 KiB
Python
219 lines
7.8 KiB
Python
"""FastAPI 入口。"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import FileResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
from app import __version__
|
|
from app.api import analysis, backtest, data, ext_data, financials, indices, intraday, kline, monitor_rules, alerts, overview, pipeline, screener, settings as settings_api, signals, strategy, watchlist
|
|
from app.api.routes import router as core_router
|
|
from app.config import settings
|
|
from app.jobs import daily_pipeline
|
|
from app.services.quote_service import QuoteService
|
|
from app.tickflow.policy import detect_capabilities
|
|
from app.tickflow.repository import DataStore, KlineRepository
|
|
|
|
logging.basicConfig(
|
|
level=settings.log_level,
|
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
logger.info(
|
|
"TickFlow Stock Panel v%s starting (mode=%s)",
|
|
__version__, "free" if settings.use_free_mode else "api_key",
|
|
)
|
|
|
|
# 数据层
|
|
store = DataStore()
|
|
repo = KlineRepository(store)
|
|
app.state.datastore = store
|
|
app.state.repo = repo
|
|
|
|
# Polars 缓存预热
|
|
repo.refresh_cache()
|
|
|
|
# 能力探测
|
|
capset = detect_capabilities()
|
|
app.state.capabilities = capset
|
|
logger.info("ready; %d capabilities active", len(capset.all()))
|
|
|
|
# 全局行情服务
|
|
qs = QuoteService()
|
|
app.state.quote_service = qs
|
|
qs.set_repo(repo)
|
|
qs.boot_check()
|
|
|
|
# QuoteService 需要访问 strategy_monitor 等单例
|
|
# 先创建 strategy_monitor,再注入 app.state
|
|
from app.strategy.monitor import StrategyMonitorService
|
|
strategy_monitor = StrategyMonitorService()
|
|
app.state.strategy_monitor = strategy_monitor
|
|
qs.set_app_state(app.state)
|
|
|
|
# 五档盘口 sealed 服务(真假涨停/跌停, 独立旁路线)
|
|
from app.services.depth_service import DepthService
|
|
depth_service = DepthService()
|
|
depth_service.set_repo(repo)
|
|
depth_service.set_app_state(app.state)
|
|
app.state.depth_service = depth_service
|
|
|
|
# 启动调度器(若 enriched 数据为空,首次启动可手动 POST /api/pipeline/run)
|
|
try:
|
|
daily_pipeline.set_app_state(app.state) # 供 depth_finalize job 访问 depth_service
|
|
scheduler = daily_pipeline.start_scheduler(repo, capset)
|
|
app.state.scheduler = scheduler
|
|
except Exception as e: # noqa: BLE001
|
|
logger.warning("scheduler not started: %s", e)
|
|
app.state.scheduler = None
|
|
|
|
# depth sealed: 启动补跑(当天文件不存在) + 盘中轮询(有能力时)
|
|
try:
|
|
depth_service.boot_check()
|
|
depth_service.start_polling()
|
|
except Exception as e: # noqa: BLE001
|
|
logger.warning("depth_service init failed: %s", e)
|
|
|
|
# 扩展数据定时拉取
|
|
from app.services.ext_pull import pull_scheduler
|
|
pull_scheduler.start(store.data_dir)
|
|
pull_scheduler.refresh(store.data_dir)
|
|
app.state.pull_scheduler = pull_scheduler
|
|
|
|
# 财务数据独立调度 (需 Expert 套餐)
|
|
from app.services.financial_sync import financial_scheduler
|
|
financial_scheduler.start(store.data_dir, capset)
|
|
app.state.financial_scheduler = financial_scheduler
|
|
|
|
# 策略引擎
|
|
from app.strategy.engine import StrategyEngine
|
|
from app.strategy.monitor import StrategyMonitorService
|
|
from app.services.screener import ScreenerService
|
|
|
|
_screener_svc = ScreenerService(repo)
|
|
strategy_dirs = [
|
|
Path(__file__).resolve().parent / "strategy" / "builtin",
|
|
store.data_dir / "strategies" / "custom",
|
|
store.data_dir / "strategies" / "ai",
|
|
]
|
|
strategy_engine = StrategyEngine(
|
|
enriched_loader=_screener_svc._load_enriched_for_date,
|
|
enriched_history_loader=_screener_svc._load_enriched_history,
|
|
strategy_dirs=strategy_dirs,
|
|
)
|
|
app.state.strategy_engine = strategy_engine
|
|
logger.info("strategy engine loaded: %d strategies", len(strategy_engine.list_strategies()))
|
|
|
|
# 通用监控规则引擎: 启动时 reload 规则到内存态 (修复重启后告警失效)
|
|
from app.strategy.monitor import MonitorRuleEngine
|
|
from app.strategy import monitor_rules as mr_store
|
|
from app.services import preferences
|
|
monitor_engine = MonitorRuleEngine()
|
|
monitor_engine.set_strategy_engine(strategy_engine)
|
|
|
|
# 自动迁移: 把旧 strategy_monitor_ids 同步为 type=strategy 规则 (统一到监控页)
|
|
try:
|
|
if preferences.get_strategy_monitor_enabled():
|
|
ids = preferences.get_strategy_monitor_ids()
|
|
if ids:
|
|
names = {s.id: s.name for s in strategy_engine.list_strategies()}
|
|
mr_store.migrate_strategy_monitors(store.data_dir, ids, names)
|
|
logger.info("strategy monitor migrated: %d strategies", len(ids))
|
|
except Exception as e: # noqa: BLE001
|
|
logger.warning("strategy monitor migration failed: %s", e)
|
|
|
|
try:
|
|
rules = mr_store.load_all(store.data_dir)
|
|
monitor_engine.set_rules(rules)
|
|
logger.info("monitor engine loaded: %d rules", monitor_engine.rule_count)
|
|
except Exception as e: # noqa: BLE001
|
|
logger.warning("monitor engine load failed: %s", e)
|
|
app.state.monitor_engine = monitor_engine
|
|
|
|
yield
|
|
|
|
if app.state.scheduler:
|
|
app.state.scheduler.shutdown(wait=False)
|
|
ps = getattr(app.state, "pull_scheduler", None)
|
|
if ps:
|
|
ps.stop()
|
|
fsc = getattr(app.state, "financial_scheduler", None)
|
|
if fsc:
|
|
fsc.stop()
|
|
qs = getattr(app.state, "quote_service", None)
|
|
if qs:
|
|
qs.stop()
|
|
dsvc = getattr(app.state, "depth_service", None)
|
|
if dsvc:
|
|
dsvc.stop_polling()
|
|
logger.info("shutdown")
|
|
|
|
|
|
app = FastAPI(
|
|
title="TickFlow Stock Panel",
|
|
version=__version__,
|
|
description="A 股选股 + 回测面板 — TickFlow 适配",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
# CORS: 允许局域网访问 (自托管场景, 放开所有来源)
|
|
# 注: allow_credentials=True 与 allow_origins=['*'] 不能共存 (浏览器规范),
|
|
# 本项目认证走 header (API Key), 不依赖 cookie, 故关闭 credentials 换取通配来源。
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=False,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# 路由
|
|
app.include_router(core_router)
|
|
app.include_router(kline.router)
|
|
app.include_router(watchlist.router)
|
|
app.include_router(screener.router)
|
|
app.include_router(backtest.router)
|
|
app.include_router(intraday.router)
|
|
app.include_router(indices.router)
|
|
app.include_router(overview.router)
|
|
app.include_router(analysis.router)
|
|
app.include_router(pipeline.router)
|
|
app.include_router(data.router)
|
|
app.include_router(ext_data.router)
|
|
app.include_router(financials.router)
|
|
app.include_router(settings_api.router)
|
|
app.include_router(strategy.router)
|
|
app.include_router(signals.router)
|
|
app.include_router(monitor_rules.router)
|
|
app.include_router(alerts.router)
|
|
|
|
# 生产期静态文件(前端 dist)
|
|
_static = Path(settings.static_dir)
|
|
if _static.exists():
|
|
if (_static / "assets").exists():
|
|
app.mount("/assets", StaticFiles(directory=_static / "assets"), name="assets")
|
|
|
|
@app.get("/{full_path:path}", include_in_schema=False)
|
|
def spa_fallback(full_path: str): # noqa: ARG001
|
|
"""所有未匹配路径回退到 index.html — React Router 接管。
|
|
|
|
index.html 禁止缓存 (Cache-Control: no-store), 确保浏览器每次拿到
|
|
最新版本引用的 JS/CSS 文件名 (assets 带 hash, 可长缓存)。
|
|
"""
|
|
index = _static / "index.html"
|
|
if index.exists():
|
|
return FileResponse(
|
|
index,
|
|
headers={"Cache-Control": "no-store, must-revalidate"},
|
|
)
|
|
return {"error": "frontend not built"}
|