"""Helper for manual M2D trade entry — derives full M2DExtraction dict from minimal user inputs. User provides 6 required fields: data, ora_ro, directie, entry, sl, outcome_path. All other fields default or are computed: - tp0 = entry ± 0.4 × |entry - sl| - tp1 = entry ± 0.6 × |entry - sl| - tp2 = entry ± 1.0 × |entry - sl| (symmetric with sl) - risc_pct = 100 × |entry - sl| / entry - ora_utc = ora_ro converted via Europe/Bucharest (DST-aware) - max_reached derived from outcome_path - be_moved = True if outcome contains TP0 else False - tf_mare/tf_mic default 5min/1min - calitate default 'n/a' - confidence = 'high' (manual entry) - screenshot_file generated if not provided: --.png """ from __future__ import annotations from datetime import date, datetime, time from typing import Literal from zoneinfo import ZoneInfo RO_TZ = ZoneInfo("Europe/Bucharest") UTC_TZ = ZoneInfo("UTC") OUTCOME_TO_MAX_REACHED = { "SL": "SL_first", "TP0→SL": "TP0", "TP0→TP1": "TP1", "TP0→TP2": "TP2", "TP0→pending": "TP0", "pending": "SL_first", # placeholder; user can override } OUTCOME_TO_BE_MOVED = { "SL": False, "TP0→SL": True, # BE move should have happened; True = rule-enforced "TP0→TP1": True, "TP0→TP2": True, "TP0→pending": True, "pending": False, } def ro_to_utc(data_iso: str, ora_ro_str: str) -> str: """Convert (YYYY-MM-DD, HH:MM RO) -> HH:MM UTC string, DST-aware.""" d = date.fromisoformat(data_iso) t = datetime.strptime(ora_ro_str, "%H:%M").time() dt_ro = datetime.combine(d, t, tzinfo=RO_TZ) dt_utc = dt_ro.astimezone(UTC_TZ) return dt_utc.strftime("%H:%M") def build_extraction( data: str, ora_ro: str, directie: Literal["Buy", "Sell"], entry: float, sl: float, outcome_path: Literal["SL", "TP0→SL", "TP0→TP1", "TP0→TP2", "TP0→pending", "pending"], instrument: Literal["DIA", "US30", "other"] = "DIA", tf_mare: Literal["5min", "15min"] = "5min", tf_mic: Literal["1min", "3min"] = "1min", calitate: Literal["Clară", "Mai mare ca impuls", "Slabă", "n/a"] = "n/a", max_reached: Literal["SL_first", "TP0", "TP1", "TP2"] | None = None, be_moved: bool | None = None, screenshot_file: str | None = None, note: str = "", ) -> dict: """Build a M2DExtraction-compatible dict from minimal manual inputs. Derived fields: - ora_utc from ora_ro (DST-aware) - tp0/tp1/tp2 from entry/sl geometry - risc_pct from |entry-sl|/entry - max_reached/be_moved from outcome_path (overridable) - screenshot_file generated from data+instrument+ora_ro if not provided The returned dict satisfies scripts.vision_schema.M2DExtraction. """ if entry == sl: raise ValueError("entry == sl — zero risk distance") risk_abs = abs(entry - sl) risc_pct = round(100 * risk_abs / entry, 4) if directie == "Buy": if sl >= entry: raise ValueError(f"Buy: sl ({sl}) must be < entry ({entry})") tp0 = round(entry + 0.4 * risk_abs, 4) tp1 = round(entry + 0.6 * risk_abs, 4) tp2 = round(entry + risk_abs, 4) else: # Sell if sl <= entry: raise ValueError(f"Sell: sl ({sl}) must be > entry ({entry})") tp0 = round(entry - 0.4 * risk_abs, 4) tp1 = round(entry - 0.6 * risk_abs, 4) tp2 = round(entry - risk_abs, 4) ora_utc = ro_to_utc(data, ora_ro) if max_reached is None: max_reached = OUTCOME_TO_MAX_REACHED[outcome_path] if be_moved is None: be_moved = OUTCOME_TO_BE_MOVED[outcome_path] if screenshot_file is None: ora_compact = ora_ro.replace(":", "") screenshot_file = f"{data}-{instrument.lower()}-{ora_compact}.png" return { "screenshot_file": screenshot_file, "data": data, "ora_utc": ora_utc, "instrument": instrument, "directie": directie, "tf_mare": tf_mare, "tf_mic": tf_mic, "calitate": calitate, "entry": round(float(entry), 4), "sl": round(float(sl), 4), "tp0": tp0, "tp1": tp1, "tp2": tp2, "risc_pct": risc_pct, "outcome_path": outcome_path, "max_reached": max_reached, "be_moved": be_moved, "confidence": "high", "ambiguities": [], "note": note, }