feat: Add weather alert engine, Polymarket data reader, and Telegram notification utility.
This commit is contained in:
@@ -7,7 +7,9 @@ from __future__ import annotations
|
|||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
from src.analysis.settlement_rounding import wu_round
|
||||||
|
|
||||||
|
|
||||||
def _sf(v: Any) -> Optional[float]:
|
def _sf(v: Any) -> Optional[float]:
|
||||||
@@ -471,6 +473,130 @@ def _bucket_label(bucket: Any) -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _to_celsius(temp: Optional[float], temp_symbol: str) -> Optional[float]:
|
||||||
|
if temp is None:
|
||||||
|
return None
|
||||||
|
if "F" in (temp_symbol or "").upper():
|
||||||
|
return (temp - 32.0) * 5.0 / 9.0
|
||||||
|
return temp
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_open_meteo_today_high_c(city_weather: Dict[str, Any]) -> Optional[float]:
|
||||||
|
forecast = city_weather.get("forecast") or {}
|
||||||
|
om_today = _sf(forecast.get("today_high"))
|
||||||
|
|
||||||
|
if om_today is None:
|
||||||
|
om = city_weather.get("open-meteo") or {}
|
||||||
|
daily = om.get("daily") or {}
|
||||||
|
series = daily.get("temperature_2m_max") or []
|
||||||
|
if isinstance(series, list) and series:
|
||||||
|
om_today = _sf(series[0])
|
||||||
|
|
||||||
|
if om_today is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
temp_symbol = str(city_weather.get("temp_symbol") or "")
|
||||||
|
return _to_celsius(om_today, temp_symbol)
|
||||||
|
|
||||||
|
|
||||||
|
def _bucket_value(row: Dict[str, Any]) -> Optional[float]:
|
||||||
|
for key in ("value", "temp"):
|
||||||
|
value = _sf(row.get(key))
|
||||||
|
if value is not None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
label = str(row.get("label") or "").strip()
|
||||||
|
m = re.search(r"(-?\d+(?:\.\d+)?)", label)
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
return _sf(m.group(1))
|
||||||
|
|
||||||
|
|
||||||
|
def _bucket_bounds(row: Dict[str, Any]) -> Optional[Tuple[Optional[float], Optional[float]]]:
|
||||||
|
value = _bucket_value(row)
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
label = str(row.get("label") or "").lower()
|
||||||
|
is_upper_tail = any(key in label for key in ("+", "or higher", "or above", "and above"))
|
||||||
|
is_lower_tail = any(key in label for key in ("<=", "or lower", "or below", "and below"))
|
||||||
|
|
||||||
|
if is_upper_tail and not is_lower_tail:
|
||||||
|
return value, None
|
||||||
|
if is_lower_tail and not is_upper_tail:
|
||||||
|
return None, value
|
||||||
|
return value, value
|
||||||
|
|
||||||
|
|
||||||
|
def _distance_to_bucket(target: float, bounds: Tuple[Optional[float], Optional[float]]) -> float:
|
||||||
|
lower, upper = bounds
|
||||||
|
if lower is not None and target < lower:
|
||||||
|
return lower - target
|
||||||
|
if upper is not None and target > upper:
|
||||||
|
return target - upper
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _pick_bucket_for_forecast(
|
||||||
|
rows: List[Dict[str, Any]],
|
||||||
|
forecast_settlement: Optional[int],
|
||||||
|
forecast_today_high_c: Optional[float],
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
if not rows:
|
||||||
|
return None
|
||||||
|
|
||||||
|
target = (
|
||||||
|
float(forecast_settlement)
|
||||||
|
if forecast_settlement is not None
|
||||||
|
else forecast_today_high_c
|
||||||
|
)
|
||||||
|
if target is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
best_row: Optional[Dict[str, Any]] = None
|
||||||
|
best_distance: Optional[float] = None
|
||||||
|
best_probability = -1.0
|
||||||
|
best_rank = 10**9
|
||||||
|
|
||||||
|
for idx, row in enumerate(rows):
|
||||||
|
bounds = _bucket_bounds(row)
|
||||||
|
if not bounds:
|
||||||
|
continue
|
||||||
|
|
||||||
|
distance = _distance_to_bucket(target, bounds)
|
||||||
|
probability = _norm_probability(row.get("probability"))
|
||||||
|
probability_rank = probability if probability is not None else -1.0
|
||||||
|
|
||||||
|
if best_row is None:
|
||||||
|
best_row = row
|
||||||
|
best_distance = distance
|
||||||
|
best_probability = probability_rank
|
||||||
|
best_rank = idx
|
||||||
|
continue
|
||||||
|
|
||||||
|
assert best_distance is not None
|
||||||
|
if distance < best_distance:
|
||||||
|
best_row = row
|
||||||
|
best_distance = distance
|
||||||
|
best_probability = probability_rank
|
||||||
|
best_rank = idx
|
||||||
|
continue
|
||||||
|
|
||||||
|
if abs(distance - best_distance) <= 1e-9:
|
||||||
|
if probability_rank > best_probability:
|
||||||
|
best_row = row
|
||||||
|
best_distance = distance
|
||||||
|
best_probability = probability_rank
|
||||||
|
best_rank = idx
|
||||||
|
elif abs(probability_rank - best_probability) <= 1e-9 and idx < best_rank:
|
||||||
|
best_row = row
|
||||||
|
best_distance = distance
|
||||||
|
best_probability = probability_rank
|
||||||
|
best_rank = idx
|
||||||
|
|
||||||
|
return best_row
|
||||||
|
|
||||||
|
|
||||||
def _extract_market_snapshot(city_weather: Dict[str, Any]) -> Dict[str, Any]:
|
def _extract_market_snapshot(city_weather: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
scan = city_weather.get("market_scan") or {}
|
scan = city_weather.get("market_scan") or {}
|
||||||
if not isinstance(scan, dict):
|
if not isinstance(scan, dict):
|
||||||
@@ -491,10 +617,14 @@ def _extract_market_snapshot(city_weather: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
|
|
||||||
top_bucket = None
|
top_bucket = None
|
||||||
top_bucket_rows: List[Dict[str, Any]] = []
|
top_bucket_rows: List[Dict[str, Any]] = []
|
||||||
top_buckets = scan.get("top_buckets") or []
|
all_bucket_rows: List[Dict[str, Any]] = []
|
||||||
if isinstance(top_buckets, list):
|
source_buckets = scan.get("all_buckets")
|
||||||
|
if not isinstance(source_buckets, list) or not source_buckets:
|
||||||
|
source_buckets = scan.get("top_buckets") or []
|
||||||
|
|
||||||
|
if isinstance(source_buckets, list):
|
||||||
normalized = []
|
normalized = []
|
||||||
for row in top_buckets:
|
for row in source_buckets:
|
||||||
if not isinstance(row, dict):
|
if not isinstance(row, dict):
|
||||||
continue
|
continue
|
||||||
p = _norm_probability(row.get("probability"))
|
p = _norm_probability(row.get("probability"))
|
||||||
@@ -504,15 +634,21 @@ def _extract_market_snapshot(city_weather: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
if normalized:
|
if normalized:
|
||||||
normalized.sort(key=lambda x: x[0], reverse=True)
|
normalized.sort(key=lambda x: x[0], reverse=True)
|
||||||
top_bucket = normalized[0][1]
|
top_bucket = normalized[0][1]
|
||||||
for p, row in normalized[:4]:
|
for p, row in normalized:
|
||||||
top_bucket_rows.append(
|
row_slug = str(row.get("slug") or "").strip()
|
||||||
|
row_market_url = f"https://polymarket.com/market/{row_slug}" if row_slug else None
|
||||||
|
all_bucket_rows.append(
|
||||||
{
|
{
|
||||||
"label": _bucket_label(row),
|
"label": _bucket_label(row),
|
||||||
"probability": p,
|
"probability": p,
|
||||||
"yes_buy": _norm_probability(row.get("yes_buy")),
|
"yes_buy": _norm_probability(row.get("yes_buy")),
|
||||||
"yes_sell": _norm_probability(row.get("yes_sell")),
|
"yes_sell": _norm_probability(row.get("yes_sell")),
|
||||||
|
"value": _sf(row.get("value") or row.get("temp")),
|
||||||
|
"slug": row_slug or None,
|
||||||
|
"market_url": row_market_url,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
top_bucket_rows = all_bucket_rows[:4]
|
||||||
|
|
||||||
market_url = None
|
market_url = None
|
||||||
websocket = scan.get("websocket") or {}
|
websocket = scan.get("websocket") or {}
|
||||||
@@ -525,6 +661,17 @@ def _extract_market_snapshot(city_weather: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
if slug:
|
if slug:
|
||||||
market_url = f"https://polymarket.com/market/{slug}"
|
market_url = f"https://polymarket.com/market/{slug}"
|
||||||
|
|
||||||
|
open_meteo_today_high_c = _extract_open_meteo_today_high_c(city_weather)
|
||||||
|
open_meteo_settlement = wu_round(open_meteo_today_high_c)
|
||||||
|
forecast_bucket = _pick_bucket_for_forecast(
|
||||||
|
rows=all_bucket_rows,
|
||||||
|
forecast_settlement=open_meteo_settlement,
|
||||||
|
forecast_today_high_c=open_meteo_today_high_c,
|
||||||
|
)
|
||||||
|
forecast_market_url = None
|
||||||
|
if isinstance(forecast_bucket, dict):
|
||||||
|
forecast_market_url = str(forecast_bucket.get("market_url") or "").strip() or None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"available": True,
|
"available": True,
|
||||||
"selected_bucket": _bucket_label(scan.get("temperature_bucket")),
|
"selected_bucket": _bucket_label(scan.get("temperature_bucket")),
|
||||||
@@ -541,7 +688,12 @@ def _extract_market_snapshot(city_weather: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
"signal_label": scan.get("signal_label"),
|
"signal_label": scan.get("signal_label"),
|
||||||
"confidence": scan.get("confidence"),
|
"confidence": scan.get("confidence"),
|
||||||
"top_bucket_rows": top_bucket_rows,
|
"top_bucket_rows": top_bucket_rows,
|
||||||
"market_url": market_url,
|
"all_bucket_rows": all_bucket_rows,
|
||||||
|
"open_meteo_today_high_c": open_meteo_today_high_c,
|
||||||
|
"open_meteo_settlement": open_meteo_settlement,
|
||||||
|
"forecast_bucket": forecast_bucket,
|
||||||
|
"primary_market_url": market_url,
|
||||||
|
"market_url": forecast_market_url or market_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -786,6 +938,81 @@ def _build_telegram_messages(
|
|||||||
return {"zh": "\n".join(lines_zh), "en": "\n".join(lines_en)}
|
return {"zh": "\n".join(lines_zh), "en": "\n".join(lines_en)}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_telegram_messages_mispricing(
|
||||||
|
city_weather: Dict[str, Any],
|
||||||
|
rules: Dict[str, Dict[str, Any]],
|
||||||
|
market_snapshot: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
temp_symbol = str(city_weather.get("temp_symbol") or "°C")
|
||||||
|
city_name = city_weather.get("display_name") or city_weather.get("name", "").title()
|
||||||
|
current = city_weather.get("current") or {}
|
||||||
|
current_temp = _sf(current.get("temp"))
|
||||||
|
if current_temp is None:
|
||||||
|
return {"zh": "", "en": ""}
|
||||||
|
|
||||||
|
snapshot = market_snapshot or _extract_market_snapshot(city_weather)
|
||||||
|
momentum = rules.get("momentum_spike", {})
|
||||||
|
local_time = str(city_weather.get("local_time") or "").strip()
|
||||||
|
obs_time = str(current.get("obs_time") or "").strip()
|
||||||
|
|
||||||
|
delta_temp = _sf(momentum.get("delta_temp"))
|
||||||
|
delta_min = momentum.get("delta_minutes")
|
||||||
|
momentum_emoji = "➡️"
|
||||||
|
if delta_temp is not None:
|
||||||
|
momentum_emoji = "🚀" if delta_temp > 0 else ("📉" if delta_temp < 0 else "➡️")
|
||||||
|
|
||||||
|
dynamic_text = f"实测 {current_temp:.1f}{temp_symbol}"
|
||||||
|
if delta_temp is not None and delta_min is not None:
|
||||||
|
dynamic_text = (
|
||||||
|
f"实测 {current_temp:.1f}{temp_symbol} "
|
||||||
|
f"({int(delta_min)}min 内 {delta_temp:+.1f}{temp_symbol}) {momentum_emoji}"
|
||||||
|
)
|
||||||
|
|
||||||
|
om_high_c = _sf(snapshot.get("open_meteo_today_high_c"))
|
||||||
|
om_settle = snapshot.get("open_meteo_settlement")
|
||||||
|
forecast_bucket = snapshot.get("forecast_bucket") or {}
|
||||||
|
match_bucket_label = str(forecast_bucket.get("label") or "--").strip() or "--"
|
||||||
|
match_bucket_yes = _fmt_cents(forecast_bucket.get("yes_buy"))
|
||||||
|
market_url = str(
|
||||||
|
snapshot.get("market_url")
|
||||||
|
or snapshot.get("primary_market_url")
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
lines_zh = [f"🚨 PolyWeather 错价雷达 [{city_name}]"]
|
||||||
|
lines_zh.append("")
|
||||||
|
if om_high_c is not None and om_settle is not None:
|
||||||
|
lines_zh.append(
|
||||||
|
f"基准:Open-Meteo 今日高温 {om_high_c:.1f}C(结算参考 {om_settle}C)"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lines_zh.append("基准:Open-Meteo 今日高温 --(结算参考 --)")
|
||||||
|
lines_zh.append(f"命中桶:{match_bucket_label} | Yes: {match_bucket_yes}")
|
||||||
|
lines_zh.append("触发:该桶 Yes 价格 < 10c,疑似低估")
|
||||||
|
lines_zh.append("")
|
||||||
|
lines_zh.append(f"动态:{dynamic_text}")
|
||||||
|
if local_time or obs_time:
|
||||||
|
if local_time and obs_time:
|
||||||
|
lines_zh.append(f"时间:当地 {local_time} | 观测 {obs_time}")
|
||||||
|
elif local_time:
|
||||||
|
lines_zh.append(f"时间:当地 {local_time}")
|
||||||
|
else:
|
||||||
|
lines_zh.append(f"时间:观测 {obs_time}")
|
||||||
|
lines_zh.append("")
|
||||||
|
if market_url:
|
||||||
|
lines_zh.append(f"市场链接:{market_url}")
|
||||||
|
|
||||||
|
lines_en = [
|
||||||
|
f"🚨 PolyWeather Mispricing Radar [{city_name}]",
|
||||||
|
"",
|
||||||
|
f"Now: {dynamic_text}",
|
||||||
|
]
|
||||||
|
if market_url:
|
||||||
|
lines_en.append(f"Market link: {market_url}")
|
||||||
|
|
||||||
|
return {"zh": "\n".join(lines_zh), "en": "\n".join(lines_en)}
|
||||||
|
|
||||||
|
|
||||||
def build_trading_alerts(
|
def build_trading_alerts(
|
||||||
city_weather: Dict[str, Any],
|
city_weather: Dict[str, Any],
|
||||||
map_url: Optional[str] = None,
|
map_url: Optional[str] = None,
|
||||||
@@ -833,12 +1060,10 @@ def build_trading_alerts(
|
|||||||
if force_push and severity == "none":
|
if force_push and severity == "none":
|
||||||
severity = "medium"
|
severity = "medium"
|
||||||
|
|
||||||
telegram = _build_telegram_messages(
|
telegram = _build_telegram_messages_mispricing(
|
||||||
city_weather=city_weather,
|
city_weather=city_weather,
|
||||||
rules=rules,
|
rules=rules,
|
||||||
map_url=map_url,
|
|
||||||
market_snapshot=market_snapshot,
|
market_snapshot=market_snapshot,
|
||||||
suppression=suppression,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -486,12 +486,21 @@ class PolymarketReadOnlyLayer:
|
|||||||
|
|
||||||
signal_label, confidence = self._derive_signal(edge_percent, liquidity)
|
signal_label, confidence = self._derive_signal(edge_percent, liquidity)
|
||||||
|
|
||||||
top_buckets = self._build_top_temperature_buckets(
|
top_bucket_limit = max(
|
||||||
|
1,
|
||||||
|
_safe_int(os.getenv("POLYMARKET_TOP_BUCKET_LIMIT", "4"), 4),
|
||||||
|
)
|
||||||
|
all_bucket_limit = max(
|
||||||
|
top_bucket_limit,
|
||||||
|
_safe_int(os.getenv("POLYMARKET_ALL_BUCKET_LIMIT", "24"), 24),
|
||||||
|
)
|
||||||
|
all_buckets = self._build_top_temperature_buckets(
|
||||||
city_key=city_key,
|
city_key=city_key,
|
||||||
target_date=date_str,
|
target_date=date_str,
|
||||||
primary_market=market,
|
primary_market=market,
|
||||||
limit=4,
|
limit=all_bucket_limit,
|
||||||
)
|
)
|
||||||
|
top_buckets = list(all_buckets[:top_bucket_limit])
|
||||||
|
|
||||||
yes_payload = {
|
yes_payload = {
|
||||||
"outcome": yes_token.get("outcome") or "Yes",
|
"outcome": yes_token.get("outcome") or "Yes",
|
||||||
@@ -560,6 +569,7 @@ class PolymarketReadOnlyLayer:
|
|||||||
"volume": volume,
|
"volume": volume,
|
||||||
"sparkline": sparkline_values,
|
"sparkline": sparkline_values,
|
||||||
"top_buckets": top_buckets,
|
"top_buckets": top_buckets,
|
||||||
|
"all_buckets": all_buckets,
|
||||||
"websocket": {
|
"websocket": {
|
||||||
"market_url": market_url,
|
"market_url": market_url,
|
||||||
"asset_ids": [
|
"asset_ids": [
|
||||||
|
|||||||
@@ -36,6 +36,28 @@ def _env_int(name: str, default: int) -> int:
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _env_float(name: str, default: float) -> float:
|
||||||
|
raw = os.getenv(name)
|
||||||
|
if raw is None:
|
||||||
|
return default
|
||||||
|
try:
|
||||||
|
return float(raw)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_prob(v: Any) -> Optional[float]:
|
||||||
|
if v is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
n = float(v)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
if n > 1.0:
|
||||||
|
n = n / 100.0
|
||||||
|
return max(0.0, min(1.0, n))
|
||||||
|
|
||||||
|
|
||||||
def _parse_city_list(raw: Optional[str]) -> List[str]:
|
def _parse_city_list(raw: Optional[str]) -> List[str]:
|
||||||
if not raw:
|
if not raw:
|
||||||
return list(CITY_REGISTRY.keys())
|
return list(CITY_REGISTRY.keys())
|
||||||
@@ -111,6 +133,58 @@ def _severity_ok(alert_payload: Dict[str, Any], min_severity: str, min_trigger_c
|
|||||||
return SEVERITY_RANK.get(severity, 0) >= SEVERITY_RANK.get(min_severity, 0)
|
return SEVERITY_RANK.get(severity, 0) >= SEVERITY_RANK.get(min_severity, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def _market_price_cap_ok(alert_payload: Dict[str, Any], max_yes_buy: float) -> bool:
|
||||||
|
if max_yes_buy >= 1.0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
market = alert_payload.get("market_snapshot") or {}
|
||||||
|
if not isinstance(market, dict) or not market.get("available"):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Prefer the market bucket that maps to Open-Meteo forecast settlement.
|
||||||
|
forecast_bucket = market.get("forecast_bucket") or {}
|
||||||
|
yes_buy = None
|
||||||
|
bucket_label = None
|
||||||
|
if isinstance(forecast_bucket, dict):
|
||||||
|
yes_buy = _norm_prob(forecast_bucket.get("yes_buy"))
|
||||||
|
bucket_label = str(forecast_bucket.get("label") or "").strip() or None
|
||||||
|
|
||||||
|
# Backward-compatible fallback.
|
||||||
|
if yes_buy is None:
|
||||||
|
yes_buy = _norm_prob(market.get("yes_buy"))
|
||||||
|
if not bucket_label:
|
||||||
|
bucket_label = str(market.get("selected_bucket") or "").strip() or None
|
||||||
|
|
||||||
|
if yes_buy is None:
|
||||||
|
# Fallback to first bucket with valid yes_buy if aggregate field is missing.
|
||||||
|
top_rows = market.get("top_bucket_rows") or []
|
||||||
|
if isinstance(top_rows, list):
|
||||||
|
for row in top_rows:
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
continue
|
||||||
|
yes_buy = _norm_prob(row.get("yes_buy"))
|
||||||
|
if yes_buy is not None:
|
||||||
|
if not bucket_label:
|
||||||
|
bucket_label = str(row.get("label") or "").strip() or None
|
||||||
|
break
|
||||||
|
|
||||||
|
if yes_buy is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if yes_buy >= max_yes_buy:
|
||||||
|
logger.info(
|
||||||
|
"trade alert skipped by mispricing cap city={} bucket={} om_settle={} yes_buy={} cap={}".format(
|
||||||
|
alert_payload.get("city"),
|
||||||
|
bucket_label or "--",
|
||||||
|
market.get("open_meteo_settlement"),
|
||||||
|
round(yes_buy, 4),
|
||||||
|
round(max_yes_buy, 4),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _trigger_type_key(alert_payload: Dict[str, Any]) -> str:
|
def _trigger_type_key(alert_payload: Dict[str, Any]) -> str:
|
||||||
trigger_types = sorted(
|
trigger_types = sorted(
|
||||||
str(alert.get("type") or "").strip()
|
str(alert.get("type") or "").strip()
|
||||||
@@ -218,6 +292,12 @@ def _maybe_send_alert(
|
|||||||
last_by_city = state.setdefault("last_by_city", {})
|
last_by_city = state.setdefault("last_by_city", {})
|
||||||
last_city = last_by_city.get(city) or {}
|
last_city = last_by_city.get(city) or {}
|
||||||
is_active = _severity_ok(alert_payload, min_severity, min_trigger_count)
|
is_active = _severity_ok(alert_payload, min_severity, min_trigger_count)
|
||||||
|
max_yes_buy = max(
|
||||||
|
0.0,
|
||||||
|
min(1.0, _env_float("TELEGRAM_ALERT_MISPRICING_MAX_YES_BUY", 0.10)),
|
||||||
|
)
|
||||||
|
if not _market_price_cap_ok(alert_payload, max_yes_buy):
|
||||||
|
is_active = False
|
||||||
message = ((alert_payload.get("telegram") or {}).get("zh") or "").strip()
|
message = ((alert_payload.get("telegram") or {}).get("zh") or "").strip()
|
||||||
|
|
||||||
if not is_active or not message:
|
if not is_active or not message:
|
||||||
|
|||||||
Reference in New Issue
Block a user