重置项目

This commit is contained in:
2026-07-04 15:59:20 +08:00
parent 374e587f2d
commit 648a8b7f1c
224 changed files with 19700 additions and 9547 deletions
+272
View File
@@ -53,6 +53,29 @@ def get_realtime_quote_interval() -> float:
return load().get("realtime_quote_interval", 10.0)
def get_realtime_watchlist_symbols() -> list[str]:
"""Free 档自选实时监控标的:直接取自选页前 5 个。"""
try:
from app.services import watchlist
rows = watchlist.list_symbols()
except Exception as e: # noqa: BLE001
logger.warning("load watchlist for realtime failed: %s", e)
return []
out: list[str] = []
for row in rows:
symbol = str((row or {}).get("symbol") or "").strip().upper()
if symbol and symbol not in out:
out.append(symbol)
if len(out) >= 5:
break
return out
def set_realtime_watchlist_symbols(symbols: list[str]) -> list[str]: # noqa: ARG001
"""兼容旧接口: Free 实时标的现在由自选页前 5 个决定。"""
return get_realtime_watchlist_symbols()
def set_realtime_quote_interval(interval: float) -> float:
"""保存行情轮询间隔(不在此做 min/max 校验,由调用方按档位限制)。"""
current = load()
@@ -71,6 +94,83 @@ def get_minute_sync_days() -> int:
return max(1, min(30, load().get("minute_sync_days", 5)))
# ===== 数据源选择 (默认 TickFlow;第一阶段仅日K切换入口) =====
_ALLOWED_DATA_PROVIDERS = {"tickflow"}
def get_daily_data_provider() -> str:
provider = str(load().get("daily_data_provider", "tickflow") or "tickflow").lower()
return provider if provider in _ALLOWED_DATA_PROVIDERS else "tickflow"
def get_adj_factor_provider() -> str:
provider = str(load().get("adj_factor_provider", "same_as_daily") or "same_as_daily").lower()
if provider == "same_as_daily":
return provider
return provider if provider in _ALLOWED_DATA_PROVIDERS else "same_as_daily"
def get_minute_data_provider() -> str:
provider = str(load().get("minute_data_provider", "tickflow") or "tickflow").lower()
return provider if provider in _ALLOWED_DATA_PROVIDERS else "tickflow"
def get_realtime_data_provider() -> str:
# 盘中实时现阶段仅支持 TickFlow。
return "tickflow"
# ===== 盘后管道拉取内容开关 (A股 / ETF / 指数 独立控制) =====
def get_pipeline_pull_a_share() -> bool:
"""A 股日K固定拉取。"""
return True
def get_pipeline_pull_etf() -> bool:
"""是否拉取 ETF 日K。默认 False(标的多,首次较慢)。"""
return load().get("pipeline_pull_etf", False)
def get_pipeline_pull_index() -> bool:
"""是否拉取指数日K。默认 True。"""
return load().get("pipeline_pull_index", True)
_PIPELINE_PULL_KEYS = ("pipeline_pull_etf", "pipeline_pull_index")
def get_pipeline_pull_types() -> dict:
"""返回三个拉取开关的当前值。"""
return {
"pipeline_pull_a_share": get_pipeline_pull_a_share(),
"pipeline_pull_etf": get_pipeline_pull_etf(),
"pipeline_pull_index": get_pipeline_pull_index(),
}
def set_pipeline_pull_types(cfg: dict) -> dict:
"""批量保存拉取开关。只接受白名单内的布尔字段。"""
updates = {
k: bool(v) for k, v in cfg.items()
if k in _PIPELINE_PULL_KEYS and v is not None
}
save(updates)
return get_pipeline_pull_types()
def get_pipeline_index_symbols() -> str:
"""指数自定义拉取代码(逗号/换行/空格分隔)。空串表示全量。"""
return str(load().get("pipeline_index_symbols", "") or "").strip()
def set_pipeline_index_symbols(symbols: str) -> str:
"""保存指数自定义代码,返回规范化后的字符串。"""
save({"pipeline_index_symbols": symbols})
return get_pipeline_index_symbols()
def get_pipeline_schedule() -> dict:
"""返回盘后管道调度时间 {"hour": 15, "minute": 30}。"""
d = load().get("pipeline_schedule", {"hour": 15, "minute": 30})
@@ -165,6 +265,73 @@ def set_depth_finalize_time(hour: int, minute: int) -> dict:
return {"hour": h, "minute": m}
# 复盘推送可选渠道白名单 (微信等暂未实现, 不在白名单内, 前端仅作占位)
# 多选: 不推送 = 空数组, 而非 'none'
REVIEW_PUSH_CHANNELS = {"feishu"}
def get_review_schedule() -> dict:
"""定时复盘调度 {"enabled": False, "hour": 15, "minute": 10}。默认关闭。
A股 15:00 收盘, 默认时间设为 15:10(收盘后即时复盘), 强制下限 15:00。
"""
d = load().get("review_schedule", {"enabled": False, "hour": 15, "minute": 10})
return {
"enabled": bool(d.get("enabled", False)),
"hour": d.get("hour", 15),
"minute": d.get("minute", 10),
}
def set_review_schedule(enabled: bool, hour: int, minute: int) -> dict:
"""保存定时复盘调度。强制时间下限 15:00(A股收盘)。
enabled=False 时时间仍保存(下次开启可沿用), 但调度器不会注册 job。
"""
h = max(0, min(23, hour))
m = max(0, min(59, minute))
# 下限 15:00: A股 15:00 收盘, 收盘后才有当日完整数据复盘
if h * 60 + m < 15 * 60:
h, m = 15, 0
save({"review_schedule": {"enabled": bool(enabled), "hour": h, "minute": m}})
return {"enabled": bool(enabled), "hour": h, "minute": m}
def get_review_push_channels() -> list[str]:
"""复盘推送渠道(多选) — 选定的外部工具列表, 复盘归档后逐个推送。
与 review_schedule / 实时行情完全独立, 常驻可单独设置。
空列表 = 不推送; ['feishu'] = 推送到飞书(复用监控中心全局 feishu_webhook_url/secret)。
向后兼容:
- 老多版本单选 review_push_channel=='feishu' → ['feishu']
- 更老布尔 review_push_enabled==True → ['feishu']
"""
d = load()
raw = d.get("review_push_channels")
if isinstance(raw, list):
return [c for c in raw if c in REVIEW_PUSH_CHANNELS]
# 兼容老单选字符串
if d.get("review_push_channel") == "feishu":
return ["feishu"]
# 兼容更老布尔开关
if d.get("review_push_enabled") is True:
return ["feishu"]
return []
def set_review_push_channels(channels: list[str]) -> list[str]:
"""保存复盘推送渠道(多选)。过滤白名单外的值、去重、保序。空列表 = 不推送。"""
seen: set[str] = set()
cleaned: list[str] = []
for c in channels or []:
if c in REVIEW_PUSH_CHANNELS and c not in seen:
seen.add(c)
cleaned.append(c)
save({"review_push_channels": cleaned})
return cleaned
# ===== 实时监控 =====
@@ -178,6 +345,59 @@ SSE_REFRESH_PAGES_DEFAULT = {
SIDEBAR_INDEX_SYMBOLS_DEFAULT = ["000001.SH", "399001.SZ", "399006.SZ", "000680.SH"]
# ===== 盘中实时行情范围 (独立于盘后管道范围) =====
def get_realtime_pull_stock() -> bool:
return load().get("realtime_pull_stock", True)
def get_realtime_pull_etf() -> bool:
# 老用户兼容: ETF 实时默认关闭,避免升级后请求量/写盘量突然增加。
return load().get("realtime_pull_etf", False)
def get_realtime_pull_index() -> bool:
return load().get("realtime_pull_index", True)
def get_realtime_index_mode() -> str:
mode = str(load().get("realtime_index_mode", "core") or "core").lower()
return mode if mode in {"core", "all"} else "core"
def get_realtime_index_symbols() -> list[str]:
stored = load().get("realtime_index_symbols", SIDEBAR_INDEX_SYMBOLS_DEFAULT)
if isinstance(stored, str):
import re
stored = [s.strip() for s in re.split(r"[,\s]+", stored) if s.strip()]
return [str(s) for s in stored if str(s).strip()]
def set_realtime_quote_scope(cfg: dict) -> dict:
updates = {}
for key in ("realtime_pull_stock", "realtime_pull_etf", "realtime_pull_index"):
if key in cfg and cfg[key] is not None:
updates[key] = bool(cfg[key])
if "realtime_index_mode" in cfg and cfg["realtime_index_mode"] in {"core", "all"}:
updates["realtime_index_mode"] = cfg["realtime_index_mode"]
if "realtime_index_symbols" in cfg and cfg["realtime_index_symbols"] is not None:
updates["realtime_index_symbols"] = cfg["realtime_index_symbols"]
if updates:
save(updates)
return get_realtime_quote_scope()
def get_realtime_quote_scope() -> dict:
return {
"realtime_pull_stock": get_realtime_pull_stock(),
"realtime_pull_etf": get_realtime_pull_etf(),
"realtime_pull_index": get_realtime_pull_index(),
"realtime_index_mode": get_realtime_index_mode(),
"realtime_index_symbols": get_realtime_index_symbols(),
}
def get_sse_refresh_pages() -> dict[str, bool]:
"""返回每个页面的 SSE 刷新开关。"""
stored = load().get("sse_refresh_pages", {})
@@ -216,6 +436,43 @@ def set_system_notify_enabled(enabled: bool) -> bool:
return bool(enabled)
def get_feishu_webhook_url() -> str:
"""飞书自定义机器人 Webhook 地址 — 全局共用一处, 所有启用推送的规则都推到这一个群。"""
return load().get("feishu_webhook_url", "")
def get_feishu_webhook_secret() -> str:
"""飞书自定义机器人签名密钥 — 机器人启用「签名校验」时必填, 留空表示不验签。"""
return load().get("feishu_webhook_secret", "")
def set_feishu_webhook_url(url: str) -> str:
"""保存飞书 Webhook 地址。传入空串表示清空配置。"""
save({"feishu_webhook_url": str(url or "").strip()})
return get_feishu_webhook_url()
def set_feishu_webhook_secret(secret: str) -> str:
"""保存飞书签名密钥。传入空串表示不验签。"""
save({"feishu_webhook_secret": str(secret or "").strip()})
return get_feishu_webhook_secret()
def get_webhook_enabled_default() -> bool:
"""新建监控规则时是否默认勾选「飞书推送」。
数据模型当前只有一个 webhook_enabled 布尔 (即飞书), QMT/ptrade 待定。
此默认值供规则编辑器新建规则时预填, 单条规则仍可独立修改。
"""
return load().get("webhook_enabled_default", False)
def set_webhook_enabled_default(enabled: bool) -> bool:
"""保存飞书推送默认勾选态。"""
save({"webhook_enabled_default": bool(enabled)})
return get_webhook_enabled_default()
def get_screener_auto_run() -> bool:
"""选股页进入时是否自动运行所有策略 (获取命中数)。默认开。"""
return load().get("screener_auto_run", True)
@@ -311,3 +568,18 @@ def set_onboarding_completed(done: bool = True) -> bool:
"""标记首次使用向导完成状态。"""
save({"onboarding_completed": bool(done)})
return bool(done)
# ===== 财务数据同步时间(持久化,重启不丢失) =====
# 结构: { "metrics": "2026-06-25T10:00:00+08:00", "income": ..., ... }
def get_financial_sync_times() -> dict[str, str]:
"""返回各财务表的最后同步时间(ISO 字符串)。未同步过的表不在返回值中。"""
return load().get("financial_sync_times", {}) or {}
def set_financial_sync_time(table: str, iso_ts: str) -> None:
"""更新单张财务表的最后同步时间(合并写入,不清除其他表)。"""
times = get_financial_sync_times()
times[table] = iso_ts
save({"financial_sync_times": times})