-
-
-
{ setQuery(e.target.value); setOpen(true) }}
- onFocus={() => { if (query.trim()) setOpen(true) }}
- placeholder="搜索股票名称/代码添加股票池"
- className="w-full rounded-input border border-border bg-surface py-1.5 pl-8 pr-2.5 text-xs focus:border-accent focus:outline-none"
- />
- {open && results.length > 0 && (
-
- {results.map(r => {
- const added = symbols.includes(r.symbol)
- return (
-
- )
- })}
-
- )}
+
+
+
+
{ setQuery(e.target.value); setOpen(true) }}
+ onFocus={() => { if (query.trim()) setOpen(true) }}
+ placeholder="搜索股票名称/代码添加股票池"
+ className="w-full rounded-input border border-border bg-surface py-1.5 pl-8 pr-2.5 text-xs focus:border-accent focus:outline-none"
+ />
+ {open && results.length > 0 && (
+
+ {results.map(r => {
+ const added = symbols.includes(r.symbol)
+ return (
+
+ )
+ })}
+
+ )}
+
+ {/* 操作按钮 — 紧贴输入框右侧 */}
+
+ {/* 当前范围 — 有范围显示个数, 无范围显示全市场 */}
+
+ {symbols.length === 0 ? '全市场' : `共 ${symbols.length} 只`}
+
+
+
+
{symbols.length === 0 ? (
- 留空 = 全市场,由基础过滤和策略条件筛选。
+ 默认全市场回测,由基础过滤和策略条件筛选。
) : symbols.map(symbol => {
const name = symbolNames[symbol]
return (
@@ -899,6 +948,7 @@ export function StrategyBacktest() {
const scoreMinValue = overrides.score_min == null ? '' : String(overrides.score_min)
const scoreMaxValue = overrides.score_max == null ? '' : String(overrides.score_max)
const stopLossPct = overrides.stop_loss == null ? '' : String(Math.abs(Number(overrides.stop_loss)) * 100)
+ const takeProfitPct = overrides.take_profit == null ? '' : String(Math.abs(Number(overrides.take_profit)) * 100)
const trailingStopPct = overrides.trailing_stop == null ? '' : String(Math.abs(Number(overrides.trailing_stop)) * 100)
const trailingTakeProfitActivatePct = overrides.trailing_take_profit_activate == null ? '' : String(Math.abs(Number(overrides.trailing_take_profit_activate)) * 100)
const trailingTakeProfitDrawdownPct = overrides.trailing_take_profit_drawdown == null ? '' : String(Math.abs(Number(overrides.trailing_take_profit_drawdown)) * 100)
@@ -942,6 +992,7 @@ export function StrategyBacktest() {
`卖点 ${exitSignals.length}`,
scoreFilterSummary,
stopLossPct !== '' ? `止损 ${stopLossPct}%` : '止损未设',
+ takeProfitPct !== '' ? `止盈 ${takeProfitPct}%` : '止盈未设',
trailingStopPct !== '' ? `移损 ${trailingStopPct}%` : '移损未设',
trailingTakeProfitActivatePct !== '' && trailingTakeProfitDrawdownPct !== '' ? `回撤 ${trailingTakeProfitActivatePct}-${trailingTakeProfitDrawdownPct}点` : '回撤未设',
maxHoldDaysValue !== '' ? `最长 ${maxHoldDaysValue}天` : '不限持仓',
@@ -1481,6 +1532,9 @@ export function StrategyBacktest() {
{result.strategy_info.stop_loss != null && (
止损 {fmtPct(result.strategy_info.stop_loss)}
)}
+ {result.strategy_info.take_profit != null && (
+ 止盈 {fmtPct(result.strategy_info.take_profit)}
+ )}
{result.strategy_info.trailing_stop != null && (
移损 {fmtPct(result.strategy_info.trailing_stop)}
)}
@@ -1869,7 +1923,7 @@ export function StrategyBacktest() {
{settingsTab === 'range' && (
-
留空 = 全市场}>
+
默认全市场回测,由基础过滤、策略条件和买卖触发器筛选;需要单票调试或自选池回测时再限定股票池。
@@ -2091,6 +2145,21 @@ export function StrategyBacktest() {
className={INPUT_CLS}
/>
+
- {/* Webhook 推送 (占位, 后续开发) */}
+ {/* Webhook 推送 — 飞书可用, QMT/ptrade 待定 */}
-
{error &&
{error}
}
diff --git a/frontend/src/pages/settings/Monitoring.tsx b/frontend/src/pages/settings/Monitoring.tsx
index 469c4bb..339795f 100644
--- a/frontend/src/pages/settings/Monitoring.tsx
+++ b/frontend/src/pages/settings/Monitoring.tsx
@@ -3,12 +3,12 @@ import { Link } from 'react-router-dom'
import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query'
import {
Activity,
- Shield,
Wifi,
BarChart3,
Flame,
Zap,
- Bell,
+ Webhook,
+ ChevronDown,
} from 'lucide-react'
import {
usePreferences,
@@ -53,8 +53,9 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
const realtimeEnabled = prefs?.realtime_quotes_enabled ?? false
const refreshPages = prefs?.sse_refresh_pages ?? {}
const limitLadderMonitor = prefs?.limit_ladder_monitor_enabled ?? false
- const systemNotify = prefs?.system_notify_enabled ?? false
const hasDepth = !!caps?.capabilities?.['depth5.batch']
+ // 新建监控规则时是否默认勾选飞书推送 (全局默认值, 单条规则可独立修改)
+ const webhookDefault = prefs?.webhook_enabled_default ?? false
const sidebarIndexSymbols = prefs?.sidebar_index_symbols ?? SIDEBAR_INDEX_OPTIONS.map(i => i.symbol)
const indicesPinned = prefs?.indices_nav_pinned ?? true
const isRunning = quoteStatus?.running ?? false
@@ -63,6 +64,17 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
const minInterval = intervalData?.min_interval ?? 5
const maxInterval = intervalData?.max_interval ?? 60
const [intervalDraft, setIntervalDraft] = useState(interval)
+ const feishuWebhookUrl = prefs?.feishu_webhook_url ?? ''
+ const feishuWebhookSecret = prefs?.feishu_webhook_secret ?? ''
+ const [feishuDraft, setFeishuDraft] = useState(feishuWebhookUrl)
+ const [feishuSecretDraft, setFeishuSecretDraft] = useState(feishuWebhookSecret)
+ const [feishuError, setFeishuError] = useState('')
+ // 飞书渠道配置区展开态 (推送通知卡片内)
+ const [channelOpen, setChannelOpen] = useState(false)
+ useEffect(() => {
+ setFeishuDraft(feishuWebhookUrl)
+ setFeishuSecretDraft(feishuWebhookSecret)
+ }, [feishuWebhookUrl, feishuWebhookSecret])
const watchlistSymbols = prefs?.realtime_watchlist_symbols ?? []
const watchlist = useQuery({
queryKey: QK.watchlist,
@@ -107,11 +119,31 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
qc.invalidateQueries({ queryKey: QK.preferences })
}, [qc])
- const toggleSystemNotify = useCallback(async (enabled: boolean) => {
- await api.updateSystemNotify(enabled)
+ const toggleWebhookDefault = useCallback(async (enabled: boolean) => {
+ await api.updateWebhookDefault(enabled)
qc.invalidateQueries({ queryKey: QK.preferences })
}, [qc])
+ const saveFeishuWebhook = useMutation({
+ mutationFn: ({ url, secret }: { url: string; secret: string }) => api.updateFeishuWebhook(url, secret),
+ onSuccess: () => {
+ setFeishuError('')
+ toast('飞书 Webhook 已保存', 'success')
+ qc.invalidateQueries({ queryKey: QK.preferences })
+ },
+ onError: (err: any) => setFeishuError(String(err?.message ?? '保存失败')),
+ })
+ const FEISHU_PREFIX = 'https://open.feishu.cn/open-apis/bot/v2/hook/'
+ const submitFeishu = useCallback(() => {
+ const url = feishuDraft.trim()
+ const secret = feishuSecretDraft.trim()
+ if (url && !url.startsWith(FEISHU_PREFIX)) {
+ setFeishuError('地址需以 ' + FEISHU_PREFIX + ' 开头')
+ return
+ }
+ saveFeishuWebhook.mutate({ url, secret })
+ }, [feishuDraft, feishuSecretDraft, saveFeishuWebhook])
+
const runFix = useMutation({
mutationFn: () => api.runLimitLadderFix(),
onSuccess: (data) => {
@@ -300,29 +332,7 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
{/* ========== 右列 ========== */}
- {/* 策略监控已迁移至监控中心 */}
-
-
- 策略监控、个股信号监控、价格监控已统一到「监控中心」页面,支持灵活配置触发条件、冷却期和作用范围。
-
-
- 前往监控中心配置 →
-
-
-
-
-
-
- {/* 连板梯队降级修正 */}
+ {/* 连板梯队降级修正 (移至右列顶部) */}
+
+ {/* 推送通知 — 监控告警的外部推送渠道 (全局配置)。
+ 飞书已实现; 微信开发中, QMT/ptrade 待定。
+ 每个渠道合并成一行: 勾选=新建规则默认推送, 点行展开地址配置。 */}
+
+
+ 监控规则命中后,可把告警推送到外部。勾选渠道作为新建规则的默认推送,
+ 单条规则仍可在编辑页独立修改。
+
+
+ {/* 渠道列表 — 每行一个渠道, 勾选默认 + 点行展开地址配置 */}
+
+ {/* 飞书 (可用): 勾选默认 + 展开地址配置 */}
+
+
setChannelOpen(o => !o)}
+ className="flex items-center gap-2 px-2.5 py-2 cursor-pointer transition-colors hover:bg-base/60"
+ >
+ { e.stopPropagation(); toggleWebhookDefault(e.target.checked) }}
+ onClick={e => e.stopPropagation()}
+ title="作为新建规则的默认推送渠道"
+ className="h-3 w-3 accent-accent cursor-pointer"
+ />
+ 飞书
+ 群机器人
+ {webhookDefault && (
+ 默认
+ )}
+
+ {feishuWebhookUrl ? '已配置' : '未配置'}
+
+
+
+
+ {/* 飞书地址配置 — 行内展开 */}
+ {channelOpen && (
+
+
+
+
+
+ {feishuError && (
+
{feishuError}
+ )}
+
+
+
+ {feishuWebhookUrl && (
+ ● 已配置
+ )}
+
+
+
+ 如何获取飞书 Webhook 地址?
+
+ - 打开飞书,进入目标群聊 → 群设置 → 群机器人
+ - 点击「添加机器人」→ 选择「自定义机器人」
+ - 填写机器人名称后添加,复制生成的 Webhook 地址
+ - 安全设置若启用了「签名校验」,把密钥一并复制填到「签名密钥」框
+ - 粘贴到上方输入框并保存
+
+
+ 📖 官方文档:
+
+ 自定义机器人使用指南 ↗
+
+
+
+
+ )}
+
+
+ {/* 占位渠道 — 不可点 */}
+ {[
+ { name: '微信', hint: '公众号/企业微信', status: '开发中' },
+ { name: 'QMT', hint: '量化交易终端', status: '待定' },
+ { name: 'ptrade', hint: '量化交易终端', status: '待定' },
+ ].map(ch => (
+
+
+ {ch.name}
+ {ch.hint}
+ {ch.status}
+
+ ))}
+
+
)
From a72445d782599bc017e038f73c1de49145391bfd Mon Sep 17 00:00:00 2001
From: shy3130 <415333856@qq.com>
Date: Mon, 29 Jun 2026 17:17:44 +0800
Subject: [PATCH 4/5] =?UTF-8?q?fix(pipeline):=20=E4=BF=AE=E5=A4=8D?=
=?UTF-8?q?=E7=9B=98=E5=90=8E=E7=AE=A1=E9=81=93=E9=99=A4=E6=9D=83=E5=9B=A0?=
=?UTF-8?q?=E5=AD=90=E6=8B=89=E5=8F=96=E8=8C=83=E5=9B=B4=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
daily_pipeline 除权因子改用日K范围回填 (实时增量模式兜底最近数日),
移除 max_date/incremental 依赖;kline_sync 修复 _normalize_adj_factor
重复列重命名崩溃, 新增 fetch_adj_factor_single 单股拉取;
kline.py fallback 改用 compute_enriched 应用复权;
index_sync 指数/ETF 日K同步增加批次进度回调;
同步指数日K步骤输出详细日志。
---
backend/app/api/kline.py | 13 +++++-
backend/app/jobs/daily_pipeline.py | 60 +++++++++++++++-----------
backend/app/main.py | 3 +-
backend/app/services/financial_sync.py | 18 ++++++--
backend/app/services/index_sync.py | 12 +++++-
backend/app/services/kline_sync.py | 25 ++++++++++-
backend/uv.lock | 2 +-
7 files changed, 99 insertions(+), 34 deletions(-)
diff --git a/backend/app/api/kline.py b/backend/app/api/kline.py
index 860ece8..8e0261f 100644
--- a/backend/app/api/kline.py
+++ b/backend/app/api/kline.py
@@ -7,7 +7,7 @@ from typing import Optional
from fastapi import APIRouter, HTTPException, Query, Request
-from app.indicators.pipeline import compute_enriched_single
+from app.indicators.pipeline import compute_enriched, compute_enriched_single
from app.services import kline_sync
logger = logging.getLogger(__name__)
@@ -126,7 +126,16 @@ def get_daily(
raise HTTPException(status_code=502, detail=f"TickFlow fetch failed: {e}") from e
if raw.is_empty():
return {"symbol": symbol, "name": stock_name, "stock_info": stock_info, "rows": []}
- enriched = compute_enriched_single(raw)
+ # 拉除权因子做前复权 (Starter+ 有权限), 否则空 df → compute_enriched 退回未复权
+ factors = pl.DataFrame()
+ capset = getattr(request.app.state, "capabilities", None)
+ try:
+ from app.tickflow.capabilities import Cap
+ if capset and capset.has(Cap.ADJ_FACTOR):
+ factors = kline_sync.fetch_adj_factor_single(symbol)
+ except Exception as e: # noqa: BLE001
+ logger.debug("单股除权因子拉取失败 %s: %s", symbol, e)
+ enriched = compute_enriched(raw, factors=factors)
rows = enriched.tail(days).to_dicts()
# 即使 live 模式也尝试追加实时蜡烛
rows = _maybe_inject_live_candle(request, symbol, rows)
diff --git a/backend/app/jobs/daily_pipeline.py b/backend/app/jobs/daily_pipeline.py
index 59a829a..bf02072 100644
--- a/backend/app/jobs/daily_pipeline.py
+++ b/backend/app/jobs/daily_pipeline.py
@@ -110,6 +110,9 @@ def run_now(
today = _date.today()
today_exists = latest_daily and latest_daily >= today
new_daily_days = 0
+ # 日K范围拉取的起点(分支3补缺口/分支4首次); 实时增量/跳过时为 None。
+ # 供 Step 1.5 除权因子回溯范围对齐: 范围拉取→用日K范围, 非范围→最近N天兜底。
+ daily_range_start: _date | None = None
# A 股日K拉取开关(默认开);关闭时跳过日K同步,保留已有数据
pull_a_share = _prefs.get_pipeline_pull_a_share()
@@ -130,6 +133,7 @@ def run_now(
# 也覆盖"今天已有数据但无实时行情权限(free/none)"的降级场景:
# 此时 start_date = latest_daily = today,batch 刷新当天日K。
start_date = latest_daily
+ daily_range_start = start_date
emit("sync_daily", 12, f"获取日K [{start_date} ~ {today}]…")
logger.info("sync_daily: [%s ~ %s] %s", start_date, today,
"refresh today" if today_exists else "gap fill")
@@ -150,6 +154,7 @@ def run_now(
else:
# 首次:无任何数据 → batch 拉 1 年
start_date = today - _td(days=365)
+ daily_range_start = start_date
emit("sync_daily", 12, f"获取日K [{start_date} ~ {today}]…")
logger.info("sync_daily: [%s ~ %s] initial fetch", start_date, today)
@@ -167,36 +172,22 @@ def run_now(
logger.info("sync_daily: [%s ~ %s] done", start_date, today)
_invalidate("daily")
- # Step 1.5: 增量同步除权因子 — 从已有数据最新日期的下一天开始获取
+ # Step 1.5: 同步除权因子 — 范围与日K拉取方式对齐
+ # 日K范围拉取(补缺口/首次) → 除权用日K范围 [daily_range_start, now]
+ # 首次会覆盖整个日K区间内的历史除权事件; 补缺口天然只增量(起点=latest_daily≈昨天)
+ # 日K实时增量/跳过(分支2/分支1) → 除权兜底拉最近 30 天, 补可能遗漏的新除权
+ # (这两类分支不拉历史日K, 除权不能用日K范围, 只能兜底最近几日)
written_adj = 0
affected_symbols: list[str] = []
if capset.has(Cap.ADJ_FACTOR):
from datetime import datetime, timedelta
adj_end = datetime.now()
- # 从已有除权因子数据的最新日期开始获取,避免重复拉取
- adj_factor_path = repo.store.data_dir / "adj_factor" / "all.parquet"
- fallback_start = adj_end - timedelta(days=30)
- if adj_factor_path.exists():
- try:
- from datetime import date as date_cls
- max_date = pl.scan_parquet(adj_factor_path).select(
- pl.col("trade_date").max()
- ).collect().item()
- if max_date is not None:
- # trade_date 可能是 date / datetime / string 类型
- if isinstance(max_date, str):
- td = date_cls.fromisoformat(max_date)
- elif isinstance(max_date, datetime):
- td = max_date.date()
- else:
- td = max_date
- adj_start = datetime.combine(td, datetime.min.time())
- else:
- adj_start = fallback_start
- except Exception:
- adj_start = fallback_start
+ if daily_range_start is not None:
+ adj_start = datetime.combine(daily_range_start, datetime.min.time())
else:
- adj_start = fallback_start
+ # 日K实时增量/跳过时, 除权兜底拉最近 N 天, 覆盖周末/长假/停机期间的新除权事件。
+ # 15 天: 覆盖春节/国庆最长约10天长假 + 故障恢复缓冲; sync_adj_factor 内部 merge+unique 幂等, 多拉无副作用。
+ adj_start = adj_end - timedelta(days=15)
adj_start_str = adj_start.strftime("%Y-%m-%d")
adj_end_str = adj_end.strftime("%Y-%m-%d")
emit("sync_adj", 50, f"获取除权因子 [{adj_start_str} ~ {adj_end_str}]…")
@@ -312,33 +303,46 @@ def run_now(
if pull_etf:
_types.append("ETF")
emit("sync_index", 88, f"同步{'+'.join(_types)}日K…")
+ # 子阶段进度分配: 88.0(开始) → 89.0(完成), 指数占前半, ETF 占后半
try:
if pull_index:
+ emit("sync_index", 88, "同步指数维表…")
index_count = index_sync.sync_index_instruments(repo, pull_index=True, pull_etf=False)
+ emit("sync_index", 88, f"指数维表完成,{index_count} 只")
index_dir = repo.store.data_dir / "kline_index_enriched"
index_dates = sorted(
d.name[5:] for d in index_dir.glob("date=*")
if d.is_dir() and d.name.startswith("date=")
) if index_dir.exists() else []
index_start = _date.fromisoformat(index_dates[-1]) if index_dates else today - _td(days=365)
+
+ def _index_chunk(cur: int, tot: int) -> None:
+ emit("sync_index", 88, f"指数日K批次 {cur}/{tot}",
+ stage_pct=int(100 * cur / tot) if tot else 100, skip_log=cur < tot)
+
written_index_daily = index_sync.sync_and_persist_index_daily(
repo,
capset,
start_date=_dt.combine(index_start, _dt.min.time()),
end_date=_dt.combine(today, _dt.min.time()),
+ on_chunk_done=_index_chunk,
)
+ emit("sync_index", 88, f"指数日K完成,{written_index_daily} 行")
_invalidate("index_instruments")
_invalidate("index_daily")
_invalidate("index_enriched")
if pull_etf:
+ emit("sync_index", 88, "同步 ETF 维表…")
etf_count = index_sync.sync_etf_instruments(repo)
+ emit("sync_index", 88, f"ETF 维表完成,{etf_count} 只")
etf_symbols: list[str] = []
etf_inst = repo.get_etf_instruments()
if not etf_inst.is_empty() and "symbol" in etf_inst.columns:
etf_symbols = sorted(set(etf_inst["symbol"].to_list()))
if etf_symbols and capset.has(Cap.ADJ_FACTOR):
try:
+ emit("sync_index", 88, "同步 ETF 除权因子…")
from datetime import datetime, timedelta
adj_end = datetime.now()
adj_path = repo.store.data_dir / "adj_factor_etf" / "all.parquet"
@@ -361,6 +365,7 @@ def run_now(
end_time=adj_end,
)
etf_adj_symbols = len(affected_etfs)
+ emit("sync_index", 88, f"ETF 除权因子完成,{etf_adj_symbols} 只")
except Exception as e: # noqa: BLE001
logger.warning("ETF adj_factor skipped: %s", e)
etf_dir = repo.store.data_dir / "kline_etf_enriched"
@@ -369,12 +374,19 @@ def run_now(
if d.is_dir() and d.name.startswith("date=")
) if etf_dir.exists() else []
etf_start = _date.fromisoformat(etf_dates[-1]) if etf_dates else today - _td(days=365)
+
+ def _etf_chunk(cur: int, tot: int) -> None:
+ emit("sync_index", 88, f"ETF 日K批次 {cur}/{tot}",
+ stage_pct=int(100 * cur / tot) if tot else 100, skip_log=cur < tot)
+
written_etf_daily = index_sync.sync_and_persist_etf_daily(
repo,
capset,
start_date=_dt.combine(etf_start, _dt.min.time()),
end_date=_dt.combine(today, _dt.min.time()),
+ on_chunk_done=_etf_chunk,
)
+ emit("sync_index", 88, f"ETF 日K完成,{written_etf_daily} 行")
_invalidate("etf_instruments")
_invalidate("etf_daily")
diff --git a/backend/app/main.py b/backend/app/main.py
index 4b15d2e..dcc7366 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -98,7 +98,8 @@ async def lifespan(app: FastAPI):
except Exception as e: # noqa: BLE001
logger.warning("内置扩展表初始化失败 (不影响启动): %s", e)
- # 财务数据独立调度 (需 Expert 套餐)
+ # 财务数据 (需 Expert 套餐): 仅初始化调度器供 /api/financials/sync/* 手动同步,
+ # 不启动自动调度——用户在「财务分析」页点「同步」手动拉取。
from app.services.financial_sync import financial_scheduler
financial_scheduler.start(store.data_dir, capset)
app.state.financial_scheduler = financial_scheduler
diff --git a/backend/app/services/financial_sync.py b/backend/app/services/financial_sync.py
index 22dee64..983b67f 100644
--- a/backend/app/services/financial_sync.py
+++ b/backend/app/services/financial_sync.py
@@ -200,13 +200,18 @@ class FinancialScheduler:
# 手动同步(run_now)是否正在进行。前端据此显示"同步中"并防重复点击。
self._is_syncing = False
- def start(self, data_dir: Path, capset: CapabilitySet) -> None:
+ def start(self, data_dir: Path, capset: CapabilitySet, *, auto_schedule: bool = False) -> None:
+ """初始化调度器,并按需启动周期同步后台任务。
+
+ auto_schedule=False (默认): 仅初始化 (设置数据目录/能力 + 恢复 last_sync),
+ 供 /api/financials/sync/* 手动同步使用, 不启动自动调度。
+ auto_schedule=True: 额外启动每周一次的 metrics 自动同步 (启动后 60s 首跑)。
+ """
if not capset.has(Cap.FINANCIAL):
logger.info("FinancialScheduler skipped: no FINANCIAL capability")
return
self._data_dir = data_dir
self._capset = capset
- self._running = True
# 从持久化恢复上次同步时间: 重启后前端仍能显示真实最后同步时间,而非"尚未同步"
try:
from app.services import preferences
@@ -227,8 +232,15 @@ class FinancialScheduler:
logger.info("FinancialScheduler restored last_sync: %s", list(self._last_sync.keys()))
except Exception as e: # noqa: BLE001
logger.warning("restore financial_sync_times failed: %s", e)
+
+ if not auto_schedule:
+ # 仅初始化 (手动同步用), 不启动周期任务。
+ logger.info("FinancialScheduler initialized (auto-schedule disabled; manual sync only)")
+ return
+
+ self._running = True
self._task = asyncio.create_task(self._run_loop())
- logger.info("FinancialScheduler started")
+ logger.info("FinancialScheduler started (auto-schedule enabled)")
def _record_sync(self, table: str) -> None:
"""记录一张表的同步完成时间: 更新内存 + 持久化到 preferences.json。
diff --git a/backend/app/services/index_sync.py b/backend/app/services/index_sync.py
index 4f689b9..42f3b1b 100644
--- a/backend/app/services/index_sync.py
+++ b/backend/app/services/index_sync.py
@@ -8,6 +8,7 @@ from __future__ import annotations
import logging
import gc
+from collections.abc import Callable
from datetime import datetime, timedelta
import polars as pl
@@ -194,11 +195,13 @@ def sync_and_persist_index_daily(
start_date: datetime | None = None,
end_date: datetime | None = None,
symbols_override: list[str] | None = None,
+ on_chunk_done: Callable[[int, int], None] | None = None,
) -> int:
"""同步指数/ETF 日K到独立 parquet,并计算 enriched。
symbols_override 非空时,只拉这些代码(跳过 instruments 表),用于自定义范围。
否则取 index_instruments 表全量(指数+ETF 合并存储)。
+ on_chunk_done(current, total) 每个批次完成后回调。
"""
if not capset.has(Cap.KLINE_DAILY_BATCH):
return 0
@@ -248,6 +251,8 @@ def sync_and_persist_index_daily(
repo.append_index_enriched(enriched)
total_rows += raw.height
logger.info("index/etf daily synced: %d/%d chunks, +%d rows", i + 1, len(chunks), raw.height)
+ if on_chunk_done:
+ on_chunk_done(i + 1, len(chunks))
del raw, enriched
gc.collect()
repo.refresh_index_views()
@@ -292,8 +297,11 @@ def sync_and_persist_etf_daily(
start_date: datetime | None = None,
end_date: datetime | None = None,
symbols_override: list[str] | None = None,
+ on_chunk_done: Callable[[int, int], None] | None = None,
) -> int:
- """同步 ETF 日K到独立 kline_etf_* parquet,并计算 ETF enriched。"""
+ """同步 ETF 日K到独立 kline_etf_* parquet,并计算 ETF enriched。
+ on_chunk_done(current, total) 每个批次完成后回调。
+ """
if not capset.has(Cap.KLINE_DAILY_BATCH):
return 0
@@ -344,6 +352,8 @@ def sync_and_persist_etf_daily(
repo.append_etf_enriched(enriched)
total_rows += raw.height
logger.info("etf daily synced: %d/%d chunks, +%d rows", i + 1, len(chunks), raw.height)
+ if on_chunk_done:
+ on_chunk_done(i + 1, len(chunks))
del raw, enriched
gc.collect()
repo.refresh_index_views()
diff --git a/backend/app/services/kline_sync.py b/backend/app/services/kline_sync.py
index cff2f70..7a4d5f5 100644
--- a/backend/app/services/kline_sync.py
+++ b/backend/app/services/kline_sync.py
@@ -241,8 +241,14 @@ def _normalize_adj_factor(raw) -> pl.DataFrame:
df = pl.from_pandas(raw.reset_index() if hasattr(raw, "reset_index") else raw)
if df.is_empty():
return df
- rename_map = {"timestamp": "trade_date", "date": "trade_date", "adj_factor": "ex_factor"}
- df = df.rename({k: v for k, v in rename_map.items() if k in df.columns})
+ # rename: timestamp/date → trade_date, adj_factor → ex_factor
+ # 注意: 新版 SDK 可能同时返回 timestamp 和 trade_date (或 adj_factor 和 ex_factor),
+ # 直接 rename 会产生重复列报错。仅当目标列不存在时才 rename。
+ rename_map: dict[str, str] = {}
+ for src, dst in (("timestamp", "trade_date"), ("date", "trade_date"), ("adj_factor", "ex_factor")):
+ if src in df.columns and dst not in df.columns:
+ rename_map[src] = dst
+ df = df.rename(rename_map)
if "trade_date" in df.columns:
if df.schema["trade_date"] in {pl.Int64, pl.Int32, pl.UInt64, pl.UInt32, pl.Float64, pl.Float32}:
df = df.with_columns(
@@ -481,6 +487,21 @@ def fetch_minute_single(symbol: str, trade_date: date) -> pl.DataFrame:
return pl.DataFrame()
+def fetch_adj_factor_single(symbol: str) -> pl.DataFrame:
+ """从 TickFlow 实时拉取单股除权因子(不写入本地), 用于单股 K 线即时前复权。
+
+ 返回结构: symbol, trade_date, ex_factor (空 DataFrame 表示无除权事件或拉取失败)。
+ 与 _apply_adj_factor / compute_enriched 的 factors 参数格式一致。
+ """
+ tf = get_client()
+ try:
+ raw = tf.klines.ex_factors([symbol], as_dataframe=True, show_progress=False)
+ except Exception as e: # noqa: BLE001
+ logger.warning("fetch_adj_factor_single(%s) failed: %s", symbol, e)
+ return pl.DataFrame()
+ return _normalize_adj_factor(raw)
+
+
def _latest_minute_datetime(repo: KlineRepository) -> datetime | None:
"""本地分钟 K 数据的最新时间。"""
try:
diff --git a/backend/uv.lock b/backend/uv.lock
index 233de5f..7ec7052 100644
--- a/backend/uv.lock
+++ b/backend/uv.lock
@@ -2491,7 +2491,7 @@ all = [
[[package]]
name = "tickflow-stock-panel-backend"
-version = "0.1.63"
+version = "0.1.64"
source = { editable = "." }
dependencies = [
{ name = "apscheduler" },
From 73490fdf5ec49c2b753b121640481c359002223c Mon Sep 17 00:00:00 2001
From: shy3130 <415333856@qq.com>
Date: Mon, 29 Jun 2026 17:21:01 +0800
Subject: [PATCH 5/5] chore: bump version to 0.1.65
---
backend/pyproject.toml | 2 +-
frontend/package.json | 2 +-
.../src/components/data/PageSettingsModal.tsx | 212 +++++++++---
frontend/src/lib/storage.ts | 2 +
frontend/src/pages/Dashboard.tsx | 17 +-
frontend/src/pages/Data.tsx | 321 ++++++++++--------
frontend/src/pages/Financials.tsx | 2 +-
frontend/src/pages/StockAnalysis.tsx | 2 +-
frontend/src/pages/settings/MenuSettings.tsx | 1 +
screenshots/financials-ultrawide-fix.png | Bin 594611 -> 0 bytes
screenshots/stock-analysis-ultrawide-fix.png | Bin 744432 -> 0 bytes
11 files changed, 365 insertions(+), 196 deletions(-)
delete mode 100644 screenshots/financials-ultrawide-fix.png
delete mode 100644 screenshots/stock-analysis-ultrawide-fix.png
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 6f07f15..ebce2c4 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "tickflow-stock-panel-backend"
-version = "0.1.64"
+version = "0.1.65"
description = "A 股选股 + 监控 + 回测面板 — TickFlow 适配"
readme = "../README.md"
requires-python = ">=3.11"
diff --git a/frontend/package.json b/frontend/package.json
index 07720ae..50abd9e 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "tickflow-stock-panel-frontend",
"private": true,
- "version": "0.1.64",
+ "version": "0.1.65",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/components/data/PageSettingsModal.tsx b/frontend/src/components/data/PageSettingsModal.tsx
index 121cce3..86abaae 100644
--- a/frontend/src/components/data/PageSettingsModal.tsx
+++ b/frontend/src/components/data/PageSettingsModal.tsx
@@ -1,5 +1,22 @@
import { useState } from 'react'
-import { Check } from 'lucide-react'
+import {
+ DndContext,
+ closestCenter,
+ KeyboardSensor,
+ PointerSensor,
+ useSensor,
+ useSensors,
+ type DragEndEvent,
+} from '@dnd-kit/core'
+import {
+ arrayMove,
+ SortableContext,
+ sortableKeyboardCoordinates,
+ useSortable,
+ verticalListSortingStrategy,
+} from '@dnd-kit/sortable'
+import { CSS } from '@dnd-kit/utilities'
+import { Check, GripVertical } from 'lucide-react'
import { storage } from '@/lib/storage'
export type CardKey =
@@ -12,20 +29,26 @@ interface CardDef {
desc: string
/** 档位能力不足时该卡片是否默认隐藏(减少干扰) */
defaultHiddenIfNoCap: boolean
+ /** 无条件默认隐藏(用户可在设置里手动开启) */
+ defaultHidden?: boolean
}
-/** 数据画像卡片定义 —— 顺序即弹窗展示顺序 */
+/** 数据画像卡片定义 —— 默认顺序即此数组顺序 */
export const DATA_CARD_DEFS: CardDef[] = [
{ key: 'instruments', label: '个股维表', desc: 'A 股股票元数据', defaultHiddenIfNoCap: false },
{ key: 'daily', label: '日 K', desc: 'A 股日K线数据', defaultHiddenIfNoCap: false },
+ { key: 'adj_factor', label: '除权因子', desc: '复权计算因子', defaultHiddenIfNoCap: true },
{ key: 'enriched', label: 'Enriched', desc: '技术指标计算结果', defaultHiddenIfNoCap: false },
{ key: 'index', label: '指数', desc: '主要市场指数日K', defaultHiddenIfNoCap: false },
- { key: 'etf', label: 'ETF', desc: '场内交易基金日K', defaultHiddenIfNoCap: false },
- { key: 'adj_factor', label: '除权因子', desc: '复权计算因子', defaultHiddenIfNoCap: true },
+ { key: 'etf', label: 'ETF', desc: '场内交易基金日K', defaultHiddenIfNoCap: false, defaultHidden: true },
{ key: 'minute', label: '分钟 K', desc: '分钟级K线(需 Pro+)', defaultHiddenIfNoCap: true },
{ key: 'financials', label: '财务数据', desc: '财报数据(需 Expert)', defaultHiddenIfNoCap: true },
]
+const DEFAULT_ORDER = DATA_CARD_DEFS.map(d => d.key)
+/** 恢复默认时显示的卡片数量(按默认顺序取前 N 张) */
+const DEFAULT_VISIBLE_COUNT = 5
+
const CAP_KEY_MAP: Partial
> = {
adj_factor: 'adj_factor',
minute: 'kline.minute.batch',
@@ -35,6 +58,7 @@ const CAP_KEY_MAP: Partial> = {
/**
* 读取卡片显隐状态。结合档位能力决定默认值:
* - 用户显式设置过 → 用设置值
+ * - 未设置 + defaultHidden → 隐藏(无条件默认隐藏)
* - 未设置 + defaultHiddenIfNoCap + 当前无能力 → 隐藏
* - 其他 → 显示
*/
@@ -47,6 +71,8 @@ export function getCardVisibility(
for (const def of DATA_CARD_DEFS) {
if (def.key in override) {
result[def.key] = override[def.key]
+ } else if (def.defaultHidden) {
+ result[def.key] = false
} else {
result[def.key] = def.defaultHiddenIfNoCap ? has(CAP_KEY_MAP[def.key] ?? '') : true
}
@@ -54,60 +80,102 @@ export function getCardVisibility(
return result
}
+/**
+ * 读取卡片显示顺序。
+ * - 用户拖拽设置过 → 用设置值(过滤掉已不存在的 key, 补齐新增的 key)
+ * - 未设置 → 用 DATA_CARD_DEFS 默认顺序
+ */
+export function getCardOrder(): CardKey[] {
+ const saved = storage.dataCardOrder.get([])
+ if (!saved.length) return [...DEFAULT_ORDER]
+ const known = new Set(DEFAULT_ORDER)
+ const ordered = saved.filter(k => known.has(k as CardKey)) as CardKey[]
+ // 补齐新增的 key(默认顺序里新增的卡片追加到末尾)
+ for (const k of DEFAULT_ORDER) {
+ if (!ordered.includes(k)) ordered.push(k)
+ }
+ return ordered
+}
+
export function PageSettingsModal({
caps,
}: {
caps: Record | undefined
}) {
const [visible, setVisible] = useState>(() => getCardVisibility(caps))
+ const [order, setOrder] = useState(() => getCardOrder())
- const toggle = (key: CardKey) => {
- const next = { ...visible, [key]: !visible[key] }
+ const persistVisible = (next: Record) => {
setVisible(next)
storage.dataCardVisible.set(next)
window.dispatchEvent(new CustomEvent('data-card-visible-change'))
}
-
- const reset = () => {
- storage.dataCardVisible.set({})
- setVisible(getCardVisibility(caps))
+ const persistOrder = (next: CardKey[]) => {
+ setOrder(next)
+ storage.dataCardOrder.set(next)
window.dispatchEvent(new CustomEvent('data-card-visible-change'))
}
+ const toggle = (key: CardKey) => persistVisible({ ...visible, [key]: !(visible[key] ?? true) })
+
+ const reset = () => {
+ // 恢复默认: 默认顺序 + 仅勾选前 5 张卡片, 其余隐藏
+ const defaultOrder = [...DEFAULT_ORDER]
+ const defaultVisible: Record = {}
+ defaultOrder.forEach((k, i) => { defaultVisible[k] = i < DEFAULT_VISIBLE_COUNT })
+ storage.dataCardVisible.set(defaultVisible)
+ storage.dataCardOrder.set(defaultOrder)
+ setVisible(defaultVisible)
+ setOrder(defaultOrder)
+ window.dispatchEvent(new CustomEvent('data-card-visible-change'))
+ }
+
+ const sensors = useSensors(
+ useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
+ )
+
+ const handleDragEnd = (event: DragEndEvent) => {
+ const { active, over } = event
+ if (!over || active.id === over.id) return
+ const oldIdx = order.indexOf(active.id as CardKey)
+ const newIdx = order.indexOf(over.id as CardKey)
+ if (oldIdx < 0 || newIdx < 0) return
+ persistOrder(arrayMove(order, oldIdx, newIdx))
+ }
+
+ // 按 order 排序卡片定义
+ const defByKey = new Map(DATA_CARD_DEFS.map(d => [d.key, d]))
+ const orderedDefs = order.map(k => defByKey.get(k)!).filter(Boolean)
+
return (
- 勾选要在数据画像区显示的卡片。未勾选的卡片将隐藏,不影响数据本身。
+ 拖动手柄调整卡片顺序,勾选控制显隐。未勾选的卡片将隐藏,不影响数据本身。
-
- {DATA_CARD_DEFS.map((def) => {
- const on = visible[def.key] ?? true
- return (
-
- )
- })}
-
+
+
+
+ {orderedDefs.map((def) => {
+ const on = visible[def.key] ?? true
+ return (
+ toggle(def.key)}
+ />
+ )
+ })}
+
+
+
+ {/* Free 档提示: 大盘看板为盘后数据, 仅自选股实时。避免用户误读为全市场实时。 */}
+ {quoteMode === 'watchlist' && (
+
+
+
+ 当前为「自选实时」模式,看板展示的大盘数据为盘后快照(最新有数据日),并非盘中实时;
+ 仅自选股({data.quote_status?.watchlist_symbol_count ?? 0} 只)支持实时监控。
+ 全市场实时需 Starter+
+
+
+ )}
+
{data.indices.map(item => )}
diff --git a/frontend/src/pages/Data.tsx b/frontend/src/pages/Data.tsx
index 54b1d92..ffc47cc 100644
--- a/frontend/src/pages/Data.tsx
+++ b/frontend/src/pages/Data.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useRef, useState } from 'react'
+import { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { motion, AnimatePresence } from 'framer-motion'
import {
@@ -42,7 +42,7 @@ import { ExtendHistoryPanel } from '@/components/data/ExtendHistoryPanel'
import { EnrichedRebuildPanel } from '@/components/data/EnrichedRebuildPanel'
import { MinuteSyncConfig } from '@/components/data/MinuteSyncConfig'
import { PipelineScopeConfig } from '@/components/data/PipelineScopeConfig'
-import { PageSettingsModal, getCardVisibility } from '@/components/data/PageSettingsModal'
+import { PageSettingsModal, getCardVisibility, getCardOrder, type CardKey } from '@/components/data/PageSettingsModal'
import { QuoteConfigCard } from '@/components/data/QuoteConfigCard'
import { EnrichedSchemaModal } from '@/components/data/SchemaModal'
import { Skeleton } from '@/components/data/Skeleton'
@@ -317,6 +317,171 @@ export function Data() {
})
}, [])
+ // 按卡片 key 渲染对应的 StatCard (顺序由 getCardOrder 控制, 显隐由 cardVisible 控制)
+ const renderStatCard = (k: CardKey): React.ReactNode => {
+ switch (k) {
+ case 'instruments':
+ return (
+
setSchemaTable('instruments')}
+ />
+ )
+ case 'daily':
+ return (
+ setSchemaTable('daily')}
+ onSettings={hasData ? () => setOpenSettings(v => v === 'daily' ? null : 'daily') : undefined}
+ settingsOpen={openSettings === 'daily'}
+ />
+ )
+ case 'adj_factor':
+ return (
+ setSchemaTable('adj_factor')}
+ />
+ )
+ case 'enriched':
+ return (
+ setSchemaTable('enriched')}
+ onSettings={hasData ? () => setOpenSettings(v => v === 'enriched' ? null : 'enriched') : undefined}
+ settingsOpen={openSettings === 'enriched'}
+ />
+ )
+ case 'index':
+ return (
+ setSchemaTable(t ?? 'index_daily')}
+ onSettings={hasData ? () => setOpenSettings(v => v === 'index' ? null : 'index') : undefined}
+ settingsOpen={openSettings === 'index'}
+ />
+ )
+ case 'etf':
+ return (
+ setSchemaTable(t ?? 'etf_daily')}
+ />
+ )
+ case 'minute':
+ return (
+ setSchemaTable('minute')}
+ onSettings={hasData ? () => setOpenSettings(v => v === 'minute' ? null : 'minute') : undefined}
+ settingsOpen={openSettings === 'minute'}
+ />
+ )
+ case 'financials':
+ return (
+
+ )
+ default:
+ return null
+ }
+ }
+
return (
<>
@@ -620,155 +785,9 @@ export function Data() {
数据画像
- {cardVisible.instruments && (
-
setSchemaTable('instruments')}
- />
- )}
- {cardVisible.daily && (
- setSchemaTable('daily')}
- onSettings={hasData ? () => setOpenSettings(v => v === 'daily' ? null : 'daily') : undefined}
- settingsOpen={openSettings === 'daily'}
- />
- )}
- {cardVisible.enriched && (
- setSchemaTable('enriched')}
- onSettings={hasData ? () => setOpenSettings(v => v === 'enriched' ? null : 'enriched') : undefined}
- settingsOpen={openSettings === 'enriched'}
- />
- )}
- {cardVisible.index && (
- setSchemaTable(t ?? 'index_daily')}
- onSettings={hasData ? () => setOpenSettings(v => v === 'index' ? null : 'index') : undefined}
- settingsOpen={openSettings === 'index'}
- />
- )}
- {cardVisible.etf && (
- setSchemaTable(t ?? 'etf_daily')}
- />
- )}
- {cardVisible.adj_factor && (
- setSchemaTable('adj_factor')}
- />
- )}
- {cardVisible.minute && (
- setSchemaTable('minute')}
- onSettings={hasData ? () => setOpenSettings(v => v === 'minute' ? null : 'minute') : undefined}
- settingsOpen={openSettings === 'minute'}
- />
- )}
- {cardVisible.financials && (
-
- )}
+ {getCardOrder().filter(k => cardVisible[k]).map((k: CardKey) => (
+ {renderStatCard(k)}
+ ))}
{(extConfigs.data?.items ?? []).map((ext) => (
-
+
{syncing && (
diff --git a/frontend/src/pages/StockAnalysis.tsx b/frontend/src/pages/StockAnalysis.tsx
index 959a284..905ed65 100644
--- a/frontend/src/pages/StockAnalysis.tsx
+++ b/frontend/src/pages/StockAnalysis.tsx
@@ -90,7 +90,7 @@ export function StockAnalysis() {
}
/>
-
+
{/* 搜索栏 */}
diff --git a/frontend/src/pages/settings/MenuSettings.tsx b/frontend/src/pages/settings/MenuSettings.tsx
index 505bbbc..14d7303 100644
--- a/frontend/src/pages/settings/MenuSettings.tsx
+++ b/frontend/src/pages/settings/MenuSettings.tsx
@@ -39,6 +39,7 @@ const BUILTIN_PAGES: NavEntry[] = [
{ id: '/concept-analysis', label: '概念分析', type: 'builtin', visible: true },
{ id: '/industry-analysis', label: '行业分析', type: 'builtin', visible: true },
{ id: '/stock-analysis', label: '个股分析', type: 'builtin', visible: true },
+ { id: '/review', label: '复盘', type: 'builtin', visible: true },
{ id: '/financials', label: '财务分析', type: 'builtin', visible: true },
{ id: '/indices', label: '指数', type: 'builtin', visible: true },
{ id: '/trading', label: '交易', type: 'builtin', visible: true },
diff --git a/screenshots/financials-ultrawide-fix.png b/screenshots/financials-ultrawide-fix.png
deleted file mode 100644
index 1ee13d8c5f20758eac874bab7d6e602bd5ca08e5..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 594611
zcmeFZXIPW@x;<=f3?c%8fS@vpbWw`*u^=i40@6zmq)3aj(20lwQgskPAfO;11PDE}
zBq&vS4?RkR011Rp5=cn$KFm4$oN=FbU)PyE{=c0sBGLTny4St#@;oo@>FaPF5j?VM
z*DlW6w{95i+QmcPwd-Ksp##7tyLpao?ELUC(z&*)q(^vR*RIpMZr`|iKfsp6Je={+
zF?5~{g%<`ZISDvDIB3Xg@$^viJNqG**B{63$KAHhnTrujaZ%|1D9^gv_p9e^J?lnz
z>;4oWhe@#;@q)DILpATX9l0OEp1)mQC@I9TRr2+RRZND7>s`JE7$U9SKM$U{mO%*2
zGS2^mjqNR-B7Y)OHO21Qy@yfI*t4YE{QY~6#ZpPX`AN5tQ}Xb#;NZRl53Ejw3L0rp
zZ_wkPpTm{draAX&tQJwTdXTcf-kf<_f*&Jj$eR{ho>YU7;-Fy@!3XCw@s*&MgPJSo
zF$O=IkTRo|5(>TA3%;~R*U&DH+-)?Oq{wsnw
=HnWIb+<5RsvL@3L=v&
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