"""Strategy “ema_cross” exported from Stochastly.

GENERATED BY STOCHASTLY — do not edit by hand, re-export.
kernel     : ema_cross
parameters : {"atr_mult": 0.75, "ema_fast": 13, "ema_slow": 21, "rr": 3}
direction  : L
fidelity   : 1.0 over 7 engine trades (direction L), compared TRADE FOR TRADE — entry bar, exit bar, side and gross R, no tolerance — against the engine's own trade table. Precision 1.0 (0 trade(s) this file books and the engine does not). IDENTICAL on every path measured. Measured on synthetic price paths, not on your own data. Worst of 4 synthetic path(s) that produced engine activity, out of 4 tried.

WARNING — this file renders the node's DECLARED RULE. It does not re-implement the Stochastly
engine. The `fidelity` line above says how far this rule drifts from the kernel that produced your
backtest. A measured gap is worth infinitely more than an assumed zero one.

HOW TO READ THAT NUMBER, honestly. It is measured on SYNTHETIC price paths, never on your own data.
The `fidelity` line above states its exact terms, and they differ by kernel - read that line:
  · engine kernels (stop/target machine) : every trade the engine booked is compared TRADE FOR
    TRADE - entry bar, exit bar, side and gross R - against `trades()` below, with no tolerance,
    and extra trades this file books are counted and reported.
  · signal kernels : compared BAR-EXACT and side-for-side against the engine's own compiled kernel,
    with no tolerance, and extra signals ARE counted and reported as a precision figure.
1.00 with 0 extra means IDENTICAL on those paths. Anything else is printed, never rounded away.

EXECUTION, exactly as the engine ran it (`trades()` below replays it trade for trade):
  - a signal at the close of bar i opens at the OPEN of bar i+1;
  - stop  = 0.75 x ATR(14) of the signal bar, checked on each bar's low/high (stop FIRST, then target), booked as -1 R;
  - target = 2.25 x ATR (rr = 3), booked as +3 R;
  - no session rule was wired: nothing forces a close, ever;
  - direction L: the engine simulates BOTH sides and drops the other side's trades afterwards, so a phantom position on the forbidden side blocks new entries while it lives. `trades()` does the same;
  - Execution-node settings this kernel IGNORES (hard-coded in the engine, so your backtest ignored them too): trail_k (this kernel has no trailing-stop input); exit_mode (this kernel uses its fixed exit policy); sl_variant (this kernel has no stop-variant input);
  - sizing is one unit per trade and the R is GROSS: your broker's cost applies instead of the engine's cost model.

NOT EXPORTED : nothing — every node wired in this graph is in this file.
"""
import numpy as np


def _roll(a, n, fn):
    out = np.full(len(a), np.nan)
    for i in range(n - 1, len(a)):
        out[i] = fn(a[i - n + 1: i + 1])
    return out


def sma(x, n): return _roll(np.asarray(x, dtype=float), n, np.mean)
def rstd(x, n): return _roll(np.asarray(x, dtype=float), n, lambda w: float(np.std(w, ddof=0)))
def rmax(x, n): return _roll(np.asarray(x, dtype=float), n, np.max)
def rmin(x, n): return _roll(np.asarray(x, dtype=float), n, np.min)


def ema(x, n):
    a = np.asarray(x, dtype=float)
    out = np.full(len(a), np.nan)
    if not len(a):
        return out
    k, acc = 2.0 / (n + 1.0), a[0]
    out[0] = acc
    for i in range(1, len(a)):
        acc = a[i] * k + acc * (1 - k)
        out[i] = acc
    return out


def delta(x, n):
    v = np.asarray(x, dtype=float)
    out = np.full(len(v), np.nan)
    out[n:] = v[n:] - v[:-n]
    return out


def div(x, y):
    with np.errstate(divide="ignore", invalid="ignore"):
        return np.where(np.asarray(y) != 0, np.asarray(x, dtype=float) / np.asarray(y, dtype=float), np.nan)


def zscore(x, n):
    v = np.asarray(x, dtype=float)
    m, s = sma(v, n), rstd(v, n)
    with np.errstate(divide="ignore", invalid="ignore"):
        return np.where(s > 0, (v - m) / s, 0.0)


def rsi(close, n):
    """Wilder's RSI, exactly as the engine computes it: 50 on the first n bars, seeded on the
    plain average of bars 1..n, then (avg * (n - 1) + x) / n; an average loss under 1e-12 reads 100."""
    c = np.asarray(close, dtype=float)
    m = len(c)
    out = np.full(m, 50.0)
    if m < n + 1:
        return out
    d = np.diff(c, prepend=c[0])
    up, dn = np.clip(d, 0, None), np.clip(-d, 0, None)
    ag, al = float(np.sum(up[1:n + 1])) / n, float(np.sum(dn[1:n + 1])) / n
    for i in range(n, m):
        if i > n:
            ag = (ag * (n - 1) + up[i]) / n
            al = (al * (n - 1) + dn[i]) / n
        out[i] = 100.0 if al < 1e-12 else 100.0 - 100.0 / (1.0 + ag / al)
    return out


