1
This commit is contained in:
parent
a05ccfd1b4
commit
4dcacc5650
Binary file not shown.
@ -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.
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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": []
|
||||||
}
|
}
|
||||||
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"
|
||||||
}
|
}
|
||||||
@ -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": []
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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\"]}}"
|
||||||
@ -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
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user