重置项目
This commit is contained in:
@@ -75,38 +75,18 @@ class AIStrategyGenerator:
|
||||
return {"code": code, "meta": meta, "valid": True, "error": None}
|
||||
|
||||
async def _call_llm(self, user_prompt: str, guide: str) -> str:
|
||||
"""调用 OpenAI 兼容 API(流式,避免 CDN 长连接超时)"""
|
||||
from openai import AsyncOpenAI
|
||||
from app import secrets_store
|
||||
"""Call the configured AI provider and return generated strategy code."""
|
||||
from app.services.ai_provider import generate_ai_text
|
||||
|
||||
ai_key = secrets_store.get_ai_key()
|
||||
if not ai_key:
|
||||
raise RuntimeError("AI API Key 未配置,请在设置页面配置")
|
||||
|
||||
client = AsyncOpenAI(
|
||||
api_key=ai_key,
|
||||
base_url=secrets_store.get_ai_config("ai_base_url", "https://api.alysc.top"),
|
||||
timeout=180.0,
|
||||
max_retries=2,
|
||||
)
|
||||
# 使用流式请求:CDN 收到首个 token 后会持续转发,不会因等待超时
|
||||
stream = await client.chat.completions.create(
|
||||
model=secrets_store.get_ai_config("ai_model", "gpt-5.5"),
|
||||
messages=[
|
||||
content = await generate_ai_text(
|
||||
[
|
||||
{"role": "system", "content": _SYSTEM_PREFIX + guide},
|
||||
{"role": "user", "content": user_prompt},
|
||||
],
|
||||
temperature=0.3,
|
||||
max_tokens=3000,
|
||||
stream=True,
|
||||
)
|
||||
chunks: list[str] = []
|
||||
async for chunk in stream:
|
||||
delta = chunk.choices[0].delta if chunk.choices else None
|
||||
if delta and delta.content:
|
||||
chunks.append(delta.content)
|
||||
content = "".join(chunks).strip()
|
||||
# 提取代码块
|
||||
# Extract fenced code if the model wrapped the answer in Markdown.
|
||||
if "```python" in content:
|
||||
content = content.split("```python", 1)[1].split("```", 1)[0].strip()
|
||||
elif "```" in content:
|
||||
|
||||
@@ -24,6 +24,44 @@ from app.strategy import config as _strategy_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 信号 / 字段中文名映射 — 与前端 lib/signals.ts 对齐, 用于告警 message / 推送文案。
|
||||
# signal_* 为内置原子信号, 其余为技术指标/行情字段。
|
||||
_SIGNAL_CN: dict[str, str] = {
|
||||
# 内置信号
|
||||
"signal_ma_golden_5_20": "MA5上穿MA20", "signal_ma_dead_5_20": "MA5下穿MA20",
|
||||
"signal_ma_golden_20_60": "MA20上穿MA60", "signal_macd_golden": "MACD金叉",
|
||||
"signal_macd_dead": "MACD死叉", "signal_ma20_breakout": "突破MA20",
|
||||
"signal_ma20_breakdown": "跌破MA20", "signal_n_day_high": "60日新高",
|
||||
"signal_n_day_low": "60日新低", "signal_boll_breakout_upper": "突破布林上轨",
|
||||
"signal_boll_breakdown_lower": "跌破布林下轨", "signal_volume_surge": "放量",
|
||||
"signal_limit_up": "涨停", "signal_limit_down": "跌停",
|
||||
"signal_limit_down_recovery": "跌停翘板", "signal_broken_limit_up": "炸板",
|
||||
# 行情字段
|
||||
"close": "收盘价", "open": "开盘价", "high": "最高价", "low": "最低价",
|
||||
"change_pct": "涨跌幅", "change_amount": "涨跌额", "amplitude": "振幅",
|
||||
"turnover_rate": "换手率", "volume": "成交量", "amount": "成交额",
|
||||
# 均线
|
||||
"ma5": "MA5", "ma10": "MA10", "ma20": "MA20", "ma30": "MA30", "ma60": "MA60",
|
||||
"ema5": "EMA5", "ema10": "EMA10", "ema20": "EMA20",
|
||||
# MACD / BOLL / KDJ / RSI
|
||||
"macd_dif": "MACD-DIF", "macd_dea": "MACD-DEA", "macd_hist": "MACD柱",
|
||||
"boll_upper": "布林上轨", "boll_lower": "布林下轨",
|
||||
"kdj_k": "KDJ-K", "kdj_d": "KDJ-D", "kdj_j": "KDJ-J",
|
||||
"rsi_6": "RSI6", "rsi_14": "RSI14", "rsi_24": "RSI24",
|
||||
# 量能 / 动量 / 波动
|
||||
"vol_ratio_5d": "5日量比", "vol_ratio_20d": "20日量比",
|
||||
"vol_ma5": "5日均量", "vol_ma10": "10日均量",
|
||||
"high_60d": "60日最高", "low_60d": "60日最低",
|
||||
"momentum_5d": "5日动量", "momentum_20d": "20日动量", "momentum_60d": "60日动量",
|
||||
"atr_14": "ATR14", "annual_vol_20d": "20日年化波动",
|
||||
"consecutive_limit_ups": "连板数", "consecutive_limit_downs": "跌停连板",
|
||||
}
|
||||
|
||||
|
||||
def _signal_cn_name(name: str) -> str:
|
||||
"""返回信号/字段的中文名, 找不到原样返回 (与前端 cnSignal 对齐)。"""
|
||||
return _SIGNAL_CN.get(name, name)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StrategyAlert:
|
||||
@@ -276,6 +314,13 @@ class MonitorRuleEngine:
|
||||
self._strategy_pools: dict[str, set[str]] = {}
|
||||
# 数据目录 (用于加载策略 overrides)
|
||||
self._data_dir = None
|
||||
# 历史窗口加载器: (target_date, lookback_days) → 多日 enriched DataFrame。
|
||||
# 用于声明 filter_history 的策略 (如反包), 实时监控时拼历史窗口 + 今日行情跑选股。
|
||||
# 为 None 时, filter_history 策略仍会被跳过 (保持旧行为, 不破坏无历史场景)。
|
||||
self._history_loader: Callable[[_dt.date, int], "pl.DataFrame"] | None = None
|
||||
# 本轮 evaluate() 产出的策略选股结果: strategy_id → {rows, total, as_of}
|
||||
# 供策略页实时回显复用 (/api/screener/cached 端点直接读取此内存结果), 避免重跑
|
||||
self._latest_strategy_results: dict[str, dict] = {}
|
||||
|
||||
def set_strategy_engine(self, engine) -> None:
|
||||
"""注入 StrategyEngine, type=strategy 规则据此跑选股。"""
|
||||
@@ -285,6 +330,15 @@ class MonitorRuleEngine:
|
||||
"""注入数据目录, 用于加载策略的用户覆盖配置。"""
|
||||
self._data_dir = data_dir
|
||||
|
||||
def set_history_loader(self, fn) -> None:
|
||||
"""注入历史窗口加载器, 用于声明 filter_history 的策略跑实时监控。
|
||||
|
||||
loader 签名: (target_date, lookback_days) → 多日 enriched DataFrame。
|
||||
复用 ScreenerService._load_enriched_history (三级缓存, 命中 ~0ms)。
|
||||
为 None 时 filter_history 策略退回到跳过逻辑 (不破坏无历史场景)。
|
||||
"""
|
||||
self._history_loader = fn
|
||||
|
||||
def set_name_map(self, name_map: dict[str, str]) -> None:
|
||||
"""注入 symbol → 股票名 映射, 用于在告警事件里回填 name 字段。
|
||||
|
||||
@@ -325,6 +379,23 @@ class MonitorRuleEngine:
|
||||
def rule_count(self) -> int:
|
||||
return len(self._rules)
|
||||
|
||||
def latest_strategy_results(self) -> dict[str, dict]:
|
||||
"""返回本轮 evaluate() 产出的策略选股结果 (strategy_id → {rows, total, as_of})。
|
||||
|
||||
供策略页实时回显复用: /api/screener/cached 端点直接读取此内存结果,
|
||||
避免对被监控的策略重跑第二遍。无 type=strategy 规则时返回空 dict。
|
||||
"""
|
||||
return self._latest_strategy_results
|
||||
|
||||
def has_rule_type(self, rtype: str) -> bool:
|
||||
"""是否存在指定类型的 (已启用) 规则。供 quote_service 判断是否需要注入特殊数据。"""
|
||||
if not self._rules:
|
||||
return False
|
||||
return any(
|
||||
r.get("enabled", True) and r.get("type") == rtype
|
||||
for r in self._rules.values()
|
||||
)
|
||||
|
||||
# ── 评估 ───────────────────────────────────────────
|
||||
def evaluate(self, df: pl.DataFrame) -> list[dict]:
|
||||
"""行情更新后评估所有规则。
|
||||
@@ -339,6 +410,8 @@ class MonitorRuleEngine:
|
||||
|
||||
now = time.time()
|
||||
events: list[dict] = []
|
||||
# 每轮重置: 只保留本次 evaluate 产出的策略结果
|
||||
self._latest_strategy_results = {}
|
||||
|
||||
for rule_id, rule in self._rules.items():
|
||||
try:
|
||||
@@ -363,6 +436,9 @@ class MonitorRuleEngine:
|
||||
if rtype == "strategy":
|
||||
# 策略类型: 跑策略选股 → 对比上期选股池 → 产出 new_entry/dropped 事件
|
||||
hit_rows = self._match_strategy(scoped, rule)
|
||||
elif rtype == "ladder":
|
||||
# 连板梯队封单监控: 独立处理 (需带预警封单值, 走专属 message)
|
||||
return self._evaluate_ladder(scoped, rule, now)
|
||||
else:
|
||||
# signal / price / market: 通用条件匹配
|
||||
for sym, name, price, pct, hit_sigs in self._match_conditions(scoped, rule):
|
||||
@@ -395,7 +471,11 @@ class MonitorRuleEngine:
|
||||
message = name # name 字段即批量消息
|
||||
else:
|
||||
resolved_name = name if name else self._name_map.get(sym)
|
||||
message = rule.get("message", "") or self._default_message(rule, ev_type=ev_type, sym=sym, name=resolved_name, pct=pct)
|
||||
message = rule.get("message", "") or self._default_message(
|
||||
rule, ev_type=ev_type, sym=sym, name=resolved_name,
|
||||
pct=pct, price=price,
|
||||
conditions=list(rule.get("conditions", [])) if rule.get("type") != "strategy" else None,
|
||||
)
|
||||
|
||||
ev = {
|
||||
"ts": int(now * 1000),
|
||||
@@ -410,6 +490,10 @@ class MonitorRuleEngine:
|
||||
"change_pct": pct,
|
||||
"signals": hit_sigs,
|
||||
"severity": severity,
|
||||
# 触发条件快照 (signal/price/market 类型): 用于触发记录展示
|
||||
# 「命中了什么条件」。strategy 类型靠策略选股池 diff, 不写条件。
|
||||
"conditions": list(rule.get("conditions", [])) if rtype != "strategy" else [],
|
||||
"logic": rule.get("logic", "and") if rtype != "strategy" else "and",
|
||||
}
|
||||
events.append(ev)
|
||||
if self._alert_handler:
|
||||
@@ -458,11 +542,6 @@ class MonitorRuleEngine:
|
||||
if s is None:
|
||||
return []
|
||||
|
||||
# 需要历史数据的策略跳过 (实时监控不支持 history loader)
|
||||
if s.filter_history_fn:
|
||||
logger.debug("策略 %s 需要历史数据, 跳过实时监控", sid)
|
||||
return []
|
||||
|
||||
# 运行策略选股: 复用当前 enriched DataFrame 跳过数据加载
|
||||
overrides = {}
|
||||
if self._data_dir:
|
||||
@@ -471,17 +550,63 @@ class MonitorRuleEngine:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 声明 filter_history 的策略 (如反包) 需要多日历史窗口才能判定形态。
|
||||
# 旧实现因"实时监控不支持 history loader"直接跳过 → 反包等策略盘中永不触发。
|
||||
# 现接入 history_loader, 拼历史窗口 + 今日实时行情, 经 precomputed_history 喂给引擎。
|
||||
# loader 为 None (未装配) 时退回跳过, 保持旧行为, 不破坏无历史场景。
|
||||
run_kwargs: dict = {
|
||||
"as_of": _dt.date.today(),
|
||||
"overrides": overrides,
|
||||
}
|
||||
if s.filter_history_fn:
|
||||
if self._history_loader is None:
|
||||
logger.debug("策略 %s 需要历史数据但未注入 history_loader, 跳过实时监控", sid)
|
||||
return []
|
||||
try:
|
||||
today = _dt.date.today()
|
||||
lookback = max(1, getattr(s, "lookback_days", 30))
|
||||
hist_df = self._history_loader(today, lookback)
|
||||
if hist_df is None or hist_df.is_empty():
|
||||
logger.debug("策略 %s 历史数据为空, 跳过本轮实时监控", sid)
|
||||
return []
|
||||
# 历史窗口可能与今日已落盘数据重叠: 排掉 hist_df 中 date==today 的行,
|
||||
# 今日行情始终以实时 df 为准 (盘中逐轮更新, 最接近收盘真相)。
|
||||
# 否则 today 行重复会污染 filter_history 的 .over("symbol") 窗口判定。
|
||||
if "date" in hist_df.columns:
|
||||
hist_df = hist_df.filter(pl.col("date") != today)
|
||||
# 拼接历史窗口 + 今日实时行情 (filter_history 用 .over("symbol") 窗口, 多日天然可用)
|
||||
run_kwargs["precomputed_history"] = pl.concat(
|
||||
[hist_df, df], how="diagonal_relaxed"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("策略 %s 加载历史窗口失败, 跳过: %s", sid, e)
|
||||
return []
|
||||
else:
|
||||
# 普通策略: 复用当前 enriched DataFrame 跳过数据加载
|
||||
run_kwargs["precomputed"] = df
|
||||
|
||||
try:
|
||||
result = self._strategy_engine.run(
|
||||
sid,
|
||||
as_of=_dt.date.today(),
|
||||
precomputed=df,
|
||||
overrides=overrides,
|
||||
)
|
||||
result = self._strategy_engine.run(sid, **run_kwargs)
|
||||
except Exception as e:
|
||||
logger.warning("策略 %s 选股执行失败: %s", sid, e)
|
||||
return []
|
||||
|
||||
# 记录本轮完整选股结果 (供策略页实时回显: /cached 端点直接读取, 不落盘)。
|
||||
# 与下面的 diff 事件无关 — 无论是否产生 new_entry/dropped, 结果都该可用于回显。
|
||||
try:
|
||||
import math
|
||||
self._latest_strategy_results[sid] = {
|
||||
"total": result.total,
|
||||
"as_of": str(_dt.date.today()),
|
||||
"rows": [
|
||||
{k: (None if isinstance(v, float) and not math.isfinite(v) else v)
|
||||
for k, v in row.items()}
|
||||
for row in result.rows
|
||||
],
|
||||
}
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
current_pool: set[str] = {r["symbol"] for r in result.rows}
|
||||
prev_pool = self._strategy_pools.get(sid)
|
||||
|
||||
@@ -577,9 +702,93 @@ class MonitorRuleEngine:
|
||||
results.append((sym, name, price, pct, hit_sigs))
|
||||
return results
|
||||
|
||||
def _evaluate_ladder(self, scoped: pl.DataFrame, rule: dict, now: float) -> list[dict]:
|
||||
"""评估连板梯队封单监控规则。
|
||||
|
||||
封单量从注入的临时列 _sealed_vol (手) 读取 (由 quote_service 评估前注入)。
|
||||
命中条件: 封单比较值 <= threshold (且封单 > 0, 排除无 depth 数据的股票)。
|
||||
涨停(direction=up) → 炸板预警; 跌停(direction=down) → 翘板预警。
|
||||
"""
|
||||
if "_sealed_vol" not in scoped.columns:
|
||||
return [] # 无封单数据 (depth 未拉取), 安全降级
|
||||
|
||||
metric = rule.get("metric", "sealed_vol")
|
||||
threshold = rule.get("threshold", 0)
|
||||
direction = rule.get("direction", "up")
|
||||
cooldown = rule.get("cooldown_seconds", 600)
|
||||
severity = rule.get("severity", "warn")
|
||||
|
||||
# 比较值: sealed_vol 直接用 (手), sealed_amount = 手 × 100股 × close
|
||||
if metric == "sealed_amount":
|
||||
cmp_expr = pl.col("_sealed_vol") * 100 * pl.col("close")
|
||||
unit = "元"
|
||||
else:
|
||||
cmp_expr = pl.col("_sealed_vol")
|
||||
unit = "手"
|
||||
|
||||
# 命中: 封单 > 0 (有数据) 且 比较值 <= 阈值
|
||||
hit = scoped.filter(
|
||||
pl.col("_sealed_vol").is_not_null()
|
||||
& (pl.col("_sealed_vol") > 0)
|
||||
& (cmp_expr <= threshold)
|
||||
)
|
||||
if hit.is_empty():
|
||||
return []
|
||||
|
||||
warn_label = "炸板预警" if direction == "up" else "翘板预警"
|
||||
events: list[dict] = []
|
||||
for row in hit.iter_rows(named=True):
|
||||
sym = row.get("symbol", "")
|
||||
key = (rule["id"], sym)
|
||||
last = self._last_fire.get(key)
|
||||
if last is not None and (now - last) < cooldown:
|
||||
continue
|
||||
self._last_fire[key] = now
|
||||
|
||||
name = row.get("name") or self._name_map.get(sym) or sym
|
||||
price = row.get("close")
|
||||
pct = row.get("change_pct")
|
||||
sealed_vol = row.get("_sealed_vol")
|
||||
# 预警封单值 (展示用)
|
||||
sealed_value = sealed_vol * 100 * (price or 0) if metric == "sealed_amount" else sealed_vol
|
||||
|
||||
# message 体现预警封单量 + 阈值
|
||||
if metric == "sealed_amount":
|
||||
sv_text = f"{sealed_value / 1e4:.0f}万{unit}"
|
||||
th_text = f"{threshold / 1e4:.0f}万{unit}"
|
||||
else:
|
||||
sv_text = f"{sealed_value:,.0f} {unit}"
|
||||
th_text = f"{threshold:,.0f} {unit}"
|
||||
message = f"{warn_label} · 封单 {sv_text} ≤ {th_text}"
|
||||
|
||||
events.append({
|
||||
"ts": int(now * 1000),
|
||||
"rule_id": rule["id"],
|
||||
"rule_name": rule.get("name", ""),
|
||||
"source": "ladder",
|
||||
"type": warn_label,
|
||||
"symbol": sym,
|
||||
"name": name,
|
||||
"message": message,
|
||||
"price": price,
|
||||
"change_pct": pct,
|
||||
"signals": [],
|
||||
"severity": severity,
|
||||
"conditions": [],
|
||||
"logic": "and",
|
||||
"sealed_value": sealed_value, # 预警封单量/额 (飞书+记录展示)
|
||||
"sealed_metric": metric,
|
||||
})
|
||||
return events
|
||||
|
||||
def _default_message(self, rule: dict, ev_type: str = "", sym: str = "",
|
||||
name: str = "", pct: Any = None) -> str:
|
||||
"""生成默认 message。策略类型按变更方向生成。"""
|
||||
name: str = "", pct: Any = None, price: Any = None,
|
||||
conditions: list[dict] | None = None) -> str:
|
||||
"""生成默认 message。
|
||||
|
||||
- strategy: 按变更方向生成 (进入/移出 + 涨跌幅)
|
||||
- signal/price/market: 条件摘要 + 现价 + 涨跌幅 (避免笼统的「信号触发」)
|
||||
"""
|
||||
rtype = rule.get("type", "signal")
|
||||
if rtype == "strategy":
|
||||
# 从 StrategyEngine 取策略名; 失败则退化为 rule_name 里截取的部分
|
||||
@@ -609,5 +818,40 @@ class MonitorRuleEngine:
|
||||
return f"策略「{sname}」移出 {name}{pct_text}"
|
||||
return f"策略「{sname}」变更"
|
||||
|
||||
name_map = {"signal": "信号触发", "price": "价格触发", "market": "市场异动"}
|
||||
return name_map.get(rtype, "监控触发")
|
||||
# signal / price / market: 条件摘要 + 现价 + 涨跌幅
|
||||
# 条件摘要: 把 conditions (truth/比较) 拼成可读串, 如 "MA20金叉 且 量比>2"
|
||||
cond_text = self._format_conditions_text(rule, conditions)
|
||||
price_text = f"现价 {price}" if price is not None else ""
|
||||
pct_text = ""
|
||||
if pct is not None:
|
||||
sign = "+" if pct >= 0 else ""
|
||||
pct_text = f"{sign}{pct * 100:.1f}%"
|
||||
tail = " · ".join(s for s in (price_text, pct_text) if s)
|
||||
if cond_text and tail:
|
||||
return f"{cond_text} · {tail}"
|
||||
return cond_text or tail or "监控触发"
|
||||
|
||||
@staticmethod
|
||||
def _format_conditions_text(rule: dict, conditions: list[dict] | None) -> str:
|
||||
"""把 rule.conditions 拼成可读文本 (用于 message / 推送)。
|
||||
|
||||
op=truth: 直接用信号中文名 (如 "MA20金叉")
|
||||
op=比较: 字段中文名 + 操作符 + 值 (如 "涨跌幅≥5")
|
||||
logic: and → "且", or → "或"
|
||||
"""
|
||||
conds = conditions if conditions is not None else list(rule.get("conditions", []))
|
||||
if not conds:
|
||||
return ""
|
||||
logic_word = "且" if rule.get("logic", "and") == "and" else "或"
|
||||
parts: list[str] = []
|
||||
for c in conds:
|
||||
field = c.get("field", "")
|
||||
op = c.get("op", "truth")
|
||||
value = c.get("value")
|
||||
label = _signal_cn_name(field) or field
|
||||
if op == "truth":
|
||||
parts.append(label)
|
||||
else:
|
||||
op_map = {"gte": "≥", "lte": "≤", "gt": ">", "lt": "<", "eq": "="}
|
||||
parts.append(f"{label}{op_map.get(op, op)}{value}")
|
||||
return f" {logic_word} ".join(parts)
|
||||
|
||||
@@ -26,12 +26,16 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# ── 常量 ────────────────────────────────────────────────
|
||||
ID_RE = re.compile(r"^[a-z0-9_]{1,40}$")
|
||||
RULE_TYPES = {"strategy", "signal", "price", "market"}
|
||||
RULE_TYPES = {"strategy", "signal", "price", "market", "ladder"}
|
||||
SCOPES = {"symbols", "all", "sector"}
|
||||
LOGICS = {"and", "or"}
|
||||
DIRECTIONS = {"entry", "exit", "both"}
|
||||
SEVERITIES = {"info", "warn", "critical"}
|
||||
OPS = {">", ">=", "<", "<=", "==", "!="}
|
||||
# ladder 规则: 封单监控的指标 (量=手, 额=元)
|
||||
LADDER_METRICS = {"sealed_vol", "sealed_amount"}
|
||||
# ladder 规则: 方向 (up=涨停炸板预警, down=跌停翘板预警)
|
||||
LADDER_DIRECTIONS = {"up", "down"}
|
||||
|
||||
# 布尔信号列前缀 (op=truth 时 field 取这些)
|
||||
_SIGNAL_PREFIXES = ("signal_", "csg_")
|
||||
@@ -107,6 +111,15 @@ def validate(rule: dict) -> None:
|
||||
raise ValueError("策略类型规则必须指定 strategy_id")
|
||||
if rule.get("direction", "entry") not in DIRECTIONS:
|
||||
raise ValueError(f"direction 必须是 {DIRECTIONS} 之一")
|
||||
elif rule.get("type") == "ladder":
|
||||
# 连板梯队封单监控: 需 metric + threshold + direction(up/down), 不用 conditions
|
||||
if rule.get("metric", "sealed_vol") not in LADDER_METRICS:
|
||||
raise ValueError(f"metric 必须是 {LADDER_METRICS} 之一")
|
||||
if rule.get("direction", "up") not in LADDER_DIRECTIONS:
|
||||
raise ValueError(f"direction 必须是 {LADDER_DIRECTIONS} 之一 (up=涨停炸板, down=跌停翘板)")
|
||||
thr = rule.get("threshold")
|
||||
if not isinstance(thr, (int, float)) or thr < 0:
|
||||
raise ValueError("threshold 必须是非负数字 (封单 ≤ 此值时报警)")
|
||||
else:
|
||||
# 信号/价格/市场类型: 需要 conditions
|
||||
conds = rule.get("conditions")
|
||||
@@ -158,8 +171,12 @@ def normalize(rule: dict) -> dict:
|
||||
r.setdefault("symbols", [])
|
||||
r.setdefault("sector", None)
|
||||
r.setdefault("strategy_id", None)
|
||||
r.setdefault("direction", "entry")
|
||||
# direction 默认值: ladder 用 "up", 其余用 "entry"
|
||||
r.setdefault("direction", "up" if r.get("type") == "ladder" else "entry")
|
||||
r.setdefault("conditions", [])
|
||||
# ladder 专属默认字段
|
||||
r.setdefault("metric", "sealed_vol")
|
||||
r.setdefault("threshold", 0)
|
||||
r.setdefault("logic", "and")
|
||||
r.setdefault("cooldown_seconds", 3600)
|
||||
r.setdefault("severity", "info")
|
||||
|
||||
Reference in New Issue
Block a user