def atr(high, low, close, n):
    """The engine's ATR: true range with TR[0] = high[0] - low[0], then a SIMPLE n-bar average."""
    h, l, c = (np.asarray(v, dtype=float) for v in (high, low, close))
    pc = np.concatenate([[c[0]], c[:-1]])
    tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
    tr[0] = h[0] - l[0]
    return _roll(tr, n, np.mean)


def and_(a, b):
    """The IR's AND. Rendering it as `(a & b)` (numpy's BITWISE and) fails on floats: any rule
    with one side going through `shift()` (which returns floats, to carry its NaN) raised
    `TypeError: ufunc 'bitwise_and' not supported` - in a file shipped with a fidelity of 1.00 in
    its header. The `> 0` test reproduces the engine exactly."""
    return (np.asarray(a, dtype=float) > 0) & (np.asarray(b, dtype=float) > 0)


def not_(a):
    """The IR's negation: `not (a > 0)`, and specifically NOT `logical_not(a)`.
    The two DIFFER on a NaN: `logical_not(nan)` is False (nan is "truthy"), while the engine
    computes `not (nan > 0)` = True. A window not yet filled would otherwise flip the signal to
    the wrong side, on the first bars only - the hardest gap to spot."""
    return np.logical_not(np.asarray(a, dtype=float) > 0)


def crossup(a, b):
    a, b = np.asarray(a, dtype=float), np.asarray(b, dtype=float)
    prev = np.concatenate([[np.nan], (a - b)[:-1]])
    return (a > b) & (prev <= 0)


def shift(x, k):
    """The value k bars ago. It was missing: `bollinger` and `donchian` call it, and both exported
    files crashed on `NameError: shift` at the first useful line (measured on 19/08 by RUNNING the
    exports). Rendered text is not code until it has run.
    The first k values are NaN, never back-filled: copying the first
    bar would invent a past that does not exist, and an invented past is look-ahead in reverse."""
    v = np.asarray(x, dtype=float)
    k = int(k)
    if k <= 0:
        return v
    out = np.full(len(v), np.nan)
    out[k:] = v[:-k]
    return out


def signal(open, high, low, close, volume):
    """+1 = long entry, -1 = short entry, 0 = nothing. No future bar is read."""
    long_entry = crossup(ema(close, 13), ema(close, 21))
    short_entry = (0 < 0)
    return np.where(long_entry, 1, np.where(short_entry, -1, 0)).astype(float)


# --- THE POSITION MACHINE - what the engine did with each entry, replayed bar by bar -------------
ATR_PERIOD = 14
SL_MULT = 0.75          # stop distance, in ATR(ATR_PERIOD) measured at the signal bar
TP_MULT = 2.25          # target distance, in ATR
TP_FROM_STOP = True  # True: target = stop_distance * RR (bit-exact with the kernel)
RR = 3.0                    # R booked when the target is hit
BACKTEST_DRAG = 0.0       # audit only; live broker charges its own execution costs
HOLD_BARS = None      # None = no timeout; else exit at the close once i - signal_bar >= HOLD_BARS
FIRST_BAR = 1            # the first bar the kernel evaluates
REENTER_SAME_BAR = True  # after an exit on bar i, may a new signal on bar i open at i+1?
NEEDS_PREVIOUS_BAR = False    # the kernel needs one evaluated bar before it can enter
DIRECTION = 'L'
SESSION = {'mode': 'off', 'week_close_h': 21, 'max_break_h': 2.0, 'close_last_bar': False}          # the graph's Session node: mode / week_close_h / max_break_h / close_last_bar


def _long_raw(open, high, low, close, volume):
    return crossup(ema(close, 13), ema(close, 21))


def _short_raw(open, high, low, close, volume):
    return crossup(ema(close, 21), ema(close, 13))


def _skip_extra(open, high, low, close, volume):
    """Bars this kernel skips entirely (no exit check, no entry) besides an undefined ATR."""
    return None


