mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 15:34:16 +08:00
feat: 完善历史换手率重算入口
- 财务数据统计纳入历史股本,并在数据页提供分批重算入口 - 缺少股本历史时阻止重算,并接入现有任务进度 - 财务分析卡片使用更新图标,明确数据拉取语义
This commit is contained in:
@@ -411,7 +411,7 @@ def _safe_aggregate_financials(repo) -> dict | None:
|
||||
tables_info: dict[str, dict] = {}
|
||||
total_rows = 0
|
||||
|
||||
for table in ("metrics", "income", "balance_sheet", "cash_flow"):
|
||||
for table in ("metrics", "income", "balance_sheet", "cash_flow", "shares"):
|
||||
path = data_dir / "financials" / table / "part.parquet"
|
||||
if path.exists():
|
||||
try:
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
from types import SimpleNamespace
|
||||
|
||||
import polars as pl
|
||||
import pytest
|
||||
|
||||
from app.api import data as data_api
|
||||
from app.indicators import pipeline
|
||||
from app.services import financial_sync
|
||||
from app.tickflow.capabilities import CapabilitySet
|
||||
@@ -159,3 +161,19 @@ def test_turnover_without_share_history_keeps_existing_behavior(monkeypatch):
|
||||
)
|
||||
|
||||
assert result["turnover_rate"][0] == pytest.approx(0.5)
|
||||
|
||||
|
||||
def test_data_status_includes_share_history(tmp_path):
|
||||
path = tmp_path / "financials" / "shares" / "part.parquet"
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
pl.DataFrame({
|
||||
"symbol": ["600000.SH", "600000.SH", "000001.SZ"],
|
||||
"period_end": ["2023-12-31", "2024-06-30", "2024-06-30"],
|
||||
}).write_parquet(path)
|
||||
|
||||
repo = SimpleNamespace(store=SimpleNamespace(data_dir=tmp_path))
|
||||
result = data_api._safe_aggregate_financials(repo)
|
||||
|
||||
assert result is not None
|
||||
assert result["rows"] == 3
|
||||
assert result["tables"]["shares"] == {"rows": 3, "symbols": 2}
|
||||
|
||||
@@ -5,10 +5,24 @@ import { api } from '@/lib/api'
|
||||
import { QK } from '@/lib/queryKeys'
|
||||
import { usePreferences } from '@/lib/useSharedQueries'
|
||||
|
||||
export function EnrichedRebuildPanel({ isRunning, onStart }: { isRunning: boolean; onStart: () => void }) {
|
||||
type Props = {
|
||||
isRunning: boolean
|
||||
onStart: (jobId: string) => void
|
||||
purpose?: 'enriched' | 'turnover'
|
||||
historicalShareRows?: number
|
||||
}
|
||||
|
||||
export function EnrichedRebuildPanel({
|
||||
isRunning,
|
||||
onStart,
|
||||
purpose = 'enriched',
|
||||
historicalShareRows = 0,
|
||||
}: Props) {
|
||||
const qc = useQueryClient()
|
||||
const prefs = usePreferences()
|
||||
const batchSize = prefs.data?.enriched_batch_size ?? 1000
|
||||
const isTurnoverRebuild = purpose === 'turnover'
|
||||
const canRebuild = !isTurnoverRebuild || historicalShareRows > 0
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [draftSize, setDraftSize] = useState(String(batchSize))
|
||||
const [hint, setHint] = useState<string | null>(null)
|
||||
@@ -28,8 +42,8 @@ export function EnrichedRebuildPanel({ isRunning, onStart }: { isRunning: boolea
|
||||
})
|
||||
const rebuild = useMutation({
|
||||
mutationFn: api.rebuildEnriched,
|
||||
onSuccess: () => {
|
||||
onStart()
|
||||
onSuccess: ({ job_id }) => {
|
||||
onStart(job_id)
|
||||
qc.invalidateQueries({ queryKey: QK.pipelineJobs })
|
||||
},
|
||||
})
|
||||
@@ -93,18 +107,38 @@ export function EnrichedRebuildPanel({ isRunning, onStart }: { isRunning: boolea
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-[10px] text-muted mb-2">基于已有 kline_daily + adj_factor 全量计算前复权 + 技术指标 + 信号</div>
|
||||
{isTurnoverRebuild && (
|
||||
<div className={`mb-3 rounded-btn border px-3 py-2 text-[10px] leading-relaxed ${
|
||||
canRebuild
|
||||
? 'border-accent/20 bg-accent/5 text-secondary'
|
||||
: 'border-warning/20 bg-warning/10 text-warning'
|
||||
}`}>
|
||||
{canRebuild
|
||||
? `已检测到 ${historicalShareRows.toLocaleString()} 条历史股本记录。重算会按公告可用日匹配历史流通股本,并覆盖全部 Enriched 分区。`
|
||||
: '未检测到历史股本数据,请先在财务分析页面同步“股本表”,再执行重算。'}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-[10px] text-muted mb-2">
|
||||
{isTurnoverRebuild
|
||||
? '为保证数据一致性,将基于现有日 K、除权因子和历史股本重新生成 Enriched;其他指标也会按当前逻辑同步更新。'
|
||||
: '基于已有 kline_daily + adj_factor 全量计算前复权 + 技术指标 + 信号'}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => rebuild.mutate()}
|
||||
disabled={isRunning || rebuild.isPending}
|
||||
disabled={!canRebuild || isRunning || rebuild.isPending}
|
||||
className="w-full inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-btn bg-accent/90 text-base text-xs font-medium hover:bg-accent disabled:opacity-40 disabled:pointer-events-none transition-colors duration-150"
|
||||
>
|
||||
{rebuild.isPending ? (
|
||||
<><Loader2 className="h-3 w-3 animate-spin" />计算中…</>
|
||||
) : (
|
||||
<>全量计算</>
|
||||
<>{isTurnoverRebuild ? '重新计算并覆盖' : '全量计算'}</>
|
||||
)}
|
||||
</button>
|
||||
{rebuild.isError && (
|
||||
<div className="mt-2 text-[10px] text-danger">
|
||||
启动失败:{String((rebuild.error as Error)?.message ?? rebuild.error)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -230,6 +230,7 @@ export function StatCard({
|
||||
className={`p-0.5 rounded hover:bg-elevated transition-colors ${
|
||||
settingsOpen ? 'text-accent' : 'text-secondary'
|
||||
}`}
|
||||
title="设置"
|
||||
>
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
||||
@@ -512,19 +512,24 @@ export function Data() {
|
||||
settingsOpen={openSettings === 'minute'}
|
||||
/>
|
||||
)
|
||||
case 'financials':
|
||||
case 'financials': {
|
||||
const historicalShareRows = s?.financials?.tables?.shares?.rows ?? 0
|
||||
return (
|
||||
<StatCard
|
||||
title="财务数据"
|
||||
hint="利润表 / 资负表 / 现金流 / 指标"
|
||||
hint="财报 / 指标 / 历史股本"
|
||||
stats={s?.financials ? { rows: s.financials.rows } : null}
|
||||
loading={isLoading}
|
||||
tierKey="financials"
|
||||
capLimits={caps.data?.capabilities}
|
||||
tierLabel={caps.data?.label}
|
||||
customProvider={getCustomProviderName('financials')}
|
||||
subLabel={`历史股本 · ${historicalShareRows.toLocaleString()} 条`}
|
||||
onSettings={hasData ? () => setOpenSettings(v => v === 'financials' ? null : 'financials') : undefined}
|
||||
settingsOpen={openSettings === 'financials'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@@ -958,7 +963,23 @@ export function Data() {
|
||||
<AnimatePresence>
|
||||
{openSettings === 'enriched' && (
|
||||
<SettingsModal title="Enriched · 计算设置" onClose={() => setOpenSettings(null)}>
|
||||
<EnrichedRebuildPanel isRunning={!!activeJobId} onStart={() => setOpenSettings(null)} />
|
||||
<EnrichedRebuildPanel
|
||||
isRunning={!!activeJobId}
|
||||
onStart={(jobId) => { setActiveJobId(jobId); setOpenSettings(null) }}
|
||||
/>
|
||||
</SettingsModal>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{openSettings === 'financials' && (
|
||||
<SettingsModal title="财务数据 · 换手率重算" onClose={() => setOpenSettings(null)}>
|
||||
<EnrichedRebuildPanel
|
||||
isRunning={!!activeJobId}
|
||||
purpose="turnover"
|
||||
historicalShareRows={s?.financials?.tables?.shares?.rows ?? 0}
|
||||
onStart={(jobId) => { setActiveJobId(jobId); setOpenSettings(null) }}
|
||||
/>
|
||||
</SettingsModal>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { RefreshCw, Lock, Loader2, X, Search, FileText, Database, Clock, CheckCircle2, Hourglass, Lightbulb, ExternalLink, ChartPie } from 'lucide-react'
|
||||
import { RefreshCw, Download, Lock, Loader2, X, Search, FileText, Database, Clock, CheckCircle2, Hourglass, Lightbulb, ExternalLink, ChartPie } from 'lucide-react'
|
||||
import { PageHeader } from '@/components/PageHeader'
|
||||
import { EmptyState } from '@/components/EmptyState'
|
||||
import { useCapabilities } from '@/lib/useSharedQueries'
|
||||
@@ -239,11 +239,11 @@ export function Financials() {
|
||||
className="text-muted hover:text-accent transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
onClick={() => handleSync(key)}
|
||||
disabled={syncing}
|
||||
title={syncing ? '正在同步…' : `同步${label}`}
|
||||
title={syncing ? '正在同步…' : `更新${label}`}
|
||||
>
|
||||
{syncing
|
||||
? <Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
: <RefreshCw className="h-3.5 w-3.5" />}
|
||||
: <Download className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 text-xl font-semibold tabular-nums text-foreground">
|
||||
|
||||
Reference in New Issue
Block a user