mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 17:54:15 +08:00
171 lines
5.5 KiB
Python
171 lines
5.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, 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(
|
|
"TF-Stocks-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)
|
|
|
|
# 启动调度器(若 enriched 数据为空,首次启动可手动 POST /api/pipeline/run)
|
|
try:
|
|
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
|
|
|
|
# 扩展数据定时拉取
|
|
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()))
|
|
|
|
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()
|
|
logger.info("shutdown")
|
|
|
|
|
|
app = FastAPI(
|
|
title="TF-Stocks-Panel",
|
|
version=__version__,
|
|
description="A 股选股 + 回测面板 — TickFlow 适配",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
# 开发期 CORS 允许 Vite dev server
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["http://localhost:3011", "http://127.0.0.1:3011"],
|
|
allow_credentials=True,
|
|
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)
|
|
|
|
# 生产期静态文件(前端 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"}
|