def session_masks(ts_ns, n):
    """(force_close, no_entry) - the Session node's rule, ported from the engine line for line.
    `ts_ns` = bar timestamps in ns since the epoch (UTC); required only when a rule is wired."""
    fc = np.zeros(n, dtype=bool)
    no_entry = np.zeros(n, dtype=bool)
    mode = str(SESSION.get("mode", "off"))
    if ts_ns is None:
        if mode != "off":
            raise ValueError("this strategy carries a Session rule (mode %r): pass ts_ns" % mode)
        ts = None
    else:
        ts = np.asarray(ts_ns, dtype=np.int64)
        if len(ts) != n:
            raise ValueError("ts_ns must have one timestamp per bar")
    if n == 0:
        return fc, no_entry
    NS_H, NS_D = 3_600_000_000_000, 86_400_000_000_000
    if mode == "calendar" and n > 1:
        weekday = ((ts // NS_D + 3) % 7).astype(np.int8)        # 0 = Monday
        hour = ((ts // NS_H) % 24).astype(np.int8)
        av = (weekday == 4) & (hour >= np.int8(int(SESSION["week_close_h"])))
        fc |= av
        no_entry |= av
    elif mode == "data_gap" and n > 1:
        gaps = np.diff(ts).astype(np.float64)
        tf = float(np.median(gaps)) if len(gaps) else 0.0
        brk = (gaps - tf) > float(SESSION["max_break_h"]) * NS_H
        fc[:-1] |= brk
        no_entry[:-1] |= brk
    if SESSION.get("close_last_bar"):
        fc[n - 1] = True
        if n >= 2:
            fc[n - 2] = True
    return fc, no_entry


def trades(open, high, low, close, volume, ts_ns=None):
    """Every trade the engine would book on these bars: a list of dicts with entry_idx, exit_idx,
    side (+1/-1), pnl_R (GROSS, before your broker's cost), entry_price, stop, target.
    Bars are oldest first; no future bar is ever read."""
    o, h, l, c, v = (np.asarray(x, dtype=float) for x in (open, high, low, close, volume))
    n = len(c)
    lg = np.asarray(_long_raw(o, h, l, c, v), dtype=float) > 0
    sh = np.asarray(_short_raw(o, h, l, c, v), dtype=float) > 0
    a = atr(h, l, c, ATR_PERIOD)
    skip_atr = ~(np.isfinite(a) & (a > 0.0))
    extra = _skip_extra(o, h, l, c, v)
    skip_extra = (np.asarray(extra, dtype=float) > 0) if extra is not None else np.zeros(n, dtype=bool)
    fc, no_entry = session_masks(ts_ns, n)
    out = []
    act = False; side = 0; ep = 0.0; sl_d = 0.0; tp_d = 0.0; ei = 0; sig_bar = 0; seen = False

    def book(exit_i, pnl):
        out.append({"entry_idx": ei, "exit_idx": exit_i, "side": side, "pnl_R": pnl,
                    "entry_price": ep, "stop": ep - side * sl_d, "target": ep + side * tp_d})

    def clamp(x):
        return max(-1.0, min(RR, x))

    for i in range(FIRST_BAR, n - 1):
        if skip_atr[i]:
            continue
        if act and fc[i]:
            book(i, clamp(side * (c[i] - ep) / sl_d) if sl_d > 0 else 0.0)
            act = False
        if skip_extra[i]:
            continue
        if act:
            if side == 1:
                closed, pnl = ((True, -1.0) if l[i] <= ep - sl_d
                               else (True, RR) if h[i] >= ep + tp_d else (False, 0.0))
            else:
                closed, pnl = ((True, -1.0) if h[i] >= ep + sl_d
                               else (True, RR) if l[i] <= ep - tp_d else (False, 0.0))
            if closed:
                book(i, pnl)
                act = False
            elif HOLD_BARS is not None and i - sig_bar >= HOLD_BARS:
                book(i, clamp(side * (c[i] - ep) / sl_d) if sl_d > 0 else 0.0)
                act = False
            if act or not REENTER_SAME_BAR:
                seen = True
                continue
        if seen or not NEEDS_PREVIOUS_BAR:
            s = 1 if lg[i] else (-1 if sh[i] else 0)
            if s != 0 and i + 1 < n and not no_entry[i + 1]:
                ei = i + 1
                ep = o[ei]
                sl_d = a[i] * SL_MULT
                tp_d = sl_d * RR if TP_FROM_STOP else a[i] * TP_MULT
                side = s
                sig_bar = i
                act = True
        seen = True
    if DIRECTION == "L":
        out = [t for t in out if t["side"] > 0]
    elif DIRECTION == "S":
        out = [t for t in out if t["side"] < 0]
    return out


def position(open, high, low, close, volume, ts_ns=None):
    """The per-bar position (+1 / -1 / 0) implied by `trades()`: held from entry_idx to exit_idx."""
    n = len(close)
    pos = np.zeros(n, dtype=float)
    for t in trades(open, high, low, close, volume, ts_ns):
        pos[t["entry_idx"]:t["exit_idx"] + 1] = t["side"]
    return pos
