1
This commit is contained in:
parent
e1d4916615
commit
76bad64163
Binary file not shown.
@ -13,11 +13,28 @@ from app.db.database import init_db
|
||||
from app.engine.scheduler import start_scheduler, stop_scheduler
|
||||
from app.api import market, sectors, recommendations, stocks, watchlists, websocket, chat, auth, debug
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG if settings.debug else logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
def configure_logging() -> None:
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG if settings.debug else logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
# 保留应用日志的调试能力,但压制基础设施层的噪音。
|
||||
noisy_loggers = {
|
||||
"aiosqlite": logging.WARNING,
|
||||
"sqlalchemy": logging.WARNING,
|
||||
"sqlalchemy.engine": logging.WARNING,
|
||||
"sqlalchemy.pool": logging.WARNING,
|
||||
"httpx": logging.INFO,
|
||||
"httpcore": logging.WARNING,
|
||||
"uvicorn.access": logging.INFO,
|
||||
}
|
||||
for name, level in noisy_loggers.items():
|
||||
logging.getLogger(name).setLevel(level)
|
||||
|
||||
|
||||
configure_logging()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@ -11,8 +11,6 @@ import type {
|
||||
SectorData,
|
||||
StrategyBoard,
|
||||
} from "@/lib/api";
|
||||
import MarketTemp from "@/components/market-temp";
|
||||
import SectorHeatmap from "@/components/sector-heatmap";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useWebSocket } from "@/hooks/use-websocket";
|
||||
@ -209,9 +207,7 @@ export default function DashboardPage() {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
先看今天能不能做,再看该做什么,最后进入执行。
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-text-muted">先看结论,再看动作,再看焦点标的。</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
@ -242,17 +238,27 @@ export default function DashboardPage() {
|
||||
observeCount={observe.length}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-[minmax(0,1.3fr)_minmax(280px,0.7fr)] gap-4 animate-fade-in-up">
|
||||
<div className="grid grid-cols-1 xl:grid-cols-[minmax(0,1.2fr)_minmax(300px,0.8fr)] gap-4 animate-fade-in-up">
|
||||
<ActionPanel
|
||||
actions={todayActions}
|
||||
focusQueue={focusQueue.slice(0, 4)}
|
||||
fallbackTitle={actionable.length ? "优先执行" : watch.length ? "重点观察" : "仅保留观察"}
|
||||
summary={marketSummary}
|
||||
/>
|
||||
<ExecutionPanel
|
||||
recommendations={recommendations}
|
||||
<FocusPanel
|
||||
focusQueue={focusQueue.slice(0, 3)}
|
||||
actionableCount={actionable.length}
|
||||
watchCount={watch.length}
|
||||
observeCount={observe.length}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-[minmax(0,1fr)_320px] gap-4 animate-fade-in-up">
|
||||
<MarketSnapshot
|
||||
marketTemperature={marketTemperature ?? data?.market_temperature ?? null}
|
||||
indices={indices}
|
||||
sectors={sectors}
|
||||
summary={marketSummary}
|
||||
/>
|
||||
<AdminPanel
|
||||
isAdmin={user?.role === "admin"}
|
||||
opsStatus={opsStatus}
|
||||
refreshing={refreshing}
|
||||
@ -261,11 +267,6 @@ export default function DashboardPage() {
|
||||
onAction={handleAdminAction}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-4 animate-fade-in-up">
|
||||
<MarketTemp data={marketTemperature ?? data?.market_temperature ?? null} indices={indices} />
|
||||
<SectorHeatmap sectors={sectors} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -321,63 +322,147 @@ function DecisionHero({
|
||||
|
||||
function ActionPanel({
|
||||
actions,
|
||||
focusQueue,
|
||||
fallbackTitle,
|
||||
summary,
|
||||
}: {
|
||||
actions: ReturnType<typeof buildActionGuides>;
|
||||
focusQueue: RecommendationData[];
|
||||
fallbackTitle: string;
|
||||
summary: ReturnType<typeof buildMarketSummary>;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="glass-card-static p-4 md:p-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-text-primary">今天该做什么</h3>
|
||||
<p className="mt-1 text-xs text-text-muted">把市场结论拆成可执行动作,而不是只给情绪描述。</p>
|
||||
</div>
|
||||
<a href="/strategy" className="text-xs text-text-muted transition-colors hover:text-amber-400">
|
||||
系统校准
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<ActionBucket title="优先动作" items={actions.priority} tone="priority" />
|
||||
<ActionBucket title="观察队列" items={actions.watch} tone="watch" />
|
||||
<ActionBucket title="回避事项" items={actions.avoid} tone="avoid" />
|
||||
</div>
|
||||
<div className="glass-card-static p-4 md:p-5">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<CompactBadge label="策略" value={summary.modeLabel} />
|
||||
<CompactBadge label="仓位" value={summary.positionLabel} />
|
||||
<CompactBadge label="风险" value={summary.riskLabel} />
|
||||
</div>
|
||||
|
||||
<div className="glass-card-static p-4 md:p-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-text-primary">{fallbackTitle}</h3>
|
||||
<p className="mt-1 text-xs text-text-muted">只展示最该盯的少量标的,避免首页无限拉长。</p>
|
||||
</div>
|
||||
<a href="/recommendations" className="text-xs text-text-muted transition-colors hover:text-amber-400">
|
||||
全部推荐
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{focusQueue.length ? (
|
||||
focusQueue.map((rec) => <FocusStockCard key={rec.ts_code} rec={rec} />)
|
||||
) : (
|
||||
<div className="col-span-full rounded-2xl border border-border-subtle bg-surface-1/70 p-6 text-center text-sm text-text-muted">
|
||||
暂无标的,等待新一轮扫描输出。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<ActionBucket title="现在做" items={actions.priority} tone="priority" />
|
||||
<ActionBucket title="盯住" items={actions.watch} tone="watch" />
|
||||
<ActionBucket title="不要做" items={actions.avoid} tone="avoid" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ExecutionPanel({
|
||||
recommendations,
|
||||
function FocusPanel({
|
||||
focusQueue,
|
||||
actionableCount,
|
||||
watchCount,
|
||||
observeCount,
|
||||
}: {
|
||||
focusQueue: RecommendationData[];
|
||||
actionableCount: number;
|
||||
watchCount: number;
|
||||
observeCount: number;
|
||||
}) {
|
||||
return (
|
||||
<div className="glass-card-static p-4 md:p-5">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-text-primary">焦点标的</h3>
|
||||
<p className="mt-1 text-xs text-text-muted">首页只保留最该处理的少量标的。</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 text-center">
|
||||
<MiniCount label="可操作" value={actionableCount} tone="text-red-400" />
|
||||
<MiniCount label="关注" value={watchCount} tone="text-amber-400" />
|
||||
<MiniCount label="观察" value={observeCount} tone="text-text-secondary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
{focusQueue.length ? (
|
||||
focusQueue.map((rec) => <FocusStockCard key={rec.ts_code} rec={rec} />)
|
||||
) : (
|
||||
<div className="rounded-2xl border border-border-subtle bg-surface-1/70 p-6 text-center text-sm text-text-muted">
|
||||
今天没有需要处理的焦点标的。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MarketSnapshot({
|
||||
marketTemperature,
|
||||
indices,
|
||||
sectors,
|
||||
summary,
|
||||
}: {
|
||||
marketTemperature: MarketTemperatureData | null;
|
||||
indices: IndexOverview[];
|
||||
sectors: SectorData[];
|
||||
summary: ReturnType<typeof buildMarketSummary>;
|
||||
}) {
|
||||
const leadingSectors = sectors.slice(0, 4);
|
||||
const majorIndices = indices.slice(0, 3);
|
||||
|
||||
return (
|
||||
<div className="glass-card-static p-4 md:p-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-text-primary">市场证据</h3>
|
||||
<p className="mt-1 text-xs text-text-muted">只保留对今天决策有用的信息。</p>
|
||||
</div>
|
||||
<span className="rounded-xl bg-surface-1/70 px-3 py-1.5 text-xs font-mono tabular-nums text-text-secondary">
|
||||
温度 {Math.round(marketTemperature?.temperature ?? 0)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
<EvidenceStat label="上涨" value={marketTemperature?.up_count ?? 0} tone="text-red-400" />
|
||||
<EvidenceStat label="下跌" value={marketTemperature?.down_count ?? 0} tone="text-emerald-400" />
|
||||
<EvidenceStat label="涨停" value={marketTemperature?.limit_up_count ?? 0} tone="text-amber-400" />
|
||||
<EvidenceStat label="跌停" value={marketTemperature?.limit_down_count ?? 0} tone="text-cyan-400" />
|
||||
</div>
|
||||
|
||||
{majorIndices.length ? (
|
||||
<div className="mt-4 grid grid-cols-1 md:grid-cols-3 gap-2">
|
||||
{majorIndices.map((item) => (
|
||||
<div key={item.code} className="rounded-2xl border border-border-subtle bg-surface-1/70 px-3 py-3">
|
||||
<div className="text-xs font-semibold text-text-primary">{item.name}</div>
|
||||
<div className="mt-1 flex items-center justify-between gap-3">
|
||||
<span className="text-xs font-mono tabular-nums text-text-secondary">{item.close.toFixed(2)}</span>
|
||||
<span className={`text-xs font-mono tabular-nums ${item.pct_chg >= 0 ? "text-red-400" : "text-emerald-400"}`}>
|
||||
{item.pct_chg >= 0 ? "+" : ""}{item.pct_chg.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{leadingSectors.length ? (
|
||||
<div className="mt-4 rounded-2xl border border-border-subtle bg-surface-1/70 p-3">
|
||||
<div className="text-[11px] font-semibold text-text-secondary">今日盯住的板块</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
{leadingSectors.map((sector) => {
|
||||
const pct = sector.realtime_pct_change ?? sector.pct_change;
|
||||
return (
|
||||
<div key={sector.sector_code} className="flex items-center justify-between gap-3 text-sm">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-medium text-text-primary">{sector.sector_name}</div>
|
||||
<div className="text-[11px] text-text-muted">
|
||||
{sector.limit_up_count > 0 ? `${sector.limit_up_count} 涨停` : "无涨停"} · {sector.stage || "mid"}
|
||||
</div>
|
||||
</div>
|
||||
<span className={`font-mono tabular-nums ${pct >= 0 ? "text-red-400" : "text-emerald-400"}`}>
|
||||
{pct >= 0 ? "+" : ""}{pct.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-4 rounded-2xl border border-border-subtle bg-surface-1/70 px-3 py-3 text-sm text-text-secondary">
|
||||
结论:{summary.headline}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AdminPanel({
|
||||
isAdmin,
|
||||
opsStatus,
|
||||
refreshing,
|
||||
@ -385,10 +470,6 @@ function ExecutionPanel({
|
||||
onRefresh,
|
||||
onAction,
|
||||
}: {
|
||||
recommendations: RecommendationData[];
|
||||
actionableCount: number;
|
||||
watchCount: number;
|
||||
observeCount: number;
|
||||
isAdmin?: boolean;
|
||||
opsStatus: OpsStatusResponse | null;
|
||||
refreshing: boolean;
|
||||
@ -396,92 +477,43 @@ function ExecutionPanel({
|
||||
onRefresh: () => void;
|
||||
onAction: (action: "update_tracking" | "generate_strategy_board" | "generate_strategy_iteration") => void;
|
||||
}) {
|
||||
if (!isAdmin || !opsStatus) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="glass-card-static p-4 md:p-5">
|
||||
<h3 className="text-sm font-semibold text-text-primary">执行入口</h3>
|
||||
<p className="mt-1 text-xs text-text-muted">不同任务进入不同页面,首页只保留关键入口和现状摘要。</p>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-3">
|
||||
<NavCard
|
||||
href="/recommendations"
|
||||
title="推荐池"
|
||||
description={`当前 ${recommendations.length} 只候选,${actionableCount} 只可操作,${watchCount} 只重点关注。`}
|
||||
/>
|
||||
<NavCard
|
||||
href="/watchlist"
|
||||
title="自选股"
|
||||
description="查看你的自选股跟踪、AI 诊断与定时分析结果。"
|
||||
/>
|
||||
<NavCard
|
||||
href="/strategy"
|
||||
title="系统校准"
|
||||
description="看系统近期方法是否有效、哪里失效,以及下一轮该怎么调整。"
|
||||
/>
|
||||
<NavCard
|
||||
href="/chat"
|
||||
title="AI 对话"
|
||||
description="用于追问个股、板块和策略,不承担首页主决策职责。"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 rounded-2xl border border-border-subtle bg-surface-1/70 p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-[11px] font-semibold text-text-secondary">推荐池状态</span>
|
||||
<span className="text-[11px] text-text-muted">{recommendations.length} 只</span>
|
||||
</div>
|
||||
<div className="mt-2 grid grid-cols-3 gap-2 text-center">
|
||||
<MiniCount label="可操作" value={actionableCount} tone="text-red-400" />
|
||||
<MiniCount label="重点关注" value={watchCount} tone="text-amber-400" />
|
||||
<MiniCount label="观察" value={observeCount} tone="text-text-secondary" />
|
||||
</div>
|
||||
<div className="glass-card-static p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-text-primary">管理员</h3>
|
||||
<p className="mt-1 text-xs text-text-muted">{opsStatus.data_freshness.message}</p>
|
||||
</div>
|
||||
<span className="rounded-full bg-surface-2 px-2 py-0.5 text-[10px] text-text-muted">
|
||||
{opsStatus.scan_running ? "扫描中" : "空闲"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isAdmin && opsStatus ? (
|
||||
<div className="glass-card-static p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-text-primary">管理员任务中心</h3>
|
||||
<p className="mt-1 text-xs text-text-muted">{opsStatus.data_freshness.message}</p>
|
||||
</div>
|
||||
<span className="rounded-full bg-surface-2 px-2 py-0.5 text-[10px] text-text-muted">
|
||||
{opsStatus.scan_running ? "扫描中" : "空闲"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2 text-[11px] text-text-muted">
|
||||
<FreshnessPill label="市场" value={opsStatus.data_freshness.market_trade_date || "暂无"} />
|
||||
<FreshnessPill label="板块" value={opsStatus.data_freshness.sector_trade_date || "暂无"} />
|
||||
<FreshnessPill label="跟踪" value={opsStatus.data_freshness.tracking_trade_date || "暂无"} />
|
||||
<FreshnessPill label="推荐" value={opsStatus.data_freshness.last_recommendation_created_at || "暂无"} />
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid grid-cols-2 gap-2 text-[11px] text-text-muted">
|
||||
<FreshnessPill label="市场" value={opsStatus.data_freshness.market_trade_date || "暂无"} />
|
||||
<FreshnessPill label="板块" value={opsStatus.data_freshness.sector_trade_date || "暂无"} />
|
||||
<FreshnessPill label="跟踪" value={opsStatus.data_freshness.tracking_trade_date || "暂无"} />
|
||||
<FreshnessPill label="推荐" value={opsStatus.data_freshness.last_recommendation_created_at || "暂无"} />
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={refreshing || !!opsRunning}
|
||||
className="rounded-xl border border-amber-500/15 bg-amber-500/10 px-3 py-2 text-xs text-amber-400 disabled:opacity-40"
|
||||
>
|
||||
{refreshing ? "扫描中..." : "立即扫描"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onAction("update_tracking")}
|
||||
disabled={refreshing || !!opsRunning}
|
||||
className="rounded-xl border border-border-subtle bg-surface-1 px-3 py-2 text-xs text-text-secondary disabled:opacity-40"
|
||||
>
|
||||
{opsRunning === "update_tracking" ? "更新中..." : "更新跟踪"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onAction("generate_strategy_board")}
|
||||
disabled={refreshing || !!opsRunning}
|
||||
className="rounded-xl border border-border-subtle bg-surface-1 px-3 py-2 text-xs text-text-secondary disabled:opacity-40"
|
||||
>
|
||||
{opsRunning === "generate_strategy_board" ? "生成中..." : "生成策略板"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={refreshing || !!opsRunning}
|
||||
className="rounded-xl border border-amber-500/15 bg-amber-500/10 px-3 py-2 text-xs text-amber-400 disabled:opacity-40"
|
||||
>
|
||||
{refreshing ? "扫描中..." : "立即扫描"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onAction("update_tracking")}
|
||||
disabled={refreshing || !!opsRunning}
|
||||
className="rounded-xl border border-border-subtle bg-surface-1 px-3 py-2 text-xs text-text-secondary disabled:opacity-40"
|
||||
>
|
||||
{opsRunning === "update_tracking" ? "更新中..." : "更新跟踪"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -530,6 +562,15 @@ function HeroFact({ label, value }: { label: string; value: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function CompactBadge({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-2 rounded-xl border border-border-subtle bg-surface-1/70 px-3 py-1.5 text-xs text-text-secondary">
|
||||
<span className="text-text-muted">{label}</span>
|
||||
<span className="font-medium text-text-primary">{value}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ActionBucket({
|
||||
title,
|
||||
items,
|
||||
@ -602,12 +643,12 @@ function FocusStockCard({ rec }: { rec: RecommendationData }) {
|
||||
);
|
||||
}
|
||||
|
||||
function NavCard({ href, title, description }: { href: string; title: string; description: string }) {
|
||||
function EvidenceStat({ label, value, tone }: { label: string; value: number; tone: string }) {
|
||||
return (
|
||||
<a href={href} className="rounded-2xl border border-border-subtle bg-surface-1/70 p-3 transition-colors hover:border-amber-500/20">
|
||||
<div className="text-sm font-semibold text-text-primary">{title}</div>
|
||||
<div className="mt-1 text-xs leading-5 text-text-muted">{description}</div>
|
||||
</a>
|
||||
<div className="rounded-2xl border border-border-subtle bg-surface-1/70 px-3 py-3">
|
||||
<div className="text-[10px] uppercase tracking-wider text-text-muted">{label}</div>
|
||||
<div className={`mt-1 text-lg font-bold font-mono tabular-nums ${tone}`}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -677,6 +718,7 @@ function buildMarketSummary(
|
||||
cannotDo,
|
||||
modeLabel: strategyProfile?.market_stance || board?.recommended_mode || "等待更新",
|
||||
positionLabel: board?.position_suggestion || (strategyProfile?.max_position_pct ? `${strategyProfile.max_position_pct}% 以内` : "等待更新"),
|
||||
riskLabel: board?.risk_level || "等待更新",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user