This commit is contained in:
aaron 2026-04-24 09:07:07 +08:00
parent a05ccfd1b4
commit 4dcacc5650
12 changed files with 164 additions and 22 deletions

View File

@ -8,6 +8,7 @@ import logging
import json
import asyncio
import traceback
from functools import partial
from datetime import datetime, timedelta
from app.engine.screener import run_screening
from app.data.models import Recommendation, MarketTemperature, SectorInfo
@ -21,6 +22,13 @@ _scan_lock = asyncio.Lock()
_scan_running = False
async def _run_async_in_worker(async_fn, *args, **kwargs):
"""在独立工作线程中运行重负载异步任务,避免阻塞主事件循环。"""
runner = partial(asyncio.run, async_fn(*args, **kwargs))
return await asyncio.to_thread(runner)
def _has_valid_market_breadth(market_temp: MarketTemperature | None) -> bool:
if not market_temp:
return False
@ -38,7 +46,9 @@ async def refresh_recommendations(trade_date: str = None, scan_session: str = "m
async with _scan_lock:
_scan_running = True
try:
result = await run_screening(trade_date)
# run_screening 内部混合了大量同步行情请求和 pandas 计算,
# 若直接在主事件循环执行,会导致页面读接口和 WebSocket 被拖住。
result = await _run_async_in_worker(run_screening, trade_date)
# 给每条推荐添加 scan_session
for rec in result.get("recommendations", []):
@ -62,7 +72,7 @@ async def _update_tracking():
from sqlalchemy import text
from app.data.tushare_client import tushare_client
trade_date = tushare_client.get_latest_trade_date()
trade_date = await asyncio.to_thread(tushare_client.get_latest_trade_date)
async with get_db() as db:
# 查找所有活跃的推荐(有 entry_price 且未被标记为 closed
@ -86,7 +96,7 @@ async def _update_tracking():
# 获取这些股票的今日收盘价
codes = [r[1] for r in rows]
daily_all = tushare_client.get_daily_all(trade_date)
daily_all = await asyncio.to_thread(tushare_client.get_daily_all, trade_date)
price_map = {}
if not daily_all.empty:
for _, row in daily_all.iterrows():
@ -100,7 +110,8 @@ async def _update_tracking():
if current_price is None or entry_price is None or entry_price <= 0:
continue
track_metrics = _calculate_tracking_metrics(
track_metrics = await asyncio.to_thread(
_calculate_tracking_metrics,
ts_code=ts_code,
entry_price=float(entry_price),
current_price=float(current_price),

Binary file not shown.

View File

@ -35,6 +35,31 @@
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/(auth)/settings/page.js"
],
"/(auth)/recommendations/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/(auth)/recommendations/page.js"
],
"/(auth)/strategy/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/(auth)/strategy/page.js"
],
"/(auth)/sectors/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/(auth)/sectors/page.js"
],
"/(auth)/stock/[code]/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/(auth)/stock/[code]/page.js"
],
"/_not-found/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/_not-found/page.js"
]
}
}

View File

@ -2,7 +2,9 @@
"polyfillFiles": [
"static/chunks/polyfills.js"
],
"devFiles": [],
"devFiles": [
"static/chunks/react-refresh.js"
],
"ampDevFiles": [],
"lowPriorityFiles": [
"static/development/_buildManifest.js",
@ -13,7 +15,16 @@
"static/chunks/main-app.js"
],
"pages": {
"/_app": []
"/_app": [
"static/chunks/webpack.js",
"static/chunks/main.js",
"static/chunks/pages/_app.js"
],
"/_error": [
"static/chunks/webpack.js",
"static/chunks/main.js",
"static/chunks/pages/_error.js"
]
},
"ampFirstPages": []
}

View File

@ -1 +1,20 @@
{}
{
"app/(auth)/sectors/page.tsx -> echarts": {
"id": "app/(auth)/sectors/page.tsx -> echarts",
"files": [
"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js"
]
},
"components/capital-flow.tsx -> echarts": {
"id": "components/capital-flow.tsx -> echarts",
"files": [
"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js"
]
},
"components/kline-chart.tsx -> echarts": {
"id": "components/kline-chart.tsx -> echarts",
"files": [
"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js"
]
}
}

View File

