"""
rules_api.py

The contract between the bot CORE (monitor.py) and the RULE MODULES
(breakout_rules.py, reversal_rules.py, exit_rules.py, or any new file
listed under config.json "rules"). Core file -- rule work never needs to
change it.

What monitor.py hands a rule module every poll (every
schedule.poll_interval_seconds) is a MarketView: a read-only picture of
one symbol right now. What it expects back is an EntryDecision (entry
modules) or an ExitDecision (the exit module).

ENTRY MODULE -- must define:
    evaluate(view: MarketView, state: dict) -> EntryDecision
        Called for every watched symbol with no open position while a
        slot is free and before schedule.no_new_entries_after.
        `state` is this module's own per-symbol dict; monitor.py keeps it
        between polls (put timers, confirmations, anything in it). It is
        cleared when the symbol leaves the watchlist or after a buy fills.
        To buy: return EntryDecision(should_enter=True, stop=<price>, ...).
        The stop is required (position size = 1% account risk to that
        stop, capped by trading.max_position_notional_pct_of_equity).

EXIT MODULE -- must define:
    evaluate(view: MarketView, position: dict, state: dict) -> ExitDecision
        Called every poll for every open position. `position` is the
        position record (entry_price, stop_price, qty, entry_time, setup =
        which entry module bought it, plan = whatever that module put in
        EntryDecision.plan). `state` is per-position (fresh dict for each
        new position).

OPTIONAL in any rule module:
    on_trade(symbol, ts, price, size)          raw ticks, straight from the
    on_quote(symbol, ts, bid, ask, bid_size, ask_size)    stream thread --
    on_bar(symbol, bar)                        keep these FAST, they run
    seed_bars(symbol, bars)                    for every tick
    on_entry(symbol, position)                 after a buy is placed
    on_exit(symbol, position, reason)          after a sell is placed
    on_session_end()                           at EOD (flush files etc.)

Anything a rule module raises is caught and logged by monitor.py; it can
never crash the core.
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Callable, Dict, List, Optional, Tuple


@dataclass
class MarketView:
    symbol: str
    now: datetime                         # UTC
    price: float                          # last price (close of the forming 1-min bar)
    bars: List[dict]                      # session 1-min bars incl. the forming one: {"t","o","h","l","c","v"}
    bars_sub: List[dict]                  # sub-minute buckets (streaming.sub_minute_bucket_seconds)
    quote: Optional[Tuple[float, float]]  # (bid, ask) or None
    levels: Dict[str, Optional[float]]    # premarket_high, prev_day_high, range_20d_high, session_high,
                                          # daily_atr (a DISTANCE, not a level), ref_5d_* keys
    volume_baseline: Optional[float]      # prior-session volume (the scanner's baseline)
    scan: dict                            # this symbol's scanner record (score, metrics)
    minutes_since_open: float
    slots_free: bool                      # a position slot is free right now
    last_trade: Optional[dict]            # this symbol's most recent CLOSED trade today, if any
    cfg: dict                             # this module's own config.json section
    benchmarks: Dict[str, List[dict]] = field(default_factory=dict)   # streaming.benchmark_symbols bars
    _imbalance: Optional[Callable[[float], Optional[float]]] = None

    def imbalance(self, window_seconds: float = 20.0) -> Optional[float]:
        """Buy-minus-sell trade imbalance over the last window, -1..+1
        (None when there are too few trades)."""
        return self._imbalance(window_seconds) if self._imbalance else None


@dataclass
class EntryDecision:
    should_enter: bool = False
    stop: Optional[float] = None          # required when should_enter
    state: str = "WAIT"                   # short label for the decision log, e.g. WAIT / WATCH / REJECT / BUY
    reason: str = ""                      # one line, plain English
    reasons: List[str] = field(default_factory=list)
    plan: dict = field(default_factory=dict)      # stored on the position (target, levels...) for exit_rules
    metrics: dict = field(default_factory=dict)   # anything worth logging


@dataclass
class ExitDecision:
    should_exit: bool = False
    state: str = "HOLD"
    reason: str = ""
    metrics: dict = field(default_factory=dict)
