mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 17:54:15 +08:00
feat(minute-k): 分钟K同步流式落盘 + 段大小可配 + 回测成交修复 + 卡死超时调整
1. 分钟K流式落盘 (修复1年全市场卡死)
- sync_minute_batch 加 on_segment 回调,每段拉完立即写盘
- 抽出 _write_minute_partition 公共函数,消除重复
- 内存峰值从全量(~25GB)降到单段,避免 OOM
2. 段大小可配 (默认20交易日,范围[5,30])
- 新增 minute_sync_segment_days 偏好项
- 「单次获取」与「获取1年」共用此分段设置
- 前端设置弹窗新增分段大小步进器
3. 回测分钟K精确成交修复 (从未成功跑通过)
- _resolve_minute_fill 改接受 DataFrame,避免 to_numpy() 退化为
object 数组后字段名索引抛 IndexError
- _load_minute_for_fills 用 partition_by 向量化分组替换逐行循环
- 新增 test_minute_fill.py 回归测试 (6 用例)
4. 卡死 watchdog 超时阈值按任务类型区分
- 普通任务 600s → 1200s;分钟K长任务 → 1800s
- create(timeout_s=) 支持按 job 存阈值,reap_stale 读 job 自身值
- 修复分钟K正常拉取被误杀导致僵尸线程继续写盘的问题
5. UI 优化
- 设置弹窗按 自动同步/手动获取/清空 三区块分组
- 「单次获取」改为按分段大小拉一段 (天数=分段设置)
- 状态文案: 盘后自动同步 → 自动同步已开启/已关闭
This commit is contained in:
@@ -11,17 +11,20 @@ export function MinuteSyncConfig({ caps, onJobStart }: { caps: { label: string;
|
||||
queryFn: api.preferences,
|
||||
})
|
||||
const update = useMutation({
|
||||
mutationFn: ({ enabled, days }: { enabled: boolean; days: number }) =>
|
||||
api.updateMinuteSync(enabled, days),
|
||||
mutationFn: ({ enabled, days, segmentDays }: { enabled: boolean; days: number; segmentDays?: number }) =>
|
||||
api.updateMinuteSync(enabled, days, segmentDays),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: QK.preferences }),
|
||||
})
|
||||
|
||||
const hasMinuteCap = !!caps?.capabilities?.['kline.minute.batch']
|
||||
const enabled = prefs.data?.minute_sync_enabled ?? false
|
||||
const days = prefs.data?.minute_sync_days ?? 5
|
||||
const segmentDays = prefs.data?.minute_sync_segment_days ?? 20
|
||||
const [localDays, setLocalDays] = useState(days)
|
||||
const [localSegment, setLocalSegment] = useState(segmentDays)
|
||||
|
||||
useEffect(() => { setLocalDays(days) }, [days])
|
||||
useEffect(() => { setLocalSegment(segmentDays) }, [segmentDays])
|
||||
|
||||
const handleToggle = () => {
|
||||
if (!hasMinuteCap) return
|
||||
@@ -34,6 +37,13 @@ export function MinuteSyncConfig({ caps, onJobStart }: { caps: { label: string;
|
||||
update.mutate({ enabled, days: clamped })
|
||||
}
|
||||
|
||||
// 段大小: 交易日/段, 步进 ±5, 范围 [5, 30]。越小越省内存但越慢。
|
||||
const setSegment = (v: number) => {
|
||||
const clamped = Math.max(5, Math.min(30, v))
|
||||
setLocalSegment(clamped)
|
||||
update.mutate({ enabled, days: localDays, segmentDays: clamped })
|
||||
}
|
||||
|
||||
// 清空分钟K数据 (二次确认)
|
||||
const [confirmClear, setConfirmClear] = useState(false)
|
||||
const clearMutation = useMutation({
|
||||
@@ -48,8 +58,8 @@ export function MinuteSyncConfig({ caps, onJobStart }: { caps: { label: string;
|
||||
const [fetchingMode, setFetchingMode] = useState<'' | '40d' | '1y'>('')
|
||||
const handleFetch = (mode: '40d' | '1y') => {
|
||||
if (!hasMinuteCap) return
|
||||
// 两个按钮都用向前扩展模式: 从本地最早数据往前补, 叠加避免缺口
|
||||
const fetchDays = mode === '40d' ? 40 : 365
|
||||
// 单次获取 = 按「分段大小」拉一段 (向前扩展); 1年 = 拉365天按分段切多段
|
||||
const fetchDays = mode === '40d' ? localSegment : 365
|
||||
setFetchingMode(mode)
|
||||
api.syncMinute(fetchDays, true).then((res) => {
|
||||
qc.invalidateQueries({ queryKey: QK.pipelineJobs })
|
||||
@@ -61,7 +71,8 @@ export function MinuteSyncConfig({ caps, onJobStart }: { caps: { label: string;
|
||||
|
||||
return (
|
||||
<div className="px-4 pb-4 pt-3 border-t border-accent/20 space-y-3">
|
||||
{/* 第 1 行: 自动同步开关 + 天数 */}
|
||||
{/* 区块 A: 自动同步 (盘后定时拉取的偏好设置) */}
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<button
|
||||
@@ -78,7 +89,7 @@ export function MinuteSyncConfig({ caps, onJobStart }: { caps: { label: string;
|
||||
/>
|
||||
</button>
|
||||
<span className="text-xs text-foreground font-medium">
|
||||
{enabled ? '盘后自动同步' : '已关闭'}
|
||||
自动同步{enabled ? '已开启' : '已关闭'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -104,8 +115,45 @@ export function MinuteSyncConfig({ caps, onJobStart }: { caps: { label: string;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 第 2 行: 两个手动获取按钮 (40天快速 / 1年分段) */}
|
||||
<div className="pt-2 border-t border-border grid grid-cols-2 gap-2">
|
||||
{/* 分段大小: 控制单次 SDK 请求覆盖的交易日数。每段拉完即落盘,避免全量攒内存 OOM。
|
||||
「往前获取」与「获取 1 年」共用此设置 (两者都经过 sync_and_persist_minute)。 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs text-foreground font-medium">分段大小</span>
|
||||
<span className="text-[10px] text-muted px-1 py-px rounded bg-warning/8 text-warning/80">内存优化</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={() => setSegment(localSegment - 5)}
|
||||
disabled={!hasMinuteCap || localSegment <= 5}
|
||||
className="h-6 w-6 flex items-center justify-center rounded-l-btn bg-elevated border border-border text-secondary hover:bg-border/50 disabled:opacity-30 transition-colors text-xs"
|
||||
>−</button>
|
||||
<div className="h-6 w-8 flex items-center justify-center border-y border-border text-[11px] font-mono tabular-nums bg-base text-foreground">
|
||||
{localSegment}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSegment(localSegment + 5)}
|
||||
disabled={!hasMinuteCap || localSegment >= 30}
|
||||
className="h-6 w-6 flex items-center justify-center rounded-r-btn bg-elevated border border-border text-secondary hover:bg-border/50 disabled:opacity-30 transition-colors text-xs"
|
||||
>+</button>
|
||||
</div>
|
||||
<span className="text-[10px] text-muted">交易日/段</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted leading-relaxed -mt-1">
|
||||
每段拉完即写盘,避免内存堆积。越小越省内存但越慢,默认 20 平衡。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 区块 B: 手动获取 (一次性操作, 独立于上方自动同步开关) */}
|
||||
<div className="pt-3 border-t border-border space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Download className="h-3 w-3 text-secondary" />
|
||||
<span className="text-[11px] text-secondary font-medium">手动获取</span>
|
||||
<span className="text-[10px] text-muted">不受自动同步开关影响</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
onClick={() => handleFetch('40d')}
|
||||
disabled={!hasMinuteCap || fetchingMode !== ''}
|
||||
@@ -114,7 +162,7 @@ export function MinuteSyncConfig({ caps, onJobStart }: { caps: { label: string;
|
||||
{fetchingMode === '40d' ? (
|
||||
<><Loader2 className="h-3.5 w-3.5 animate-spin" /><span>获取中…</span></>
|
||||
) : (
|
||||
<><Download className="h-3.5 w-3.5" /><span>往前获取 (单次拉满)</span></>
|
||||
<><Download className="h-3.5 w-3.5" /><span>单次获取 {localSegment} 天</span></>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
@@ -128,9 +176,14 @@ export function MinuteSyncConfig({ caps, onJobStart }: { caps: { label: string;
|
||||
<><Calendar className="h-3.5 w-3.5" /><span>获取最近 1 年</span><span className="text-[9px] opacity-70">分段拉取</span></>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted leading-relaxed">
|
||||
A股标的 · 前复权价格 · 从本地最早数据向前叠加 ·{' '}
|
||||
均按上方「分段大小」分段拉取、每段即落盘
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 第 3 行: 清空 */}
|
||||
{/* 区块 C: 清空 (危险操作, 独立分隔) */}
|
||||
<button
|
||||
onClick={() => setConfirmClear(true)}
|
||||
disabled={clearMutation.isPending}
|
||||
@@ -141,13 +194,6 @@ export function MinuteSyncConfig({ caps, onJobStart }: { caps: { label: string;
|
||||
清空分钟K数据
|
||||
</button>
|
||||
|
||||
{/* 说明 */}
|
||||
<div className="text-[10px] text-muted leading-relaxed">
|
||||
A股标的 · 前复权价格 · 均从本地最早数据向前叠加 ·{' '}
|
||||
<span className="text-accent">单次拉满</span>约 40 个交易日,{' '}
|
||||
<span className="text-amber-400">1 年</span>按月分段 (速度较慢)
|
||||
</div>
|
||||
|
||||
{/* 清空确认弹窗 */}
|
||||
{confirmClear && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
|
||||
Reference in New Issue
Block a user