@ -1,6 +1,9 @@
{
"/(auth)/settings/page": "app/(auth)/settings/page.js",
"/_not-found/page": "app/_not-found/page.js",
"/(auth)/dashboard/page": "app/(auth)/dashboard/page.js",
"/(public)/page": "app/(public)/page.js",
"/(public)/login/page": "app/(public)/login/page.js"
"/(auth)/recommendations/page": "app/(auth)/recommendations/page.js",
"/(auth)/stock/[code]/page": "app/(auth)/stock/[code]/page.js",
"/(auth)/strategy/page": "app/(auth)/strategy/page.js",
"/(auth)/sectors/page": "app/(auth)/sectors/page.js",
"/(public)/page": "app/(public)/page.js"
}

View File

@ -2,7 +2,9 @@ self.__BUILD_MANIFEST = {
"polyfillFiles": [
"static/chunks/polyfills.js"
],
"devFiles": [],
"devFiles": [
"static/chunks/react-refresh.js"
],
"ampDevFiles": [],
"lowPriorityFiles": [],
"rootMainFiles": [
@ -10,7 +12,16 @@ self.__BUILD_MANIFEST = {
"static/chunks/main-app.js"
],
"pages": {
"/_app": []
"/_app": [
"static/chunks/webpack.js",
"static/chunks/main.js",
"static/chunks/pages/_app.js"
],
"/_error": [
"static/chunks/webpack.js",
"static/chunks/main.js",
"static/chunks/pages/_error.js"
]
},
"ampFirstPages": []
};

View File

@ -1 +1 @@
self.__REACT_LOADABLE_MANIFEST="{}"
self.__REACT_LOADABLE_MANIFEST="{\"app/(auth)/sectors/page.tsx -> echarts\":{\"id\":\"app/(auth)/sectors/page.tsx -> echarts\",\"files\":[\"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js\"]},\"components/capital-flow.tsx -> echarts\":{\"id\":\"components/capital-flow.tsx -> echarts\",\"files\":[\"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js\"]},\"components/kline-chart.tsx -> echarts\":{\"id\":\"components/kline-chart.tsx -> echarts\",\"files\":[\"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js\"]}}"

View File

@ -1 +1,5 @@
{}
{
"/_error": "pages/_error.js",
"/_app": "pages/_app.js",
"/_document": "pages/_document.js"
}

File diff suppressed because one or more lines are too long

View File

@ -42,20 +42,29 @@ export default function DashboardPage() {
const loadData = useCallback(async () => {
try {
const [latest, sectorData, status, overview, board, ops] = await Promise.all([
const [latestResult, sectorResult, statusResult, overviewResult, boardResult, opsResult] = await Promise.allSettled([
fetchAPI<LatestResult>("/api/recommendations/latest"),
fetchAPI<SectorData[]>("/api/sectors/hot?limit=8"),
fetchAPI<ScanStatus>("/api/recommendations/status"),
fetchAPI<IndexOverview[]>("/api/market/overview").catch(() => []),
fetchAPI<StrategyBoard>("/api/market/strategy-board").catch(() => null),
fetchAPI<OpsStatusResponse>("/api/market/ops-status").catch(() => null),
fetchAPI<IndexOverview[]>("/api/market/overview"),
fetchAPI<StrategyBoard>("/api/market/strategy-board"),
fetchAPI<OpsStatusResponse>("/api/market/ops-status"),
]);
const realtimeTemp = await fetchAPI<MarketTemperatureData>("/api/market/temperature").catch(() => latest.market_temperature);
setData(latest);
setMarketTemperature(realtimeTemp ?? latest.market_temperature ?? null);
const latest = latestResult.status === "fulfilled" ? latestResult.value : null;
const sectorData = sectorResult.status === "fulfilled" ? sectorResult.value : [];
const status = statusResult.status === "fulfilled" ? statusResult.value : null;
const overview = overviewResult.status === "fulfilled" ? overviewResult.value : [];
const board = boardResult.status === "fulfilled" ? boardResult.value : null;
const ops = opsResult.status === "fulfilled" ? opsResult.value : null;
if (latest) {
setData(latest);
const realtimeTemp = await fetchAPI<MarketTemperatureData>("/api/market/temperature").catch(() => latest.market_temperature);
setMarketTemperature(realtimeTemp ?? latest.market_temperature ?? null);
}
setSectors(sectorData);
setScanStatus(status);
if (status) setScanStatus(status);
setIndices(overview);
setStrategyBoard(board);
setOpsStatus(ops);