清理旧决策卡片、市场总览、跑道面板等已废弃组件
This commit is contained in:
@@ -1,102 +0,0 @@
|
||||
"""Anomaly detection — pure math, no AI call.
|
||||
|
||||
Flags cities where current observations deviate from model predictions.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from web.scan_city_ai_helpers import _safe_float
|
||||
|
||||
|
||||
def _check_city_anomaly(
|
||||
data: Dict[str, Any],
|
||||
*,
|
||||
high_temp_threshold: float = 2.0,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Return anomaly flag if current observation breaks model cluster bounds."""
|
||||
current = data.get("current") if isinstance(data.get("current"), dict) else {}
|
||||
airport = data.get("airport_current") if isinstance(data.get("airport_current"), dict) else {}
|
||||
multi = data.get("multi_model") if isinstance(data.get("multi_model"), dict) else {}
|
||||
deb = data.get("deb") if isinstance(data.get("deb"), dict) else {}
|
||||
|
||||
observed = _safe_float(current.get("temp") or airport.get("temp"))
|
||||
if observed is None:
|
||||
return None
|
||||
|
||||
model_highs = [
|
||||
_safe_float(v)
|
||||
for v in multi.values()
|
||||
if _safe_float(v) is not None
|
||||
]
|
||||
deb_pred = _safe_float(deb.get("prediction"))
|
||||
if deb_pred is not None:
|
||||
model_highs.append(deb_pred)
|
||||
|
||||
if not model_highs:
|
||||
return None
|
||||
|
||||
model_max = max(model_highs)
|
||||
model_min = min(model_highs)
|
||||
model_median = sorted(model_highs)[len(model_highs) // 2]
|
||||
|
||||
delta_above_max = observed - model_max
|
||||
delta_below_min = model_min - observed
|
||||
delta_from_median = observed - model_median
|
||||
|
||||
anomaly: Optional[Dict[str, Any]] = None
|
||||
|
||||
if delta_above_max > high_temp_threshold:
|
||||
anomaly = {
|
||||
"level": "breakout_above",
|
||||
"observed": observed,
|
||||
"model_max": model_max,
|
||||
"delta": round(delta_above_max, 1),
|
||||
"model_count": len(model_highs),
|
||||
}
|
||||
elif delta_below_min > high_temp_threshold:
|
||||
anomaly = {
|
||||
"level": "breakout_below",
|
||||
"observed": observed,
|
||||
"model_min": model_min,
|
||||
"delta": round(delta_below_min, 1),
|
||||
"model_count": len(model_highs),
|
||||
}
|
||||
elif abs(delta_from_median) > 1.5:
|
||||
anomaly = {
|
||||
"level": "deviation",
|
||||
"observed": observed,
|
||||
"model_median": model_median,
|
||||
"delta": round(delta_from_median, 1),
|
||||
"model_count": len(model_highs),
|
||||
}
|
||||
|
||||
if anomaly:
|
||||
anomaly.update(
|
||||
{
|
||||
"city": data.get("name") or data.get("city"),
|
||||
"local_date": data.get("local_date"),
|
||||
"temp_unit": data.get("temp_symbol", "°C"),
|
||||
"deb_prediction": deb_pred,
|
||||
}
|
||||
)
|
||||
return anomaly
|
||||
|
||||
|
||||
def detect_scan_terminal_anomalies(
|
||||
rows: List[Dict[str, Any]],
|
||||
*,
|
||||
high_temp_threshold: float = 2.0,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Scan all terminal rows and return anomaly flags."""
|
||||
anomalies = []
|
||||
for row in rows:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
city_data = row.get("city_data") or row
|
||||
flag = _check_city_anomaly(city_data, high_temp_threshold=high_temp_threshold)
|
||||
if flag:
|
||||
flag["row_id"] = row.get("row_id") or row.get("id")
|
||||
anomalies.append(flag)
|
||||
return anomalies
|
||||
@@ -1,169 +0,0 @@
|
||||
"""Deterministic market overview for scan terminal rows, cached 10 minutes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from src.utils.refresh_policy import MARKET_OVERVIEW_TTL_SEC
|
||||
|
||||
_OVERVIEW_CACHE: Dict[str, Dict[str, Any]] = {}
|
||||
_OVERVIEW_CACHE_LOCK = threading.Lock()
|
||||
OVERVIEW_CACHE_TTL_SEC = MARKET_OVERVIEW_TTL_SEC
|
||||
|
||||
|
||||
def _safe_float(value: Any) -> Optional[float]:
|
||||
try:
|
||||
if value is None or value == "":
|
||||
return None
|
||||
number = float(value)
|
||||
except Exception:
|
||||
return None
|
||||
return number if number == number else None
|
||||
|
||||
|
||||
def _row_city(row: Dict[str, Any]) -> str:
|
||||
return str(row.get("display_name") or row.get("city") or row.get("name") or "").strip()
|
||||
|
||||
|
||||
def _row_edge(row: Dict[str, Any]) -> Optional[float]:
|
||||
return (
|
||||
_safe_float(row.get("edge_percent"))
|
||||
or _safe_float(row.get("edge_pct"))
|
||||
or _safe_float(row.get("edge"))
|
||||
)
|
||||
|
||||
|
||||
def _row_score(row: Dict[str, Any]) -> float:
|
||||
edge = _row_edge(row) or 0.0
|
||||
final_score = _safe_float(row.get("final_score")) or 0.0
|
||||
liquidity = _safe_float(row.get("liquidity")) or _safe_float(row.get("liquidity_num")) or 0.0
|
||||
return edge * 10.0 + final_score + min(liquidity / 1000.0, 25.0)
|
||||
|
||||
|
||||
def _row_liquidity(row: Dict[str, Any]) -> float:
|
||||
return _safe_float(row.get("liquidity")) or _safe_float(row.get("liquidity_num")) or 0.0
|
||||
|
||||
|
||||
def _row_prob_gap(row: Dict[str, Any]) -> Optional[float]:
|
||||
model_prob = _safe_float(row.get("model_probability"))
|
||||
market_prob = _safe_float(row.get("market_probability"))
|
||||
if model_prob is None or market_prob is None:
|
||||
return None
|
||||
return model_prob - market_prob
|
||||
|
||||
|
||||
def _cache_key(rows: List[Dict[str, Any]], locale: str) -> str:
|
||||
finger = {
|
||||
"locale": locale,
|
||||
"rows": [
|
||||
{
|
||||
"city": row.get("city") or row.get("name") or "",
|
||||
"edge": _row_edge(row),
|
||||
"score": _safe_float(row.get("final_score")),
|
||||
"liquidity": _row_liquidity(row),
|
||||
"status": row.get("status") or row.get("signal_status") or "",
|
||||
}
|
||||
for row in rows
|
||||
if isinstance(row, dict)
|
||||
],
|
||||
}
|
||||
raw = json.dumps(finger, sort_keys=True, ensure_ascii=False, default=str)
|
||||
return "overview:" + hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _build_highlights(rows: List[Dict[str, Any]]) -> List[Dict[str, str]]:
|
||||
ranked = sorted(
|
||||
(row for row in rows if isinstance(row, dict) and _row_city(row)),
|
||||
key=lambda item: (_row_score(item), _row_liquidity(item)),
|
||||
reverse=True,
|
||||
)
|
||||
highlights: List[Dict[str, str]] = []
|
||||
for row in ranked[:5]:
|
||||
city = _row_city(row)
|
||||
edge = _row_edge(row)
|
||||
liquidity = _row_liquidity(row)
|
||||
gap = _row_prob_gap(row)
|
||||
edge_text = f"{edge:.1f}%" if edge is not None else "--"
|
||||
gap_text = f"{gap * 100:.1f}pp" if gap is not None else "--"
|
||||
liquidity_text = f"{liquidity:,.0f}" if liquidity else "--"
|
||||
highlights.append(
|
||||
{
|
||||
"city": city,
|
||||
"note_zh": f"edge {edge_text},模型/市场概率差 {gap_text},流动性 {liquidity_text}。",
|
||||
"note_en": f"edge {edge_text}, model-market gap {gap_text}, liquidity {liquidity_text}.",
|
||||
}
|
||||
)
|
||||
return highlights
|
||||
|
||||
|
||||
def _build_payload(rows: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
clean_rows = [row for row in rows if isinstance(row, dict)]
|
||||
total = len(clean_rows)
|
||||
tradable = sum(1 for row in clean_rows if not row.get("closed") and not row.get("stale_for_today"))
|
||||
high_risk = sum(
|
||||
1
|
||||
for row in clean_rows
|
||||
if str(row.get("risk_level") or row.get("risk") or "").lower() in {"high", "danger", "red"}
|
||||
)
|
||||
avg_edge_values = [_row_edge(row) for row in clean_rows]
|
||||
avg_edge_nums = [value for value in avg_edge_values if value is not None]
|
||||
avg_edge = sum(avg_edge_nums) / len(avg_edge_nums) if avg_edge_nums else 0.0
|
||||
total_liquidity = sum(_row_liquidity(row) for row in clean_rows)
|
||||
highlights = _build_highlights(clean_rows)
|
||||
|
||||
overview_zh = (
|
||||
f"当前区域共有 {total} 个天气合约,{tradable} 个可交易;"
|
||||
f"高风险 {high_risk} 个,平均 edge {avg_edge:.1f}%,总流动性 {total_liquidity:,.0f}。"
|
||||
"优先查看 edge、final score 与流动性同时靠前的城市。"
|
||||
)
|
||||
overview_en = (
|
||||
f"{total} weather contracts are in scope, {tradable} tradable; "
|
||||
f"{high_risk} high-risk rows, average edge {avg_edge:.1f}%, total liquidity {total_liquidity:,.0f}. "
|
||||
"Prioritize rows where edge, final score and liquidity align."
|
||||
)
|
||||
|
||||
return {
|
||||
"overview_zh": overview_zh,
|
||||
"overview_en": overview_en,
|
||||
"highlights": highlights,
|
||||
"generated_at": datetime.utcnow().isoformat() + "Z",
|
||||
"cache_ttl_sec": OVERVIEW_CACHE_TTL_SEC,
|
||||
"source": "deterministic",
|
||||
}
|
||||
|
||||
|
||||
def build_market_overview_payload(
|
||||
rows: List[Dict[str, Any]],
|
||||
*,
|
||||
locale: str = "zh-CN",
|
||||
force_refresh: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
if not rows:
|
||||
return {
|
||||
"overview_zh": "",
|
||||
"overview_en": "",
|
||||
"highlights": [],
|
||||
"generated_at": None,
|
||||
"cache_ttl_sec": OVERVIEW_CACHE_TTL_SEC,
|
||||
"source": "deterministic",
|
||||
}
|
||||
|
||||
key = _cache_key(rows, locale)
|
||||
if not force_refresh:
|
||||
with _OVERVIEW_CACHE_LOCK:
|
||||
cached = _OVERVIEW_CACHE.get(key)
|
||||
if cached and cached.get("expires_at", 0) >= time.time():
|
||||
return cached["payload"]
|
||||
|
||||
payload = _build_payload(rows)
|
||||
with _OVERVIEW_CACHE_LOCK:
|
||||
_OVERVIEW_CACHE[key] = {
|
||||
"expires_at": time.time() + OVERVIEW_CACHE_TTL_SEC,
|
||||
"payload": payload,
|
||||
}
|
||||
return payload
|
||||
Reference in New Issue
Block a user