1
This commit is contained in:
parent
76bad64163
commit
a0407f69a2
Binary file not shown.
@ -6,6 +6,7 @@ from fastapi import APIRouter, Depends
|
||||
|
||||
from app.data.tushare_client import tushare_client
|
||||
from app.data import tencent_client
|
||||
from app.data.cache import cache
|
||||
from app.data.market_breadth_client import get_market_breadth
|
||||
from app.analysis.market_temp import build_realtime_market_temperature, calculate_market_temperature
|
||||
from app.engine.recommender import get_latest_recommendations
|
||||
@ -72,15 +73,27 @@ async def get_overview():
|
||||
@router.get("/strategy-board")
|
||||
async def get_strategy_board():
|
||||
"""获取今日市场作战面板(只读,不触发 LLM)"""
|
||||
cache_key = "market:strategy_board:rules"
|
||||
cached = cache.get(cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
from app.llm.strategy_board import build_strategy_board
|
||||
return await build_strategy_board(include_llm=False)
|
||||
result = await build_strategy_board(include_llm=False)
|
||||
cache.set(cache_key, result, settings.cache_ttl_realtime)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/strategy-iteration")
|
||||
async def get_strategy_iteration(limit: int = 50):
|
||||
"""获取策略复盘迭代建议(只读,不触发 LLM)"""
|
||||
cache_key = f"market:strategy_iteration:{limit}:rules"
|
||||
cached = cache.get(cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
from app.llm.strategy_iteration import build_strategy_iteration_report
|
||||
return await build_strategy_iteration_report(limit=limit, include_llm=False)
|
||||
result = await build_strategy_iteration_report(limit=limit, include_llm=False)
|
||||
cache.set(cache_key, result, settings.cache_ttl_realtime)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/ops-status")
|
||||
@ -160,14 +173,18 @@ async def get_ops_status():
|
||||
async def generate_strategy_board(_admin: dict = Depends(get_current_admin)):
|
||||
"""管理员手动生成带 LLM 说明的策略看板"""
|
||||
from app.llm.strategy_board import build_strategy_board
|
||||
return await build_strategy_board(include_llm=True)
|
||||
result = await build_strategy_board(include_llm=True)
|
||||
cache.delete("market:strategy_board:rules")
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/generate-strategy-iteration")
|
||||
async def generate_strategy_iteration(limit: int = 50, _admin: dict = Depends(get_current_admin)):
|
||||
"""管理员手动生成带 LLM 分析的策略复盘"""
|
||||
from app.llm.strategy_iteration import build_strategy_iteration_report
|
||||
return await build_strategy_iteration_report(limit=limit, include_llm=True)
|
||||
result = await build_strategy_iteration_report(limit=limit, include_llm=True)
|
||||
cache.delete(f"market:strategy_iteration:{limit}:rules")
|
||||
return result
|
||||
|
||||
async def _overview_realtime():
|
||||
"""盘中:腾讯实时指数行情"""
|
||||
|
||||
@ -27,6 +27,12 @@ async def build_strategy_iteration_report(limit: int = 50, include_llm: bool = F
|
||||
return rule_report
|
||||
|
||||
|
||||
async def build_strategy_feedback_controls(limit: int = 50) -> dict:
|
||||
rows = await _load_recent_tracking(limit)
|
||||
report = _build_rule_report(rows)
|
||||
return _derive_feedback_controls(report)
|
||||
|
||||
|
||||
async def _load_recent_tracking(limit: int) -> list[dict]:
|
||||
from sqlalchemy import text
|
||||
from app.db.database import get_db
|
||||
@ -240,6 +246,67 @@ def _build_adjustment_suggestions(
|
||||
return suggestions[:6]
|
||||
|
||||
|
||||
def _derive_feedback_controls(report: dict) -> dict:
|
||||
suggestions = report.get("adjustment_suggestions", []) or []
|
||||
sample_size = int(report.get("sample_size") or 0)
|
||||
|
||||
controls = {
|
||||
"sample_size": sample_size,
|
||||
"enabled": sample_size >= 10,
|
||||
"buy_threshold_delta": 0,
|
||||
"max_position_pct_delta": 0,
|
||||
"actionable_limit_delta": 0,
|
||||
"watch_limit_delta": 0,
|
||||
"force_defensive": False,
|
||||
"notes": [],
|
||||
}
|
||||
|
||||
if sample_size < 10:
|
||||
controls["notes"].append("样本不足,暂不启用自动回写。")
|
||||
return controls
|
||||
|
||||
promote_count = 0
|
||||
tighten_count = 0
|
||||
reduce_count = 0
|
||||
|
||||
for item in suggestions[:6]:
|
||||
action = item.get("action")
|
||||
reason = item.get("reason", "")
|
||||
|
||||
if action == "promote":
|
||||
promote_count += 1
|
||||
controls["buy_threshold_delta"] -= 1
|
||||
controls["watch_limit_delta"] += 1
|
||||
elif action == "tighten":
|
||||
tighten_count += 1
|
||||
controls["buy_threshold_delta"] += 1
|
||||
controls["actionable_limit_delta"] -= 1
|
||||
controls["max_position_pct_delta"] -= 5
|
||||
elif action == "reduce":
|
||||
reduce_count += 1
|
||||
controls["buy_threshold_delta"] += 1
|
||||
controls["watch_limit_delta"] -= 1
|
||||
|
||||
if "弱势市场" in reason or item.get("target") == "defensive_watch":
|
||||
controls["force_defensive"] = True
|
||||
|
||||
controls["buy_threshold_delta"] = max(-2, min(3, controls["buy_threshold_delta"]))
|
||||
controls["max_position_pct_delta"] = max(-10, min(5, controls["max_position_pct_delta"]))
|
||||
controls["actionable_limit_delta"] = max(-2, min(1, controls["actionable_limit_delta"]))
|
||||
controls["watch_limit_delta"] = max(-2, min(2, controls["watch_limit_delta"]))
|
||||
|
||||
if controls["force_defensive"]:
|
||||
controls["notes"].append("最近弱市亏损样本偏多,优先启用防守约束。")
|
||||
elif tighten_count > promote_count:
|
||||
controls["notes"].append("最近失效样本偏多,整体建议略收紧。")
|
||||
elif promote_count > 0 and reduce_count == 0:
|
||||
controls["notes"].append("最近有效样本改善,可适度放宽观察与出手空间。")
|
||||
else:
|
||||
controls["notes"].append("最近样本无明显单边倾向,仅做轻微校正。")
|
||||
|
||||
return controls
|
||||
|
||||
|
||||
async def _generate_ai_iteration(rule_report: dict, rows: list[dict]) -> str:
|
||||
from app.llm.client import chat_completion
|
||||
|
||||
|
||||
@ -121,6 +121,7 @@ async def select_strategy_profile(
|
||||
intraday: bool,
|
||||
) -> StrategyProfile:
|
||||
profile = _select_rule_profile(market_temp, hot_sectors, intraday)
|
||||
profile = await _apply_strategy_feedback(profile)
|
||||
|
||||
if settings.deepseek_api_key:
|
||||
llm_profile = await _select_llm_profile(market_temp, hot_sectors, intraday, profile)
|
||||
@ -151,6 +152,41 @@ def _select_rule_profile(
|
||||
return get_strategy_profile_by_id("defensive_watch")
|
||||
|
||||
|
||||
async def _apply_strategy_feedback(profile: StrategyProfile) -> StrategyProfile:
|
||||
from app.llm.strategy_iteration import build_strategy_feedback_controls
|
||||
|
||||
try:
|
||||
controls = await build_strategy_feedback_controls(limit=50)
|
||||
except Exception as e:
|
||||
logger.debug(f"策略反馈控制生成失败: {e}")
|
||||
return profile
|
||||
|
||||
if not controls.get("enabled"):
|
||||
return profile
|
||||
|
||||
updated = profile.model_copy(deep=True)
|
||||
|
||||
if controls.get("force_defensive"):
|
||||
updated.allow_trading = False
|
||||
updated.actionable_limit = 0
|
||||
updated.watch_limit = min(updated.watch_limit, 3)
|
||||
updated.max_position_pct = min(updated.max_position_pct, 10)
|
||||
updated.market_stance = "防守观察"
|
||||
|
||||
updated.buy_threshold = max(updated.min_score, min(updated.buy_threshold + int(controls.get("buy_threshold_delta") or 0), 80))
|
||||
updated.max_position_pct = max(0, min(updated.max_position_pct + int(controls.get("max_position_pct_delta") or 0), 40))
|
||||
updated.actionable_limit = max(0, min(updated.actionable_limit + int(controls.get("actionable_limit_delta") or 0), settings.actionable_limit))
|
||||
updated.watch_limit = max(1, min(updated.watch_limit + int(controls.get("watch_limit_delta") or 0), settings.watch_limit))
|
||||
|
||||
notes = controls.get("notes") or []
|
||||
if notes:
|
||||
updated.notes.extend(notes[:2])
|
||||
updated.decision_note = notes[0]
|
||||
|
||||
updated.generated_by = f"{updated.generated_by}+feedback"
|
||||
return updated
|
||||
|
||||
|
||||
async def _select_llm_profile(
|
||||
market_temp: MarketTemperature | None,
|
||||
hot_sectors: list[SectorInfo],
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"/(auth)/chat/page": "app/(auth)/chat/page.js",
|
||||
"/(auth)/stock/[code]/page": "app/(auth)/stock/[code]/page.js",
|
||||
"/(public)/page": "app/(public)/page.js",
|
||||
"/(auth)/chat/page": "app/(auth)/chat/page.js"
|
||||
"/(public)/page": "app/(public)/page.js"
|
||||
}
|
||||
@ -40,21 +40,17 @@ export default function DashboardPage() {
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
const [latestResult, sectorResult, statusResult, overviewResult, boardResult, opsResult] = await Promise.allSettled([
|
||||
const [latestResult, sectorResult, statusResult, overviewResult] = 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"),
|
||||
fetchAPI<StrategyBoard>("/api/market/strategy-board"),
|
||||
fetchAPI<OpsStatusResponse>("/api/market/ops-status"),
|
||||
]);
|
||||
|
||||
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);
|
||||
@ -64,8 +60,6 @@ export default function DashboardPage() {
|
||||
setSectors(sectorData);
|
||||
if (status) setScanStatus(status);
|
||||
setIndices(overview);
|
||||
setStrategyBoard(board);
|
||||
setOpsStatus(ops);
|
||||
} catch (error) {
|
||||
console.error("加载数据失败:", error);
|
||||
} finally {
|
||||
@ -73,6 +67,19 @@ export default function DashboardPage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadSecondaryData = useCallback(async () => {
|
||||
try {
|
||||
const [board, ops] = await Promise.all([
|
||||
fetchAPI<StrategyBoard>("/api/market/strategy-board").catch(() => null),
|
||||
fetchAPI<OpsStatusResponse>("/api/market/ops-status").catch(() => null),
|
||||
]);
|
||||
setStrategyBoard(board);
|
||||
setOpsStatus(ops);
|
||||
} catch (error) {
|
||||
console.error("加载次要数据失败:", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearScanTimeout = useCallback(() => {
|
||||
if (scanTimeoutRef.current) {
|
||||
clearTimeout(scanTimeoutRef.current);
|
||||
@ -84,6 +91,10 @@ export default function DashboardPage() {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSecondaryData();
|
||||
}, [loadSecondaryData]);
|
||||
|
||||
useWebSocket(
|
||||
useCallback((msg: { type: string; count?: number; scan_mode?: string; message?: string }) => {
|
||||
clearScanTimeout();
|
||||
@ -92,6 +103,7 @@ export default function DashboardPage() {
|
||||
setRefreshResult(`${modeLabel}扫描完成,发现 ${msg.count ?? 0} 只股票`);
|
||||
setRefreshing(false);
|
||||
loadData();
|
||||
loadSecondaryData();
|
||||
setTimeout(() => setRefreshResult(null), 5000);
|
||||
} else if (msg.type === "scan_error") {
|
||||
setRefreshResult("扫描失败,请重试");
|
||||
@ -99,8 +111,9 @@ export default function DashboardPage() {
|
||||
setTimeout(() => setRefreshResult(null), 5000);
|
||||
} else {
|
||||
loadData();
|
||||
loadSecondaryData();
|
||||
}
|
||||
}, [clearScanTimeout, loadData]),
|
||||
}, [clearScanTimeout, loadData, loadSecondaryData]),
|
||||
["scan_update", "scan_error", "llm_analysis_ready", "sector_scan_ready", "scan_complete"]
|
||||
);
|
||||
|
||||
@ -124,6 +137,7 @@ export default function DashboardPage() {
|
||||
setRefreshResult("扫描超时,已自动刷新数据");
|
||||
setRefreshing(false);
|
||||
loadData();
|
||||
loadSecondaryData();
|
||||
setTimeout(() => setRefreshResult(null), 5000);
|
||||
}, SCAN_TIMEOUT_MS);
|
||||
} catch (error) {
|
||||
@ -134,21 +148,14 @@ export default function DashboardPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdminAction = async (action: "update_tracking" | "generate_strategy_board" | "generate_strategy_iteration") => {
|
||||
const handleAdminAction = async (action: "update_tracking") => {
|
||||
setOpsRunning(action);
|
||||
setRefreshResult(null);
|
||||
try {
|
||||
if (action === "update_tracking") {
|
||||
const result = await postAPI<{ tracked?: number; win_rate?: number }>("/api/recommendations/update-tracking");
|
||||
setRefreshResult(`跟踪已更新,样本 ${result.tracked ?? 0},胜率 ${(result.win_rate ?? 0).toFixed(1)}%`);
|
||||
} else if (action === "generate_strategy_board") {
|
||||
await postAPI("/api/market/generate-strategy-board");
|
||||
setRefreshResult("策略板已生成");
|
||||
} else {
|
||||
await postAPI("/api/market/generate-strategy-iteration");
|
||||
setRefreshResult("策略复盘已生成");
|
||||
}
|
||||
const result = await postAPI<{ tracked?: number; win_rate?: number }>("/api/recommendations/update-tracking");
|
||||
setRefreshResult(`跟踪已更新,样本 ${result.tracked ?? 0},胜率 ${(result.win_rate ?? 0).toFixed(1)}%`);
|
||||
await loadData();
|
||||
await loadSecondaryData();
|
||||
setTimeout(() => setRefreshResult(null), 5000);
|
||||
} catch (error) {
|
||||
console.error("管理员操作失败:", error);
|
||||
@ -475,7 +482,7 @@ function AdminPanel({
|
||||
refreshing: boolean;
|
||||
opsRunning: string | null;
|
||||
onRefresh: () => void;
|
||||
onAction: (action: "update_tracking" | "generate_strategy_board" | "generate_strategy_iteration") => void;
|
||||
onAction: (action: "update_tracking") => void;
|
||||
}) {
|
||||
if (!isAdmin || !opsStatus) return null;
|
||||
|
||||
|
||||
@ -6,9 +6,7 @@ import type {
|
||||
DayGroup,
|
||||
LatestResult,
|
||||
OpsStatusResponse,
|
||||
PerformanceStats,
|
||||
RecommendationData,
|
||||
StrategyIterationReport,
|
||||
} from "@/lib/api";
|
||||
import StockCard from "@/components/stock-card";
|
||||
|
||||
@ -43,24 +41,18 @@ export default function RecommendationsPage() {
|
||||
const [expandedDays, setExpandedDays] = useState<Set<string>>(new Set());
|
||||
const [historyFilter, setHistoryFilter] = useState<string>("all");
|
||||
const [focusTab, setFocusTab] = useState<FocusTab>("actionable");
|
||||
const [performance, setPerformance] = useState<PerformanceStats | null>(null);
|
||||
const [iteration, setIteration] = useState<StrategyIterationReport | null>(null);
|
||||
const [opsStatus, setOpsStatus] = useState<OpsStatusResponse | null>(null);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
const [history, latestResult, perf, iterationReport, ops] = await Promise.all([
|
||||
const [history, latestResult, ops] = await Promise.all([
|
||||
fetchAPI<DayGroup[]>("/api/recommendations/history?days=14"),
|
||||
fetchAPI<LatestResult>("/api/recommendations/latest").catch(() => null),
|
||||
fetchAPI<PerformanceStats>("/api/recommendations/performance").catch(() => null),
|
||||
fetchAPI<StrategyIterationReport>("/api/market/strategy-iteration?limit=50").catch(() => null),
|
||||
fetchAPI<OpsStatusResponse>("/api/market/ops-status").catch(() => null),
|
||||
]);
|
||||
|
||||
setDayGroups(history);
|
||||
setLatest(latestResult);
|
||||
setPerformance(perf);
|
||||
setIteration(iterationReport);
|
||||
setOpsStatus(ops);
|
||||
|
||||
setExpandedDays((prev) => {
|
||||
@ -144,8 +136,6 @@ export default function RecommendationsPage() {
|
||||
observe,
|
||||
tracking,
|
||||
closed,
|
||||
iteration,
|
||||
performance,
|
||||
});
|
||||
|
||||
return (
|
||||
@ -264,9 +254,6 @@ export default function RecommendationsPage() {
|
||||
只给今天真正要处理的少量标的,剩余候选不占主视图。
|
||||
</p>
|
||||
</div>
|
||||
<a href="/strategy" className="text-xs text-text-muted transition-colors hover:text-amber-400">
|
||||
看系统校准
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{focusItems.length ? (
|
||||
@ -283,64 +270,32 @@ export default function RecommendationsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-[minmax(0,1fr)_340px] gap-4 animate-fade-in-up">
|
||||
<div className="glass-card-static p-4 md:p-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-text-primary">后台观察</h2>
|
||||
<p className="mt-1 text-xs text-text-muted">只保留少量名字方便回看,不参与今天的主决策。</p>
|
||||
</div>
|
||||
<span className="text-xs text-text-muted">{observe.length} 只</span>
|
||||
<div className="glass-card-static p-4 md:p-5 animate-fade-in-up">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-text-primary">后台观察</h2>
|
||||
<p className="mt-1 text-xs text-text-muted">只保留少量名字方便回看,不参与今天的主决策。</p>
|
||||
</div>
|
||||
|
||||
{observe.length ? (
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{observe.slice(0, 12).map((rec) => (
|
||||
<a
|
||||
key={`observe-${rec.ts_code}`}
|
||||
href={`/stock/${rec.ts_code}`}
|
||||
className="rounded-xl border border-border-subtle bg-surface-1/70 px-3 py-2 text-xs text-text-secondary transition-colors hover:border-amber-500/20"
|
||||
>
|
||||
<span className="font-semibold text-text-primary">{rec.name}</span>
|
||||
<span className="mx-1 text-text-muted">·</span>
|
||||
<span>{rec.sector}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 text-sm text-text-muted">暂无观察池标的。</div>
|
||||
)}
|
||||
<span className="text-xs text-text-muted">{observe.length} 只</span>
|
||||
</div>
|
||||
|
||||
<div className="glass-card-static p-4 md:p-5">
|
||||
<div className="text-[10px] uppercase tracking-[0.22em] text-cyan-400 font-semibold">闭环概览</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
<SummaryMetric
|
||||
label="胜率"
|
||||
value={Number((performance?.win_rate ?? 0).toFixed(1))}
|
||||
suffix="%"
|
||||
tone={(performance?.win_rate ?? 0) >= 50 ? "text-red-400" : "text-emerald-400"}
|
||||
/>
|
||||
<SummaryMetric
|
||||
label="平均收益"
|
||||
value={Number((performance?.avg_return ?? 0).toFixed(2))}
|
||||
suffix="%"
|
||||
tone={(performance?.avg_return ?? 0) >= 0 ? "text-red-400" : "text-emerald-400"}
|
||||
signed
|
||||
/>
|
||||
<SummaryMetric label="已跟踪" value={performance?.tracked ?? 0} tone="text-text-primary" />
|
||||
<SummaryMetric label="今日筛选" value={todayCount} tone="text-text-primary" />
|
||||
{observe.length ? (
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{observe.slice(0, 12).map((rec) => (
|
||||
<a
|
||||
key={`observe-${rec.ts_code}`}
|
||||
href={`/stock/${rec.ts_code}`}
|
||||
className="rounded-xl border border-border-subtle bg-surface-1/70 px-3 py-2 text-xs text-text-secondary transition-colors hover:border-amber-500/20"
|
||||
>
|
||||
<span className="font-semibold text-text-primary">{rec.name}</span>
|
||||
<span className="mx-1 text-text-muted">·</span>
|
||||
<span>{rec.sector}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{(iteration?.summary || iteration?.ai_analysis) ? (
|
||||
<div className="mt-4 rounded-2xl border border-cyan-500/10 bg-cyan-500/[0.04] p-3">
|
||||
<div className="text-[10px] uppercase tracking-wider text-cyan-400 font-semibold">AI 迭代提示</div>
|
||||
<div className="mt-2 text-xs leading-6 text-cyan-400/85">
|
||||
{iteration?.ai_analysis || iteration?.summary}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 text-sm text-text-muted">暂无观察池标的。</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="glass-card-static p-4 md:p-5 animate-fade-in-up">
|
||||
@ -423,8 +378,6 @@ function buildFocusSummary({
|
||||
observe,
|
||||
tracking,
|
||||
closed,
|
||||
iteration,
|
||||
performance,
|
||||
}: {
|
||||
strategyProfile: LatestResult["strategy_profile"];
|
||||
actionable: RecommendationData[];
|
||||
@ -432,8 +385,6 @@ function buildFocusSummary({
|
||||
observe: RecommendationData[];
|
||||
tracking: RecommendationData[];
|
||||
closed: RecommendationData[];
|
||||
iteration: StrategyIterationReport | null;
|
||||
performance: PerformanceStats | null;
|
||||
}) {
|
||||
const allowTrading = strategyProfile?.allow_trading ?? actionable.length > 0;
|
||||
const headline =
|
||||
@ -453,7 +404,7 @@ function buildFocusSummary({
|
||||
? "首页只保留最接近执行的标的,真正长分析进入个股详情。"
|
||||
: watch.length > 0
|
||||
? "今天偏等待确认,不适合在大量候选里反复横跳。"
|
||||
: "当前更多是维护候选池和复盘闭环,不是积极出手阶段。");
|
||||
: "当前没有明确优势机会,先观察,不主动扩池。");
|
||||
|
||||
const now = [
|
||||
!allowTrading
|
||||
@ -470,15 +421,13 @@ function buildFocusSummary({
|
||||
: "没有确认信号前,不把观察股抬升为执行名单。",
|
||||
tracking.length > 0
|
||||
? `${tracking.length} 只跟踪中标的继续看兑现情况,避免只看新增不看结果。`
|
||||
: "如果没有跟踪样本,说明闭环还不够,要继续积累和复盘。",
|
||||
: "没有跟踪中的标的时,就把注意力集中在今天的新结论上。",
|
||||
];
|
||||
|
||||
const later = [
|
||||
observe.length > 0 ? `${observe.length} 只后台观察标的不应占据首页主注意力。` : "没有必要把弱标的堆在默认视图。",
|
||||
closed.length > 0 ? `${closed.length} 只已结束样本主要用于复盘,不参与今日执行决策。` : "没有结束样本时,先积累更多闭环数据。",
|
||||
iteration?.summary || performance
|
||||
? "策略迭代和胜率统计放在辅助区,不应该压过今天的执行结论。"
|
||||
: "方法说明和统计信息只作为辅助,不抢首页决策位置。",
|
||||
closed.length > 0 ? `${closed.length} 只已结束样本只在需要回看时再展开,不参与今日执行决策。` : "没有结束样本时,也不需要额外制造解释性信息。",
|
||||
"方法说明只作为辅助,不应该压过今天的执行结论。",
|
||||
];
|
||||
|
||||
return { headline, detail, now, later };
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { fetchAPI } from "@/lib/api";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import type {
|
||||
PerformanceStats,
|
||||
StrategyAdjustment,
|
||||
@ -18,6 +20,8 @@ const ACTION_LABELS: Record<string, string> = {
|
||||
};
|
||||
|
||||
export default function StrategyPage() {
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [iteration, setIteration] = useState<StrategyIterationReport | null>(null);
|
||||
const [performance, setPerformance] = useState<PerformanceStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@ -36,12 +40,20 @@ export default function StrategyPage() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && user?.role !== "admin") {
|
||||
router.replace("/dashboard");
|
||||
return;
|
||||
}
|
||||
}, [authLoading, router, user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (authLoading || user?.role !== "admin") return;
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
}, [authLoading, loadData, user]);
|
||||
|
||||
const diagnosis = useMemo(() => buildCalibrationDiagnosis(iteration, performance), [iteration, performance]);
|
||||
|
||||
if (loading) {
|
||||
if (authLoading || user?.role !== "admin" || loading) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 md:px-8 pt-6 space-y-4">
|
||||
<div className="h-32 glass-card-static animate-shimmer" />
|
||||
|
||||
@ -116,13 +116,15 @@ export function SidebarNav() {
|
||||
<nav className="flex-1 py-5 px-3 space-y-1">
|
||||
<SideNavItem href="/dashboard" icon={<DashboardIcon />} label="今日作战" />
|
||||
<SideNavItem href="/recommendations" icon={<TargetIcon />} label="推荐池" />
|
||||
<SideNavItem href="/strategy" icon={<StrategyIcon />} label="系统校准" />
|
||||
<SideNavItem href="/sectors" icon={<FireIcon />} label="板块主线" />
|
||||
<SideNavItem href="/watchlists" icon={<WatchlistIcon />} label="自选股" />
|
||||
<SideNavItem href="/diagnose" icon={<DiagnoseIcon />} label="个股诊断" />
|
||||
<SideNavItem href="/chat" icon={<ChatIcon />} label="作战问答" />
|
||||
{user?.role === "admin" && (
|
||||
<SideNavItem href="/settings" icon={<SettingsIcon />} label="系统设置" />
|
||||
<>
|
||||
<SideNavItem href="/strategy" icon={<StrategyIcon />} label="系统校准" />
|
||||
<SideNavItem href="/settings" icon={<SettingsIcon />} label="系统设置" />
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
@ -146,6 +148,8 @@ function MobileNavItem({ href, label, children }: { href: string; label: string;
|
||||
}
|
||||
|
||||
export function MobileBottomNav() {
|
||||
const { user } = useAuth();
|
||||
|
||||
return (
|
||||
<nav className="fixed bottom-0 left-0 right-0 md:hidden z-50 bg-bg-secondary/95 backdrop-blur-xl border-t border-border-subtle">
|
||||
<div className="flex justify-around py-2 pb-[max(0.5rem,env(safe-area-inset-bottom))]">
|
||||
@ -155,12 +159,14 @@ export function MobileBottomNav() {
|
||||
<MobileNavItem href="/recommendations" label="推荐池">
|
||||
<TargetIcon />
|
||||
</MobileNavItem>
|
||||
<MobileNavItem href="/strategy" label="校准">
|
||||
<StrategyIcon />
|
||||
</MobileNavItem>
|
||||
<MobileNavItem href="/watchlists" label="自选">
|
||||
<WatchlistIcon />
|
||||
</MobileNavItem>
|
||||
{user?.role === "admin" ? (
|
||||
<MobileNavItem href="/strategy" label="校准">
|
||||
<StrategyIcon />
|
||||
</MobileNavItem>
|
||||
) : null}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user