Files
tick-stock-panel/backend/app/main.py
T
shy3130 2015ef0c78 feat: 个股分析(Beta) — 专用日K + 9类关键价位 + AI四维分析
新增个股分析页,以行情与关键价位为视觉主体,与财务分析(财务质量评级)定位互补:

后端:
- indicators/levels.py: 9 类关键价位纯函数计算
  · 压力支撑(布林带)/ 成交密集区(POC)/ 枢轴点(可配档位)/ 前高前低
  · Keltner通道(MA±n×ATR 短/中/长)/ ATR止损(close±nATR)
  · 缺口位(未回补跳空)/ 斐波那契回撤 / 整数关口(自适应步长)
- services/stock_analyzer.py: AI 四维(技术/基本面/财务/消息面)流式分析,NDJSON
- services/stock_reports.py: 报告持久化(最多50条),独立 JSON 存储
- api/stock_analysis.py: levels/analyze/reports 端点

前端:
- AnalysisKChart: 专用 ECharts 日K(主图+成交量+滑块),9 类价位开关
- 蓝色胶囊(Bubble/Host/Dialog),与财务分析紫色主题并存
- stockAnalysisStore: useSyncExternalStore 全局存储,后台流式
- useLastStock + LastStockChip: 记忆最近查看个股(财务/个股分析页)

其他优化:
- AI 设置页:一键清空配置(保留UA) + 保存后乐观更新连接状态/菜单
- MarkdownRenderer: 去除 space-y-0 遮蔽,各块间距与分隔线可见性优化
- 个股分析页 Beta 标识(标题 + 菜单)
- 版本号 0.1.45 → 0.1.50
2026-06-26 15:36:52 +08:00

238 lines
8.5 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, stock_analysis, 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 import client as tf_client
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__, tf_client.current_mode(),
)
# 数据层
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)
monitor_engine.set_data_dir(store.data_dir)
# 自动迁移: 把旧 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(stock_analysis.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)
# 能力门控异常 → 403(而非默认 500)
# 业务代码用 capset.require(Cap.X) 断言能力,缺失时抛 CapabilityDenied;
# 若不注册 handler 会冒泡成 500 Internal Server Error,对前端不友好且语义错误。
from fastapi import Request
from fastapi.responses import JSONResponse
from app.tickflow.capabilities import CapabilityDenied
@app.exception_handler(CapabilityDenied)
async def capability_denied_handler(request: Request, exc: CapabilityDenied) -> JSONResponse:
return JSONResponse(
status_code=403,
content={"detail": str(exc), "suggestion": exc.suggestion},
)
# 生产期静态文件(前端 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"}