mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
- 策略: 引擎只加载 data/strategies/{custom,ai,composite} 与研究模板目录,
原 25 个内置策略一次性迁移为用户自定义文件(ID 不变, 引用零迁移);
研究模板移至 app/strategy/research, 打包 spec 同步; 新装环境策略库为空
- 信号: 管线不再硬编码任何信号表达式; 涨跌停判定价成为运行时列
limit_up_price/limit_down_price(生效价∓0.005 容差), 20 个信号转为
data/user_data/custom_signals 定义(显式 column 沿用 signal_* 原列名);
依赖解析/注入顺序/当日路径 prev-join 全部定义驱动; 语义统一为历史路径
口径(顺带修正当日路径 BOLL 比较符与翘板/炸板空值两处既有偏差)
- 前端: 信号库单列表(全部可编辑), SignalPicker/监控选项/信号标签全走
定义接口; 回测页新增免责声明(模拟性质/数据源准确性不作保证/不构成
投资建议)
- 测试: 25 个策略夹具与 20 个信号种子入库 tests/fixtures, 受影响测试
全部自包含化(不依赖运行目录用户数据); 77 万行 MA 死叉新旧表达式对拍
逐行一致; 回测实跑 45 笔卖点信号出场验证矩阵链路
- 同批包含工作区既有进度: 策略结果 ext 列同日投影修复(含测试)、数据源插件、fuyao/auth/api_gateway/data_integrity 调整、README 信号库
描述与 strategy.md 导入路径补充
验证: 后端 2592 passed(空数据目录 CI 模拟 2588 passed, 4 例为模拟环境
伪影复验排除); 前端 vitest 125 passed + tsc 0 错 + vite build 通过
80 lines
3.4 KiB
Python
80 lines
3.4 KiB
Python
"""ETF 监控轮与股票轮/指数轮同口径: 快照日期不是当日时不评估。
|
|
|
|
_evaluate_monitors 的节假日与陈旧数据兜底靠「快照日期 = 当日」新鲜度判据
|
|
(股票轮 stock_ready、指数轮 index_date == cn_today()), ETF 轮只看缓存是否为空。
|
|
但 ETF 缓存并不只由 ETF 实时落盘焐热: 自选页/ETF 策略/ETF K 线等以 refresh=True
|
|
调 get_enriched_latest_asset("etf") 会把磁盘上最新一日 (上一交易日) 读进缓存。
|
|
ETF 实时拉取默认关闭 (realtime_pull_etf=False), 工作日休市时实时快照也不落盘,
|
|
这两种情况下 ETF 规则每轮都拿上一交易日的数据当作今天评估并推送告警。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
import polars as pl
|
|
|
|
from app.services.quote_service import QuoteService
|
|
from app.strategy.monitor import MonitorRuleEngine
|
|
|
|
TODAY = date(2026, 9, 18)
|
|
YESTERDAY = date(2026, 9, 17)
|
|
|
|
|
|
class _Repo:
|
|
def __init__(self, etf_date: date) -> None:
|
|
self.etf_date = etf_date
|
|
self.store = SimpleNamespace(data_dir=None)
|
|
|
|
def get_name_map(self, symbols=None):
|
|
return {"510300.SH": "沪深300ETF"}
|
|
|
|
def get_enriched_latest_asset(self, asset_type, refresh=True):
|
|
if asset_type != "etf":
|
|
return pl.DataFrame(), None
|
|
return (
|
|
pl.DataFrame({"symbol": ["510300.SH"], "date": [self.etf_date],
|
|
"close": [4.2], "change_pct": [0.012]}),
|
|
self.etf_date,
|
|
)
|
|
|
|
|
|
def _run(etf_date: date) -> list[dict]:
|
|
engine = MonitorRuleEngine()
|
|
engine.set_rules([{
|
|
"id": "etf_px", "name": "ETF 价格提醒", "type": "price", "asset_type": "etf",
|
|
"scope": "symbols", "symbols": ["510300.SH"], "logic": "and",
|
|
"conditions": [{"field": "close", "op": ">=", "value": 4.0}],
|
|
"cooldown_seconds": 0, "enabled": True,
|
|
}])
|
|
svc = QuoteService.__new__(QuoteService)
|
|
svc._repo = _Repo(etf_date)
|
|
svc._app_state = SimpleNamespace(monitor_engine=engine, repo=svc._repo)
|
|
broadcast: list[dict] = []
|
|
with (
|
|
patch.object(QuoteService, "_is_continuous_trading", return_value=True),
|
|
patch.object(QuoteService, "get_enriched_today", return_value=(pl.DataFrame(), None)),
|
|
patch.object(QuoteService, "_inject_intraday_signals", side_effect=lambda df, e, at: df),
|
|
patch.object(QuoteService, "_enrich_alerts_ext", lambda self, alerts: None),
|
|
patch.object(QuoteService, "_broadcast_alerts", lambda self, alerts: broadcast.extend(alerts)),
|
|
patch.object(QuoteService, "_maybe_send_system_notifications", lambda self, alerts: None),
|
|
patch.object(QuoteService, "_maybe_send_webhook", lambda self, events, engine: None),
|
|
patch("app.services.alert_store.append_many", lambda *a, **k: None),
|
|
patch("app.services.quote_service.cn_today", return_value=TODAY),
|
|
):
|
|
svc._evaluate_monitors(pl.DataFrame(), None)
|
|
return broadcast
|
|
|
|
|
|
def test_stale_etf_snapshot_is_not_evaluated():
|
|
"""ETF 缓存是上一交易日 (无当日实时落盘): 不评估, 不推送。"""
|
|
assert _run(YESTERDAY) == []
|
|
|
|
|
|
def test_today_etf_snapshot_still_triggers():
|
|
"""ETF 缓存为当日实时快照: 照常评估 (回归确认守卫不误伤)。"""
|
|
alerts = _run(TODAY)
|
|
assert [a["rule_id"] for a in alerts] == ["etf_px"]
|
|
assert alerts[0]["symbol"] == "510300.SH"
|