mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 17:54:15 +08:00
feat(v0.2): 市场阶段与主线识别 + 因子挖掘全链路 + 数据层完善
- 市场环境: 新增情绪周期6阶段(冰点/启动/主升/高潮/退潮/修复, 连板梯队驱动, EMA平滑+2日确认+弱档否决, 平均段长9.7天)与概念/行业主线排名(涨停梯队聚合, 可配置宽基/风格标签过滤); 市场环境页重构, regime 透明加列, 与5档state并存 - 挖掘: 因子与策略挖掘全链路(API/worker/进程锁/候选库/前端工作台/文档), 周度调度默认关闭且永不自动发布 - 回测: 财务快照因子(点时口径), 批量回测预计算共享下期收益, 信号路径矩阵列依赖展开修复(consecutive_limit_ups 缺列报错) - 数据/性能: enriched 生成与预热治理, 重任务限流, 行情/K线缓存复用, 时区修复 - 测试: 后端全量 914 通过; GUI 黑盒验证截图存证 gui-test-screenshots/
This commit is contained in:
@@ -5,12 +5,19 @@
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 进程内缓存: 行情轮询线程一轮会调用 8~12 次 getter, 每次读盘+parse 是纯重复;
|
||||
# 文件仅在用户改设置时变化, 以 (mtime_ns, size) 签名判断是否重读。
|
||||
_cache: dict | None = None
|
||||
_cache_sig: tuple[int, int] | None = None
|
||||
|
||||
|
||||
def _path() -> Path:
|
||||
from app.config import settings
|
||||
@@ -19,14 +26,32 @@ def _path() -> Path:
|
||||
return p
|
||||
|
||||
|
||||
def _invalidate_cache() -> None:
|
||||
global _cache, _cache_sig
|
||||
_cache = None
|
||||
_cache_sig = None
|
||||
|
||||
|
||||
def load() -> dict:
|
||||
"""读取 preferences.json (带 mtime 签名缓存)。返回深拷贝, 调用方可自由修改。"""
|
||||
global _cache, _cache_sig
|
||||
p = _path()
|
||||
if p.exists():
|
||||
try:
|
||||
return json.loads(p.read_text(encoding="utf-8"))
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("preferences.json malformed: %s", e)
|
||||
return {}
|
||||
try:
|
||||
sig = (p.stat().st_mtime_ns, p.stat().st_size)
|
||||
except OSError:
|
||||
return {}
|
||||
if _cache is not None and sig == _cache_sig:
|
||||
return copy.deepcopy(_cache)
|
||||
try:
|
||||
data = json.loads(p.read_text(encoding="utf-8"))
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.warning("preferences.json malformed: %s", e)
|
||||
return {}
|
||||
_cache = data
|
||||
_cache_sig = sig
|
||||
return copy.deepcopy(_cache)
|
||||
|
||||
|
||||
def save(updates: dict) -> dict:
|
||||
@@ -36,6 +61,7 @@ def save(updates: dict) -> dict:
|
||||
_path().write_text(
|
||||
json.dumps(current, indent=2, ensure_ascii=False), encoding="utf-8",
|
||||
)
|
||||
_invalidate_cache()
|
||||
return current
|
||||
|
||||
|
||||
@@ -88,6 +114,7 @@ def set_realtime_quote_interval(interval: float) -> float:
|
||||
_path().write_text(
|
||||
json.dumps(current, indent=2, ensure_ascii=False), encoding="utf-8",
|
||||
)
|
||||
_invalidate_cache()
|
||||
return interval
|
||||
|
||||
|
||||
@@ -316,6 +343,74 @@ def get_regime_warmup_days() -> int:
|
||||
return 40
|
||||
|
||||
|
||||
# ── 市场主线(概念/行业涨停梯队)过滤 ──
|
||||
# 宽基/风格标签(融资融券 ~7700 成分、深股通/沪股通 ~3300-3700、国企改革 ~2900)
|
||||
# 会按"家数"霸占主线榜首, 但它们不是可操作的题材主线。默认按成分股数上限过滤。
|
||||
# 标定(2026-08 THS 概念): 成员 >600 的 55 个概念几乎全是此类风格标签,
|
||||
# 真实题材(华为概念 2006/人工智能 2166/固态电池等)均在 600 以下或可自行调整。
|
||||
_MAINLINE_MAX_MEMBERS_MIN = 50
|
||||
_MAINLINE_MAX_MEMBERS_MAX = 5000
|
||||
_MAINLINE_MIN_MEMBERS_MIN = 1
|
||||
_MAINLINE_MIN_MEMBERS_MAX = 200
|
||||
|
||||
|
||||
def get_mainline_max_members() -> int:
|
||||
"""主线维度成员数上限, 超过视为宽基/风格标签被过滤。默认 600。"""
|
||||
v = load().get("mainline_max_members", 600)
|
||||
try:
|
||||
return max(_MAINLINE_MAX_MEMBERS_MIN, min(_MAINLINE_MAX_MEMBERS_MAX, int(v)))
|
||||
except (TypeError, ValueError):
|
||||
return 600
|
||||
|
||||
|
||||
def get_mainline_min_members() -> int:
|
||||
"""主线维度成员数下限, 过滤微型标签。默认 4。"""
|
||||
v = load().get("mainline_min_members", 4)
|
||||
try:
|
||||
return max(_MAINLINE_MIN_MEMBERS_MIN, min(_MAINLINE_MIN_MEMBERS_MAX, int(v)))
|
||||
except (TypeError, ValueError):
|
||||
return 4
|
||||
|
||||
|
||||
def get_mainline_blacklist() -> list[str]:
|
||||
"""用户自定义屏蔽的维度成员名(不论成员数大小)。默认空。
|
||||
|
||||
保存时接受 list 或逗号/顿号/分号/空白分隔的字符串。
|
||||
"""
|
||||
v = load().get("mainline_blacklist", [])
|
||||
if isinstance(v, str):
|
||||
v = [part for part in re.split(r"[,,、;;\s]+", v) if part] # noqa: RUF001
|
||||
if not isinstance(v, list):
|
||||
return []
|
||||
return [str(x).strip() for x in v if str(x).strip()]
|
||||
|
||||
|
||||
def get_mainline_filter_config() -> dict:
|
||||
"""主线过滤配置汇总(供 API 返回与计算读取)。"""
|
||||
return {
|
||||
"min_members": get_mainline_min_members(),
|
||||
"max_members": get_mainline_max_members(),
|
||||
"blacklist": get_mainline_blacklist(),
|
||||
}
|
||||
|
||||
|
||||
def set_mainline_filter_config(cfg: dict) -> dict:
|
||||
"""保存主线过滤配置(白名单字段, 部分更新)。修改后需重算主线生效。"""
|
||||
updates: dict = {}
|
||||
if "min_members" in cfg and cfg["min_members"] is not None:
|
||||
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 "blacklist" in cfg and cfg["blacklist"] is not None:
|
||||
raw = cfg["blacklist"]
|
||||
if isinstance(raw, str):
|
||||
raw = [part for part in re.split(r"[,,、;;\s]+", raw) if part] # noqa: RUF001
|
||||
updates["mainline_blacklist"] = [str(x).strip() for x in (raw or []) if str(x).strip()]
|
||||
if updates:
|
||||
save(updates)
|
||||
return get_mainline_filter_config()
|
||||
|
||||
|
||||
_PIPELINE_PULL_KEYS = ("pipeline_pull_etf", "pipeline_pull_index")
|
||||
|
||||
|
||||
@@ -475,6 +570,43 @@ def set_review_schedule(enabled: bool, hour: int, minute: int) -> dict:
|
||||
return {"enabled": bool(enabled), "hour": h, "minute": m}
|
||||
|
||||
|
||||
MINING_BUDGET_PROFILES = frozenset({"balanced", "strict"})
|
||||
|
||||
|
||||
def get_mining_schedule() -> dict:
|
||||
"""返回周度自动 mining 配置。历史配置缺字段时默认关闭。"""
|
||||
data = load()
|
||||
weekday = data.get("mining_schedule_weekday", 4)
|
||||
if isinstance(weekday, bool) or not isinstance(weekday, int) or not 0 <= weekday <= 4:
|
||||
weekday = 4
|
||||
profile = data.get("mining_budget_profile", "balanced")
|
||||
if not isinstance(profile, str) or profile not in MINING_BUDGET_PROFILES:
|
||||
profile = "balanced"
|
||||
enabled = data.get("mining_schedule_enabled", False)
|
||||
if not isinstance(enabled, bool):
|
||||
enabled = False
|
||||
return {
|
||||
"mining_schedule_enabled": enabled,
|
||||
"mining_schedule_weekday": weekday,
|
||||
"mining_budget_profile": profile,
|
||||
}
|
||||
|
||||
|
||||
def set_mining_schedule(enabled: bool, weekday: int, profile: str) -> dict:
|
||||
"""校验并一次写入周度自动 mining 的整组配置。"""
|
||||
if isinstance(weekday, bool) or not isinstance(weekday, int) or not 0 <= weekday <= 4:
|
||||
raise ValueError("mining schedule weekday must be between 0 and 4")
|
||||
if profile not in MINING_BUDGET_PROFILES:
|
||||
raise ValueError("mining budget profile must be balanced or strict")
|
||||
result = {
|
||||
"mining_schedule_enabled": bool(enabled),
|
||||
"mining_schedule_weekday": weekday,
|
||||
"mining_budget_profile": profile,
|
||||
}
|
||||
save(result)
|
||||
return result
|
||||
|
||||
|
||||
def get_review_push_channels() -> list[str]:
|
||||
"""复盘推送渠道(多选) — 选定的外部工具列表, 复盘归档后逐个推送。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user