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

Binary file not shown.

View File

@ -35,6 +35,31 @@
"static/chunks/webpack.js", "static/chunks/webpack.js",
"static/chunks/main-app.js", "static/chunks/main-app.js",
"static/chunks/app/(auth)/settings/page.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": [ "polyfillFiles": [
"static/chunks/polyfills.js" "static/chunks/polyfills.js"
], ],
"devFiles": [], "devFiles": [
"static/chunks/react-refresh.js"
],
"ampDevFiles": [], "ampDevFiles": [],
"lowPriorityFiles": [ "lowPriorityFiles": [
"static/development/_buildManifest.js", "static/development/_buildManifest.js",
@ -13,7 +15,16 @@
"static/chunks/main-app.js" "static/chunks/main-app.js"
], ],
"pages": { "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": [] "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", "/(auth)/dashboard/page": "app/(auth)/dashboard/page.js",
"/(public)/page": "app/(public)/page.js", "/(auth)/recommendations/page": "app/(auth)/recommendations/page.js",
"/(public)/login/page": "app/(public)/login/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": [ "polyfillFiles": [
"static/chunks/polyfills.js" "static/chunks/polyfills.js"
], ],
"devFiles": [], "devFiles": [
"static/chunks/react-refresh.js"
],
"ampDevFiles": [], "ampDevFiles": [],
"lowPriorityFiles": [], "lowPriorityFiles": [],
"rootMainFiles": [ "rootMainFiles": [
@ -10,7 +12,16 @@ self.__BUILD_MANIFEST = {
"static/chunks/main-app.js" "static/chunks/main-app.js"
], ],
"pages": { "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": [] "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 () => { const loadData = useCallback(async () => {
try { 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<LatestResult>("/api/recommendations/latest"),
fetchAPI<SectorData[]>("/api/sectors/hot?limit=8"), fetchAPI<SectorData[]>("/api/sectors/hot?limit=8"),
fetchAPI<ScanStatus>("/api/recommendations/status"), fetchAPI<ScanStatus>("/api/recommendations/status"),
fetchAPI<IndexOverview[]>("/api/market/overview").catch(() => []), fetchAPI<IndexOverview[]>("/api/market/overview"),
fetchAPI<StrategyBoard>("/api/market/strategy-board").catch(() => null), fetchAPI<StrategyBoard>("/api/market/strategy-board"),
fetchAPI<OpsStatusResponse>("/api/market/ops-status").catch(() => null), fetchAPI<OpsStatusResponse>("/api/market/ops-status"),
]); ]);
const realtimeTemp = await fetchAPI<MarketTemperatureData>("/api/market/temperature").catch(() => latest.market_temperature);
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); setData(latest);
const realtimeTemp = await fetchAPI<MarketTemperatureData>("/api/market/temperature").catch(() => latest.market_temperature);
setMarketTemperature(realtimeTemp ?? latest.market_temperature ?? null); setMarketTemperature(realtimeTemp ?? latest.market_temperature ?? null);
}
setSectors(sectorData); setSectors(sectorData);
setScanStatus(status); if (status) setScanStatus(status);
setIndices(overview); setIndices(overview);
setStrategyBoard(board); setStrategyBoard(board);
setOpsStatus(ops); setOpsStatus(ops);