"""Tutorial strategy: EMA crossover on MNQ with a tick-based OCO bracket.

A worked companion to ``sma_cross.py`` — same chassis, same seams, an
exponential moving average instead of a simple one. Read
``docs/TUTORIAL_EMA_CROSSOVER.md`` alongside this file; it walks every line.

The two seams that matter (identical to the SMA reference):

* Parity seam — the class reaches the venue ONLY through protocol surfaces:
  the ``buy``/``close`` sugar delegates to ``self.ctx.orders`` /
  ``self.ctx.positions``, which both the deterministic ``SimBroker`` and the
  live ``AsyncTopstepClient`` satisfy, so the SAME class runs unchanged in a
  backtest or against the real gateway. Hooks stay async; orders stay awaited.
* Chassis guarantees — ``SymbolStrategy`` owns the bookkeeping: ``use()``-d
  indicators are auto-updated once per matching bar (registration order ==
  update order), ``on_bar`` is ready-gated until every indicator has seen its
  lookback, and ``position`` / ``working_orders`` are folded from the same SDK
  events the live hub emits.

``Ema(n)`` is TA-Lib's ``EMA`` driven bar by bar — every indicator here is
TA-Lib (``indicators/talib_adapter.py``), so no formula in this project can
drift from the reference implementation. It is seeded with the SMA of the
first ``n`` closes and then recurses with ``k = 2/(n+1)``, TA-Lib's convention;
the declared warmup is TA-Lib's own, so ``Ema(12)`` still needs 12 bars and the
``use()`` gate still holds ``on_bar`` until the slowest one is ``ready``.
Values cross into ``Decimal`` at the library boundary and are NOT tick-snapped:
an EMA level is not a tradeable price. Prices remain exact Decimal; indicator
values are float64-precise and deterministic, so reruns stay byte-equal.

    uv run python examples/ema_cross.py
"""

from __future__ import annotations

from topstep_backtest.indicators import Cross, Ema
from topstep_backtest.protocols import Bar
from topstep_backtest.strategy import SymbolStrategy


class EmaCross(SymbolStrategy):
    """Go long on a fast/slow EMA cross-up; exit via the bracket or cross-down."""

    def __init__(
        self,
        contract_id: str,
        *,
        fast: int = 12,
        slow: int = 26,
        size: int = 2,
        stop_loss_ticks: int = 40,
        take_profit_ticks: int = 80,
    ) -> None:
        super().__init__(contract_id)
        # Registration ORDER == update order. The Cross reads fast.value and
        # slow.value, so both EMAs must be use()d BEFORE the Cross (SymbolStrategy
        # enforces this — an unregistered Cross input raises at construction).
        # The same rule covers the generic layer: to cross two outputs of one
        # multi-output function, use() the OWNER and pass its lines to the Cross
        # (see examples/talib_macd.py) — a line has no state to update.
        self.fast = self.use(Ema(fast))  # == TalibIndicator("EMA", timeperiod=fast)
        self.slow = self.use(Ema(slow))  # == TalibIndicator("EMA", timeperiod=slow)
        # Cross is the ONE thing here that is not TA-Lib: the C library has no
        # crossover function. It compares two TA-Lib outputs, it does not
        # compute an indicator.
        self.cross = self.use(Cross(self.fast, self.slow))
        # Each EMA retains a bounded window (`fast.history_bars`), not the whole
        # series: that is the parity decision. EMA is recursive, but its weights
        # decay, and past ~40x the period the truncated tail is below float64
        # resolution — so for THIS family (EMA, Wilder RSI/ATR/ADX, MACD, T3,
        # ...) the windowed value is also BIT-IDENTICAL to an unbounded one.
        # That bonus is not universal (an accumulator like OBV has a
        # window-relative level; KAMA smooths far more slowly than its lookback
        # suggests), but parity does not depend on it: sim and live run the same
        # window over the same bars either way.
        #
        # `ready` and `warm` are NOT the same gate. `ready` (what use() waits
        # for) means a value exists — bar 26 for Ema(26). `warm` means the
        # window is full — bar 1664 — and only from there is the value
        # independent of where this run started. Preload `history_bars` bars
        # live and every indicator reports warm and matches sim bit for bit;
        # preload only `lookback` and the values are merely close.
        self.size = size
        self.stop_loss_ticks = stop_loss_ticks
        self.take_profit_ticks = take_profit_ticks

    async def on_bar(self, bar: Bar) -> None:
        # Called only for self.contract_id, only once every use()d indicator is
        # ready, with every indicator already updated for THIS bar. An order
        # placed here is eligible from the NEXT bar (the accepted_ts firewall);
        # a market order fills at that next bar's open.
        if self.cross.up and self.position.flat and not self.working_orders:
            await self.buy(
                self.size,
                stop_loss_ticks=self.stop_loss_ticks,
                take_profit_ticks=self.take_profit_ticks,
            )
        elif self.cross.down and self.position.is_long:
            await self.close()


if __name__ == "__main__":
    from datetime import date
    from decimal import Decimal

    from topstep_backtest import AccountSize, Backtest
    from topstep_backtest.core.instruments import spec_for_symbol
    from topstep_backtest.data.synthetic import synthetic_bars

    contract = "CON.F.US.MNQ.U26"
    bars = synthetic_bars(
        contract_id=contract,
        spec=spec_for_symbol("MNQ"),
        start_day=date(2026, 5, 4),
        days=6,
        seed=7,
        start_price=Decimal("18000.00"),
        bars_per_day=120,
        vol_ticks=12,
    )
    report = Backtest(bars, EmaCross(contract), account=AccountSize.S50K).run()
    print(report)
