forex-scanner / utils /normalizer.py
Prachi Sharma
Added agent
650e6f4
Raw History Blame Contribute Delete
8.79 kB
"""
utils/normalizer.py
Central payload normalizer for live scanner and PA scanner outputs.
Purpose:
- prevent UI crashes from mixed payload shapes
- normalize dict/string/list fields
- fill missing nested keys with safe defaults
"""
from __future__ import annotations
from copy import deepcopy
from typing import Any, Dict, List
def _safe_dict(v: Any) -> Dict[str, Any]:
return v if isinstance(v, dict) else {}
def _safe_list(v: Any) -> List[Any]:
return v if isinstance(v, list) else []
def _safe_float(v: Any, default: float | None = None) -> float | None:
try:
if v is None or v == "":
return default
return float(v)
except Exception:
return default
def _safe_int(v: Any, default: int = 0) -> int:
try:
if v is None or v == "":
return default
return int(v)
except Exception:
return default
def _normalize_regime(v: Any) -> Dict[str, Any]:
if isinstance(v, dict):
return {
"regime": v.get("regime", v.get("label", "unknown")),
"label": v.get("label", v.get("regime", "unknown")),
}
if isinstance(v, str):
return {"regime": v, "label": v}
return {"regime": "unknown", "label": "unknown"}
def _normalize_setup(v: Any) -> Dict[str, Any]:
if isinstance(v, dict):
return {"name": v.get("name", "No clear setup"), **v}
if isinstance(v, str):
return {"name": v}
return {"name": "No clear setup"}
def _normalize_levels(v: Any) -> Dict[str, Any]:
d = _safe_dict(v)
return {
"entry": d.get("entry"),
"sl": d.get("sl"),
"tp": d.get("tp"),
"tp1": d.get("tp1"),
"tp2": d.get("tp2"),
"tp3": d.get("tp3"),
"be": d.get("be"),
"support": d.get("support"),
"resistance": d.get("resistance"),
"rr_ratio": d.get("rr_ratio"),
"good_rr": bool(d.get("good_rr", False)),
"room_pips": d.get("room_pips"),
"sl_pips": d.get("sl_pips"),
"tp_pips": d.get("tp_pips"),
"tp1_pips": d.get("tp1_pips"),
"tp2_pips": d.get("tp2_pips"),
"tp3_pips": d.get("tp3_pips"),
"risk_pips": d.get("risk_pips"),
"reward_pips": d.get("reward_pips"),
"sl_source": d.get("sl_source", "n/a"),
"tp_source": d.get("tp_source", "n/a"),
"digits": _safe_int(d.get("digits"), 5),
"atr_pips": d.get("atr_pips"),
"spread": d.get("spread"),
**d,
}
def _normalize_patterns(v: Any) -> Dict[str, Any]:
d = _safe_dict(v)
return {
"patterns": _safe_dict(d.get("patterns")),
"pattern_score": _safe_float(d.get("pattern_score"), 0.0),
"any_pattern": bool(d.get("any_pattern", False)),
"best": d.get("best"),
**d,
}
def _normalize_session(v: Any) -> Dict[str, Any]:
d = _safe_dict(v)
return {
"allowed": bool(d.get("allowed", False)),
"sessions": _safe_list(d.get("sessions")),
"active": _safe_list(d.get("active")),
"overlaps": _safe_list(d.get("overlaps")),
"score": d.get("score", 0),
"reason": d.get("reason", d.get("note", "")),
"note": d.get("note", d.get("reason", "")),
**d,
}
def _normalize_news(v: Any) -> Dict[str, Any]:
d = _safe_dict(v)
blocking = d.get("blocking", [])
if not isinstance(blocking, list):
blocking = [blocking]
return {
"allowed": bool(d.get("allowed", True)),
"ok": bool(d.get("ok", d.get("allowed", True))),
"note": d.get("note", "Clear"),
"events": _safe_list(d.get("events")),
"blocking": blocking,
"score": d.get("score", 1.0),
**d,
}
def _normalize_tf_block(v: Any, include_bias: bool = True) -> Dict[str, Any]:
d = _safe_dict(v)
out = {
"rsi": d.get("rsi"),
"adx": d.get("adx"),
"zone": d.get("zone", "mixed"),
}
if include_bias:
out.update(
{
"bias": d.get("bias", 0),
"label": d.get("label", "neutral"),
"stack_bull": bool(d.get("stack_bull", False)),
"stack_bear": bool(d.get("stack_bear", False)),
"ema20": d.get("ema20"),
"ema50": d.get("ema50"),
"sma200": d.get("sma200"),
"sma_slope": d.get("sma_slope", "?"),
}
)
out.update(d)
return out
def _normalize_ltf(v: Any) -> Dict[str, Any]:
d = _safe_dict(v)
return {
"ltf": d.get("ltf", "H1"),
"rsi": d.get("rsi"),
"atr": d.get("atr"),
"adx": d.get("adx"),
"triggered": bool(d.get("triggered", False)),
"signal": d.get("signal", 0),
"cross_ok": bool(d.get("cross_ok", False)),
"rsi_ok": bool(d.get("rsi_ok", False)),
"zone": d.get("zone", "mixed"),
"rsi_mode": d.get("rsi_mode", "pullback"),
"trigger_mode": d.get("trigger_mode", "none"),
"trigger_note": d.get("trigger_note", ""),
"entry_value_ok": bool(d.get("entry_value_ok", True)),
"entry_value_note": d.get("entry_value_note", ""),
"rsi_phase": d.get("rsi_phase", "unknown"),
"value_score": d.get("value_score"),
"value_score_note": d.get("value_score_note", ""),
"entry_timing_score": d.get("entry_timing_score"),
"entry_timing_note": d.get("entry_timing_note", ""),
**d,
}
def _normalize_explanation(v: Any) -> Dict[str, Any]:
d = _safe_dict(v)
return {
"summary": d.get("summary", ""),
"state": d.get("state", ""),
"bias_story": d.get("bias_story", ""),
"location_story": d.get("location_story", ""),
"mtf_story": d.get("mtf_story", ""),
"mtf_checks_verbose": _safe_dict(d.get("mtf_checks_verbose")),
"ltf_story": d.get("ltf_story", ""),
"trigger_expectation": d.get("trigger_expectation", ""),
"confirmation_story": d.get("confirmation_story", ""),
"momentum_story": d.get("momentum_story", ""),
"risk_story": d.get("risk_story", ""),
"session_story": d.get("session_story", ""),
"news_story": d.get("news_story", ""),
"blockers_story": d.get("blockers_story", ""),
"waiting_story": d.get("waiting_story", ""),
"next_step": d.get("next_step", ""),
**d,
}
def normalize_signal_payload(result: Dict[str, Any]) -> Dict[str, Any]:
"""
Normalize one scanner result payload into a stable shape for UI/audit.
"""
r = deepcopy(result) if isinstance(result, dict) else {}
r["setup"] = _normalize_setup(r.get("setup"))
r["regime"] = _normalize_regime(r.get("regime"))
if "regime_v2" in r:
r["regime_v2"] = _normalize_regime(r.get("regime_v2"))
r["levels"] = _normalize_levels(r.get("levels"))
r["patterns"] = _normalize_patterns(r.get("patterns"))
r["session"] = _normalize_session(r.get("session"))
r["news"] = _normalize_news(r.get("news"))
r["wtf"] = _normalize_tf_block(r.get("wtf"), include_bias=True)
r["htf"] = _normalize_tf_block(r.get("htf"), include_bias=True)
mtf = _safe_dict(r.get("mtf"))
r["mtf"] = {
"zone": mtf.get("zone", "mixed"),
"zone_label": mtf.get("zone_label", "mixed"),
"sr_levels": _safe_list(mtf.get("sr_levels")),
"score": _safe_int(mtf.get("score"), 0),
"max": _safe_int(mtf.get("max"), 0),
"confirmed": bool(mtf.get("confirmed", False)),
"rsi": mtf.get("rsi"),
"adx": mtf.get("adx"),
"checks": _safe_dict(mtf.get("checks")),
**mtf,
}
r["ltf"] = _normalize_ltf(r.get("ltf"))
r["explanation"] = _normalize_explanation(r.get("explanation"))
r["blockers"] = [str(x) for x in _safe_list(r.get("blockers"))]
r["waiting_for"] = [str(x) for x in _safe_list(r.get("waiting_for"))]
r["signal"] = _safe_int(r.get("signal"), 0)
r["tradeable"] = bool(r.get("tradeable", False))
r["grade"] = str(r.get("grade", "NO TRADE"))
r["direction"] = str(r.get("direction", "—"))
r["decision"] = str(r.get("decision", "NO TRADE"))
r["instrument"] = str(r.get("instrument", "—"))
r["ltf_tf"] = str(r.get("ltf_tf", "H1"))
# PA-specific optional blocks
r["context"] = _safe_dict(r.get("context"))
r["confirmed_setups"] = [str(x) for x in _safe_list(r.get("confirmed_setups"))]
r["near_levels"] = _safe_dict(r.get("near_levels"))
r["key_levels"] = _safe_dict(r.get("key_levels"))
r["pa_setups"] = _safe_dict(r.get("pa_setups"))
return r
def normalize_signal_list(results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
return [normalize_signal_payload(x) for x in results if isinstance(x, dict)]