"""Trade one session, compute indicators from another — the two session switches.

A 24h futures tape prints around the clock, but a strategy usually wants to act
in one session only. That raises a question the bar loop cannot answer for you:
**which bars should feed each indicator?** This file answers it both ways in one
strategy, because the honest answer differs per indicator.

* **Level measures** (`Sma`, `Ema`) answer *where is price*. Price genuinely
  traded overnight and for equity index futures a large share of the drift
  accrues there, so an NY-only `Ema` is anchored to yesterday's 16:00 close and
  blind to a London repricing on an ECB headline — it will report "trend up"
  into a market that already moved. Feed these the CONTINUOUS tape.
* **Dispersion measures** (`Atr`, `StdDev`, `Rsi`, `Stoch`, `BBands`) answer
  *how much does price move per bar*, and that is session-dependent. Asia on
  MNQ is thin and range-compressed, so an `Atr(14)` on 24h 5-minute bars read at
  09:30 ET is computed almost entirely from pre-market bars: it UNDERSTATES NY
  volatility at the open — the most violent minutes of the day, and exactly when
  stop distance is being sized. The bias washes out about 70 minutes in, so the
  contamination is concentrated precisely where it does damage. SCOPE these.

The rule that generalises to any indicator you add later: **levels are
continuous across sessions, dispersion is not.**

THE TWO SWITCHES ARE INDEPENDENT, AND THAT IS THE WHOLE POINT.

* ``use(indicator, session=...)`` scopes an indicator's INPUT DATA.
* ``trade_sessions=`` scopes DECISIONS — when ``on_bar`` may fire.

Restricting trading to New York does NOT starve the continuous ``Ema``: it keeps
consuming Asia and London bars, because an indicator fed only the tradable
window would develop gaps and compute a different value from the same tape.
Equally, scoping the ``Atr`` to New York does not stop the strategy trading
anywhere. Either switch can be used without the other.

WHAT SCOPING COSTS: a scoped indicator warms in ITS OWN cadence, so it needs
``history_bars`` bars *of its session*. On 5-minute bars an NY-scoped ``Sma(30)``
sees 78 bars a session, so its 1920 updates span ~25 trading days against ~7 for
the same indicator on a 24h feed. Read ``strategy.warm`` rather than comparing
bar counts by hand — a single count cannot express two cadences.

Sessions are defined in their OWN timezone (`Asia/Tokyo`, `Europe/London`,
`America/New_York`), so daylight saving comes from the IANA database and there
is no table to maintain. This is not pedantry: London and New York switch on
different dates, so the London session really is 04:00 ET rather than 03:00 for
about three weeks each spring and one each autumn. If you prefer fixed ET
blocks, `Session` is a plain value type — `Session("LONDON_ET", ET, time(3),
time(11))` behaves identically, midnight-wrapping included.

Session membership is a DIFFERENT axis from the Topstep trading day. Asia sits
after the 18:00 ET rollover, so its bars belong to the NEXT trading day; both
classifications are needed and neither replaces the other. And `trade_sessions`
only narrows when THIS strategy chooses to act — the 16:10 ET flatten and the
16:10-18:00 no-trade window still apply regardless.

    uv run python examples/session_scoped.py
"""

from __future__ import annotations

from topstep_backtest import NEW_YORK, SymbolStrategy
from topstep_backtest.indicators import Atr, Ema
from topstep_backtest.protocols import Bar


class NySessionTrend(SymbolStrategy):
    """Long the trend during New York, with a stop sized from NY volatility."""

    def __init__(
        self,
        contract_id: str,
        *,
        trend: int = 50,
        atr_period: int = 14,
        size: int = 1,
        stop_atrs: int = 2,
        target_atrs: int = 3,
    ) -> None:
        # Switch 2: decisions happen in New York only. Indicators are untouched
        # by this — see the update counts printed at the bottom of this file.
        super().__init__(contract_id, trade_sessions=(NEW_YORK,))
        # Switch 1, applied differently per indicator, for the reasons in the
        # module docstring. The Ema sees Asia and London; the Atr does not.
        self.trend = self.use(Ema(trend))
        self.atr = self.use(Atr(atr_period), session=NEW_YORK)
        self.size = size
        self.stop_atrs = stop_atrs
        self.target_atrs = target_atrs

    async def on_bar(self, bar: Bar) -> None:
        # Reached only for bars inside NEW_YORK, and only once BOTH indicators
        # are ready. The NY-scoped Atr is the slow one: it advances on roughly a
        # quarter of the tape, so it — not the Ema — sets when trading starts.
        if not (self.position.flat and not self.working_orders):
            return
        if bar.close <= self.trend.value:
            return
        # An indicator value is NOT a tradeable price and is not tick-snapped,
        # so convert to a tick COUNT rather than routing it into grid math.
        atr_ticks = int(self.atr.value / self.spec.tick_size)
        if atr_ticks < 1:
            return
        await self.buy(
            self.size,
            stop_loss_ticks=max(1, atr_ticks * self.stop_atrs),
            take_profit_ticks=max(1, atr_ticks * self.target_atrs),
        )


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

    from topstep_backtest import ASIA, LONDON, 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"
    # hours="globex" is REQUIRED here: the default "rth" mode stamps 09:30-16:00
    # ET only, which is the New York session and nothing else. An RTH tape cannot
    # demonstrate — or test — anything session-scoped.
    bars = synthetic_bars(
        contract_id=contract,
        spec=spec_for_symbol("MNQ"),
        start_day=date(2026, 5, 4),
        days=5,
        seed=11,
        start_price=Decimal("18000.00"),
        hours="globex",
        vol_ticks=6,
    )
    strategy = NySessionTrend(contract)
    report = Backtest(bars, strategy, account=AccountSize.S50K).run()

    per_session = {s.name: sum(1 for b in bars if s.contains(b)) for s in (ASIA, LONDON, NEW_YORK)}
    print(f"tape: {len(bars)} bars over 5 trading days -> {per_session}\n")

    # The decoupling, made visible: the Ema advanced on EVERY bar while the
    # strategy only ever traded in New York.
    for registration in strategy.registrations:
        scope = registration.session.name if registration.session else "continuous"
        print(
            f"  {type(registration.indicator).__name__:<5} scope={scope:<11} "
            f"updates={registration.updates:>5}  warm={registration.warm}"
        )
    traded = strategy.bars_seen - strategy.bars_gated - strategy.bars_out_of_session
    print(f"\n  bars seen           {strategy.bars_seen}")
    print(f"  held by ready-gate  {strategy.bars_gated}")
    print(f"  outside NEW_YORK    {strategy.bars_out_of_session}")
    print(f"  on_bar fired        {traded}")
    print()
    print(report)
