mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 16:44:15 +08:00
feat(mainline): 主线与情绪周期统计默认剔除风险警示(ST)股
问题: ST板块 241 天中 209 天上主线榜、108 天第一。原因: ① ST 是跨行业 状态桶(416只)非投资题材, 真实主线被概念拆分而 ST 统一聚桶; ② 主板 ST 2026-07-06 前涨跌幅 5%(价格规则已内置切换日), 封板成本减半, 弱市刷板 (7/1 全市场 218 家涨停中 67 家 ST); ③ 概念成分/ST 标记按当前维表快照 回写历史存在归属污染。 改动: - market_mainline: load_risk_warning_symbols(维表名称含 ST, 与涨跌停 判定共用 price_limits 权威实现, 600s 缓存); compute_mainline_range 聚合前剔除, exclude_st 参数可覆盖 - regime_builder: run_regime_batch 汇聚点统一剔除(情绪驱动与主线同 口径); 宽度类指标同 df 过滤, 占比几乎不受影响 - preferences: sentiment_exclude_st 默认 True; 经 mainline-filter API 可读写; 前端过滤面板加开关, 切换时自动全量重算 - 测试: 符号集加载/缓存、主线剔除(计数/龙头/最小涨停门槛交互)、 regime 剔除 A/B、偏好往返; 说明书 FAQ 24.11 验证: 后端 918 测试通过(新增 5 个); tsc 通过; 全量重算后主线 0 行 ST 上榜(此前 209 天), rank1 变为海工装备/卫星导航/液冷服务器等真实 主题; 7/1 涨停数 218→151 与 ST 占比精确吻合; A/B 对照: 阶段标签 34/242 天变化(rally/ignite→repair 为主, 方向符合预期 — 剔除 ST 灌水 后正面阶段更难触发), repair 主导(78%)为口径切换前既有特征。
This commit is contained in:
@@ -966,16 +966,20 @@ class PipelineIndexSymbolsIn(BaseModel):
|
||||
|
||||
|
||||
class MainlineFilterIn(BaseModel):
|
||||
"""市场主线过滤配置(宽基/风格标签按成员数过滤 + 名称黑名单)。"""
|
||||
"""市场主线过滤配置(宽基/风格标签按成员数过滤 + 名称黑名单 + ST 剔除开关)。"""
|
||||
|
||||
min_members: int | None = None
|
||||
max_members: int | None = None
|
||||
blacklist: list[str] | str | None = None
|
||||
exclude_st: bool | None = None
|
||||
|
||||
|
||||
@router.put("/preferences/mainline-filter")
|
||||
def update_mainline_filter(req: MainlineFilterIn) -> dict:
|
||||
"""更新市场主线过滤配置。部分更新; 修改后需重算主线(POST /api/regime/mainline/recompute)生效。"""
|
||||
"""更新市场主线过滤配置。部分更新; 修改后需重算主线(POST /api/regime/mainline/recompute)生效。
|
||||
|
||||
exclude_st 同步控制市场环境(regime)统计口径 — 切换后需全量重算 regime。
|
||||
"""
|
||||
from app.services import preferences
|
||||
|
||||
payload = req.model_dump()
|
||||
|
||||
@@ -15,6 +15,7 @@ join 概念映射后 group_by, 峰值内存 <100MB。
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
@@ -68,6 +69,35 @@ def mainline_path(data_dir: Path) -> Path:
|
||||
return data_dir / MAINLINE_DIR / "part.parquet"
|
||||
|
||||
|
||||
_ST_SYMBOLS_CACHE: tuple[float, frozenset[str]] | None = None
|
||||
|
||||
|
||||
def load_risk_warning_symbols(data_dir: Path) -> frozenset[str]:
|
||||
"""当前维表快照中名称含 ST 标记的 symbol 集合(大写), 供主线/情绪统计剔除。
|
||||
|
||||
判定与 indicators 涨跌停口径共用同一权威实现(price_limits.polars_is_risk_warning_name,
|
||||
即名称含 "ST", 覆盖 ST/*ST/S*ST)。维表是快照无历史版本, 与概念成分同样的
|
||||
回看限制。600s 进程内缓存(维表 snapshot 进程内不变)。
|
||||
"""
|
||||
global _ST_SYMBOLS_CACHE
|
||||
now = time.time()
|
||||
if _ST_SYMBOLS_CACHE is not None and now - _ST_SYMBOLS_CACHE[0] < 600:
|
||||
return _ST_SYMBOLS_CACHE[1]
|
||||
from app.price_limits import polars_is_risk_warning_name
|
||||
|
||||
syms: frozenset[str] = frozenset()
|
||||
inst_dir = data_dir / "instruments"
|
||||
if inst_dir.exists():
|
||||
try:
|
||||
df = pl.read_parquet(inst_dir / "**" / "*.parquet").select(["symbol", "name"])
|
||||
st = df.filter(polars_is_risk_warning_name(pl.col("name")))
|
||||
syms = frozenset(s.upper() for s in st["symbol"].to_list())
|
||||
except Exception as e:
|
||||
logger.warning("load risk-warning symbols failed: %s", e)
|
||||
_ST_SYMBOLS_CACHE = (now, syms)
|
||||
return syms
|
||||
|
||||
|
||||
def load_mainline_history(data_dir: Path, kind: str = "concept") -> pl.DataFrame:
|
||||
"""读取主线时序(全部 kind), 不存在返回空 DataFrame。"""
|
||||
p = mainline_path(data_dir)
|
||||
@@ -92,12 +122,15 @@ def _industry_member(member: str, kind: str) -> str:
|
||||
|
||||
def compute_mainline_range(repo, data_dir: Path, start: date, end: date,
|
||||
kind: str = "concept",
|
||||
filter_cfg: dict | None = None) -> pl.DataFrame:
|
||||
filter_cfg: dict | None = None,
|
||||
exclude_st: bool | None = None) -> pl.DataFrame:
|
||||
"""计算 [start, end] 每日主线排行(按 _SCORE_WEIGHTS 加权截面分)。
|
||||
|
||||
filter_cfg: {"min_members", "max_members", "blacklist"}; None 时读用户偏好。
|
||||
宽基/风格标签(融资融券/沪深股通等数千成分)按成员数上限过滤,
|
||||
用户黑名单按名称过滤(不论大小)。修改配置后重算主线生效。
|
||||
exclude_st: 是否剔除风险警示(ST)股(按当前维表名称); None 时读用户偏好
|
||||
(默认剔除 — ST 是状态桶非题材, 主板 5% 便宜板时代曾系统性霸榜)。
|
||||
|
||||
返回列: date, kind, member, limit_up_count, ge2_count, max_boards,
|
||||
boards_sum, rungs_filled, leader_symbol, score, rank。空数据返回空表。
|
||||
@@ -138,6 +171,19 @@ def compute_mainline_range(repo, data_dir: Path, start: date, end: date,
|
||||
return pl.DataFrame()
|
||||
|
||||
limit_rows = limit_rows.with_columns(pl.col("symbol").str.to_uppercase().alias("_sym_up"))
|
||||
|
||||
# 剔除风险警示股: ST 板块的涨停生态(主板曾 5% 便宜板)不代表题材主线。
|
||||
if exclude_st is None:
|
||||
try:
|
||||
from app.services import preferences
|
||||
exclude_st = preferences.get_sentiment_exclude_st()
|
||||
except Exception:
|
||||
exclude_st = True
|
||||
if exclude_st:
|
||||
st_syms = load_risk_warning_symbols(repo.store.data_dir)
|
||||
if st_syms:
|
||||
limit_rows = limit_rows.filter(~pl.col("_sym_up").is_in(sorted(st_syms)))
|
||||
|
||||
joined = limit_rows.join(map_df, on="_sym_up", how="inner")
|
||||
if joined.is_empty():
|
||||
return pl.DataFrame()
|
||||
|
||||
@@ -385,12 +385,28 @@ def get_mainline_blacklist() -> list[str]:
|
||||
return [str(x).strip() for x in v if str(x).strip()]
|
||||
|
||||
|
||||
def get_sentiment_exclude_st() -> bool:
|
||||
"""市场环境/主线统计是否剔除风险警示(ST)股。默认 True。
|
||||
|
||||
口径: 主板 ST 在 2026-07 前享 5% 涨跌幅(封板成本减半), 且 ST 是跨行业的
|
||||
状态桶而非投资题材, 混入会系统性抬高涨停宽度/高度(弱市尤甚)。剔除后
|
||||
涨跌家数等宽度占比几乎不受影响。修改后需重算 regime 与主线生效。
|
||||
"""
|
||||
return bool(load().get("sentiment_exclude_st", True))
|
||||
|
||||
|
||||
def set_sentiment_exclude_st(v: bool) -> bool:
|
||||
save({"sentiment_exclude_st": bool(v)})
|
||||
return get_sentiment_exclude_st()
|
||||
|
||||
|
||||
def get_mainline_filter_config() -> dict:
|
||||
"""主线过滤配置汇总(供 API 返回与计算读取)。"""
|
||||
return {
|
||||
"min_members": get_mainline_min_members(),
|
||||
"max_members": get_mainline_max_members(),
|
||||
"blacklist": get_mainline_blacklist(),
|
||||
"exclude_st": get_sentiment_exclude_st(),
|
||||
}
|
||||
|
||||
|
||||
@@ -401,6 +417,8 @@ def set_mainline_filter_config(cfg: dict) -> dict:
|
||||
updates["mainline_min_members"] = cfg["min_members"]
|
||||
if "max_members" in cfg and cfg["max_members"] is not None:
|
||||
updates["mainline_max_members"] = cfg["max_members"]
|
||||
if "exclude_st" in cfg and cfg["exclude_st"] is not None:
|
||||
updates["sentiment_exclude_st"] = bool(cfg["exclude_st"])
|
||||
if "blacklist" in cfg and cfg["blacklist"] is not None:
|
||||
raw = cfg["blacklist"]
|
||||
if isinstance(raw, str):
|
||||
|
||||
@@ -449,6 +449,23 @@ def run_regime_batch(repo, start: date, end: date) -> pl.DataFrame:
|
||||
logger.info("regime batch: no enriched data for [%s~%s]", start, end)
|
||||
return pl.DataFrame()
|
||||
|
||||
# 口径: 默认剔除风险警示(ST)股(与主线统计同一开关) — 主板 ST 在 2026-07 前
|
||||
# 享 5% 涨跌幅且是跨行业状态桶, 混入会系统性抬高涨停宽度/高度(弱市炒 ST 尤甚)。
|
||||
# 涨跌家数/MA20 占比等宽度指标几乎不受影响。切换口径需全量重算 regime。
|
||||
try:
|
||||
from app.services import preferences as _prefs_st
|
||||
exclude_st = _prefs_st.get_sentiment_exclude_st()
|
||||
except Exception:
|
||||
exclude_st = True
|
||||
if exclude_st:
|
||||
from app.services.market_mainline import load_risk_warning_symbols
|
||||
|
||||
st_syms = load_risk_warning_symbols(repo.store.data_dir)
|
||||
if st_syms and "symbol" in df.columns:
|
||||
df = df.filter(
|
||||
~pl.col("symbol").str.to_uppercase().is_in(sorted(st_syms))
|
||||
)
|
||||
|
||||
return _aggregate_daily(df, index_pct_map)
|
||||
|
||||
|
||||
|
||||
@@ -159,4 +159,70 @@ class TestMainlineFilterPreferences:
|
||||
path = tmp_path / "preferences.json"
|
||||
monkeypatch.setattr(preferences, "_path", lambda: path)
|
||||
cfg = preferences.get_mainline_filter_config()
|
||||
assert cfg == {"min_members": 4, "max_members": 600, "blacklist": []}
|
||||
assert cfg == {"min_members": 4, "max_members": 600, "blacklist": [], "exclude_st": True}
|
||||
|
||||
def test_sentiment_exclude_st_roundtrip(self, tmp_path, monkeypatch):
|
||||
path = tmp_path / "preferences.json"
|
||||
monkeypatch.setattr(preferences, "_path", lambda: path)
|
||||
assert preferences.get_sentiment_exclude_st() is True # 默认剔除
|
||||
assert preferences.set_sentiment_exclude_st(False) is False
|
||||
assert preferences.get_sentiment_exclude_st() is False
|
||||
# 经主线过滤配置部分更新同样生效
|
||||
got = preferences.set_mainline_filter_config({"exclude_st": True})
|
||||
assert got["exclude_st"] is True
|
||||
|
||||
|
||||
class TestExcludeST:
|
||||
"""风险警示股剔除: 维表名称含 ST → 主线聚合前过滤。"""
|
||||
|
||||
@staticmethod
|
||||
def _write_instruments(tmp_path, names: dict[str, str]) -> None:
|
||||
part = tmp_path / "instruments" / "part.parquet"
|
||||
part.parent.mkdir(parents=True, exist_ok=True)
|
||||
pl.DataFrame({
|
||||
"symbol": list(names),
|
||||
"name": list(names.values()),
|
||||
}).write_parquet(part)
|
||||
|
||||
def _reset_cache(self, monkeypatch):
|
||||
monkeypatch.setattr(market_mainline, "_ST_SYMBOLS_CACHE", None)
|
||||
|
||||
def test_load_risk_warning_symbols(self, tmp_path, monkeypatch):
|
||||
self._reset_cache(monkeypatch)
|
||||
self._write_instruments(tmp_path, {
|
||||
"s1.SH": "*ST环保", "S2.SH": "ST万邦", "S3.SZ": "正常股",
|
||||
"s4.BJ": "S*ST京", "S5.SH": "斯太尔", # 中文名含"斯"不含 ST 标记
|
||||
})
|
||||
got = market_mainline.load_risk_warning_symbols(tmp_path)
|
||||
assert got == frozenset({"S1.SH", "S2.SH", "S4.BJ"}) # 大写归一
|
||||
# 缓存命中: 再次读取不重扫磁盘
|
||||
self._write_instruments(tmp_path, {"S9.SH": "ST新增"})
|
||||
assert market_mainline.load_risk_warning_symbols(tmp_path) == got
|
||||
|
||||
def test_load_risk_warning_symbols_empty_dir(self, tmp_path, monkeypatch):
|
||||
self._reset_cache(monkeypatch)
|
||||
assert market_mainline.load_risk_warning_symbols(tmp_path) == frozenset()
|
||||
|
||||
def test_compute_mainline_excludes_st(self, tmp_path, monkeypatch):
|
||||
"""S1(ST) 涨停被剔除 → 概念 X 计数/高度/龙头随之变化; 关闭开关恢复。"""
|
||||
self._reset_cache(monkeypatch)
|
||||
self._write_instruments(tmp_path, {"S1.SH": "*ST一", "S2.SH": "正常一"})
|
||||
repo, d1, d2 = TestComputeMainline()._setup(tmp_path, monkeypatch)
|
||||
cfg = {"min_members": 4, "max_members": 600, "blacklist": []}
|
||||
|
||||
out = market_mainline.compute_mainline_range(
|
||||
repo, tmp_path, d1, d2, kind="concept", filter_cfg=cfg, exclude_st=True,
|
||||
)
|
||||
x_d1 = out.filter((pl.col("date") == d1) & (pl.col("member") == "X")).to_dicts()[0]
|
||||
assert x_d1["limit_up_count"] == 3 # S1(ST) 被剔除, 剩 S2,S3,S4
|
||||
assert x_d1["ge2_count"] == 1 # 仅 S4=2板
|
||||
assert x_d1["max_boards"] == 2
|
||||
assert x_d1["leader_symbol"] == "S4.SH"
|
||||
|
||||
out_keep = market_mainline.compute_mainline_range(
|
||||
repo, tmp_path, d1, d2, kind="concept", filter_cfg=cfg, exclude_st=False,
|
||||
)
|
||||
x_d1_keep = out_keep.filter((pl.col("date") == d1) & (pl.col("member") == "X")).to_dicts()[0]
|
||||
assert x_d1_keep["limit_up_count"] == 4
|
||||
assert x_d1_keep["ge2_count"] == 2 # S1=2板, S4=2板
|
||||
assert x_d1_keep["leader_symbol"] == "S1.SH"
|
||||
|
||||
@@ -146,6 +146,43 @@ def test_aggregate_empty_returns_empty():
|
||||
assert regime_builder._aggregate_daily(pl.DataFrame()).is_empty()
|
||||
|
||||
|
||||
def test_run_regime_batch_excludes_st(tmp_path, monkeypatch):
|
||||
"""run_regime_batch 默认剔除风险警示股: A(*ST) 的涨停/涨幅不进入统计。
|
||||
|
||||
宽度类指标(涨跌家数)同样只统计非 ST 标的 — 同一 df 统一过滤。
|
||||
关闭开关(preferences)后恢复全市场口径。
|
||||
"""
|
||||
from app.services import market_mainline, preferences
|
||||
|
||||
instruments = tmp_path / "instruments" / "part.parquet"
|
||||
instruments.parent.mkdir(parents=True, exist_ok=True)
|
||||
pl.DataFrame({"symbol": ["A", "B"], "name": ["*ST甲", "正常乙"]}).write_parquet(instruments)
|
||||
monkeypatch.setattr(market_mainline, "_ST_SYMBOLS_CACHE", None)
|
||||
monkeypatch.setattr(preferences, "get_sentiment_exclude_st", lambda: True)
|
||||
monkeypatch.setattr(regime_builder, "_load_index_pct", lambda *a, **k: {})
|
||||
|
||||
class _FakeRepo:
|
||||
class store:
|
||||
data_dir = tmp_path
|
||||
|
||||
def get_enriched_range(self, start, end):
|
||||
return _enriched_df()
|
||||
|
||||
out = regime_builder.run_regime_batch(_FakeRepo(), date(2026, 1, 2), date(2026, 1, 3))
|
||||
r1 = out.filter(pl.col("date") == date(2026, 1, 2)).row(0, named=True)
|
||||
r2 = out.filter(pl.col("date") == date(2026, 1, 3)).row(0, named=True)
|
||||
assert r1["limit_up"] == 0 # A(ST) 涨停被剔除
|
||||
assert r1["up_count"] == 1 # 剩 B/C/D 中仅 C 上涨
|
||||
assert r1["down_count"] == 2
|
||||
assert r2["max_consecutive"] == 1 # A 的 2板不计, C=1板
|
||||
|
||||
monkeypatch.setattr(preferences, "get_sentiment_exclude_st", lambda: False)
|
||||
out_all = regime_builder.run_regime_batch(_FakeRepo(), date(2026, 1, 2), date(2026, 1, 3))
|
||||
r1_all = out_all.filter(pl.col("date") == date(2026, 1, 2)).row(0, named=True)
|
||||
assert r1_all["limit_up"] == 1 # A 计入
|
||||
assert r1_all["up_count"] == 2 # A,C 上涨
|
||||
|
||||
|
||||
# ───────────────────────── 持久化(upsert) ─────────────────────────
|
||||
|
||||
|
||||
|
||||
@@ -582,6 +582,7 @@ export interface MainlineFilter {
|
||||
min_members: number
|
||||
max_members: number
|
||||
blacklist: string[]
|
||||
exclude_st: boolean
|
||||
}
|
||||
|
||||
export interface MainlineResult {
|
||||
@@ -2072,7 +2073,7 @@ export const api = {
|
||||
},
|
||||
regimeMainlineRecompute: () =>
|
||||
request<{ ok: boolean; rows: number }>('/api/regime/mainline/recompute', { method: 'POST' }),
|
||||
mainlineFilterUpdate: (payload: { min_members?: number; max_members?: number; blacklist?: string[] }) =>
|
||||
mainlineFilterUpdate: (payload: { min_members?: number; max_members?: number; blacklist?: string[]; exclude_st?: boolean }) =>
|
||||
request<MainlineFilter>('/api/settings/preferences/mainline-filter', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload),
|
||||
|
||||
@@ -1138,13 +1138,15 @@ export function Regime() {
|
||||
// ── 主线过滤设置面板 ──────────────────────────────────────
|
||||
// 宽基/风格标签(融资融券/沪深股通等数千成分)会霸占主线榜首。默认按成员数
|
||||
// 上限过滤; 用户可调阈值并按名称屏蔽特定概念, 保存后自动重算主线。
|
||||
// ST 剔除开关联动情绪周期口径 — 切换时额外触发 regime 全量重算。
|
||||
function MainlineFilterPanel({ filter, onDone }: {
|
||||
filter: { min_members: number; max_members: number; blacklist: string[] } | undefined
|
||||
filter: { min_members: number; max_members: number; blacklist: string[]; exclude_st?: boolean } | undefined
|
||||
onDone: () => Promise<void>
|
||||
}) {
|
||||
const [minMembers, setMinMembers] = useState(String(filter?.min_members ?? 4))
|
||||
const [maxMembers, setMaxMembers] = useState(String(filter?.max_members ?? 600))
|
||||
const [blacklist, setBlacklist] = useState<string[]>(filter?.blacklist ?? [])
|
||||
const [excludeSt, setExcludeSt] = useState(filter?.exclude_st ?? true)
|
||||
const [input, setInput] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
@@ -1161,9 +1163,17 @@ function MainlineFilterPanel({ filter, onDone }: {
|
||||
min_members: Math.max(1, Number(minMembers) || 4),
|
||||
max_members: Math.max(50, Number(maxMembers) || 600),
|
||||
blacklist,
|
||||
exclude_st: excludeSt,
|
||||
})
|
||||
const stChanged = excludeSt !== (filter?.exclude_st ?? true)
|
||||
if (stChanged) {
|
||||
// 口径切换影响情绪周期驱动指标, 需全量重算 regime+主线(较重, 需等待)
|
||||
await api.regimeRecompute()
|
||||
toast('过滤已保存, 主线与情绪周期已全量重算', 'success')
|
||||
} else {
|
||||
await api.regimeMainlineRecompute()
|
||||
toast('过滤已保存, 主线已重算', 'success')
|
||||
}
|
||||
await onDone()
|
||||
} catch (e) {
|
||||
toast(`保存失败 · ${String((e as Error)?.message || e)}`, 'error')
|
||||
@@ -1213,8 +1223,31 @@ function MainlineFilterPanel({ filter, onDone }: {
|
||||
{saving ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : '保存并重算'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2 border-t border-border/60 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={excludeSt}
|
||||
aria-label="统计剔除 ST 股"
|
||||
onClick={() => setExcludeSt(v => !v)}
|
||||
className={cn(
|
||||
'relative inline-flex h-4.5 w-8 items-center rounded-full border transition-all duration-200',
|
||||
excludeSt ? 'border-accent/50 bg-accent' : 'border-border bg-elevated hover:border-muted',
|
||||
)}
|
||||
>
|
||||
<span className={cn(
|
||||
'inline-block h-3 w-3 rounded-full border border-black/5 bg-white shadow-sm transition-transform duration-200',
|
||||
excludeSt ? 'translate-x-[17px]' : 'translate-x-0.5',
|
||||
)} />
|
||||
</button>
|
||||
<span className="text-[11px] text-secondary">
|
||||
统计剔除 ST 股
|
||||
<span className="ml-1.5 text-[9px] text-muted">主线 + 情绪周期统一口径; 切换后自动全量重算(约 1-2 分钟)</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1.5 text-[9px] text-muted">
|
||||
说明: 成分股数超过上限的概念(如 融资融券~7700家/沪深股通~3300家)视为宽基/风格标签, 不参与主线排名; 修改后自动重算全部历史主线(秒级)。
|
||||
说明: 成分股数超过上限的概念(如 融资融券~7700家/沪深股通~3300家)视为宽基/风格标签, 不参与主线排名;
|
||||
风险警示股(名称含 ST)不参与涨停梯队统计 — 主板 5% 便宜板时代曾系统性霸榜, 且 ST 是状态桶而非题材。修改后自动重算全部历史(秒级~分钟级)。
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1191,7 +1191,14 @@ git clean -fdx
|
||||
- 历史判断应使用对应交易日规则,而不是当前名称直接覆盖历史状态。
|
||||
- 实时信号应优先使用合法涨跌停价与实时价格比较,并处理价格精度。
|
||||
|
||||
### 24.11 飞书或企业微信没有收到通知
|
||||
### 24.11 主线排行或情绪周期里没有 ST 板块
|
||||
|
||||
- 市场环境页的「主线过滤」默认开启"统计剔除 ST 股":名称含 ST 的风险警示股不参与涨停梯队与主线统计,情绪周期驱动指标同口径。
|
||||
- 原因:ST 是跨行业的状态标签而非投资题材,且主板 ST 在 2026-07 前涨跌幅为 5%,封板成本减半,混入会系统性抬高涨停宽度与高度。
|
||||
- 如需恢复全市场口径,在「市场环境 → 情绪周期 → 主线排行 → 过滤」中关闭开关并保存(会自动全量重算,约 1-2 分钟)。
|
||||
- 修改开关或维表名称快照变化后,建议点「重算」使历史与新口径一致。
|
||||
|
||||
### 24.12 飞书或企业微信没有收到通知
|
||||
|
||||
- 测试 Webhook 是否有效。
|
||||
- 检查监控规则是否启用且产生触发记录。
|
||||
@@ -1199,14 +1206,14 @@ git clean -fdx
|
||||
- 检查服务端网络能否访问通知地址。
|
||||
- 查看后端日志中的发送状态。
|
||||
|
||||
### 24.12 前端仍是旧界面
|
||||
### 24.13 前端仍是旧界面
|
||||
|
||||
1. 打开“设置 → 系统设置”。
|
||||
2. 点击“刷新前端缓存”。
|
||||
3. 重新加载页面。
|
||||
4. Docker 部署确认镜像已重新构建。
|
||||
|
||||
### 24.13 删除策略失败或返回 500
|
||||
### 24.14 删除策略失败或返回 500
|
||||
|
||||
- 确认删除的是用户策略,不是内置策略。
|
||||
- 检查 `data/strategies/` 挂载目录是否可写。
|
||||
@@ -1215,11 +1222,11 @@ git clean -fdx
|
||||
|
||||
当前删除逻辑支持从页面删除用户策略文件,不要求用户进入 Docker 容器操作。
|
||||
|
||||
### 24.14 数据删除提示回收站不可用
|
||||
### 24.15 数据删除提示回收站不可用
|
||||
|
||||
Windows 沙箱或容器可能无法使用系统回收站。当前版本会在目标通过安全路径校验后使用受控直接删除。若仍失败,检查数据目录权限和挂载状态,不要手工扩大删除范围。
|
||||
|
||||
### 24.15 查看日志
|
||||
### 24.16 查看日志
|
||||
|
||||
开发模式:查看运行 `dev.sh` 或 `dev.ps1` 的前后端终端。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user