""" Multi-symbol strategy screen, ranked the way the account is actually judged. RANKING RULE (non-negotiable here): drawdown first, not profit. A candidate is only reported as viable if it clears ALL THREE of * cadence >= 2 trades / month (a strategy that never trades is not one) * maxDD <= the prop limit * ret/DD >= 2 Profit ranking is what produces the curve-fit winners this project keeps re-discovering, so it is deliberately not the sort key. COSTS ARE REAL. The exported `spread` column is in POINTS at each bar, and point size is recovered from the decimal count the exporter wrote (it used DoubleToString(price, digits), so the file's precision IS the symbol's digits). A long round trip pays the full spread once: you buy the ask and sell the bid. This matters more than any parameter -- the decade verdict on this project was that spread is the whole gap between paper and traded results. CONTROL. Every candidate is compared against a LONG-ONLY RANDOM ENTRY with a matched holding period, not a two-sided coin flip. On a drifting index a two-sided control is a straw man: it loses money by construction, so beating it proves nothing. Long bias on an index is not a flaw, it is the asset. """ from __future__ import annotations import numpy as np COMMON = r"C:\Users\admin\AppData\Roaming\MetaQuotes\Terminal\Common\Files" # ------------------------------------------------------------------ loading def load(symbol: str, period: str = "PERIOD_D1"): path = rf"{COMMON}\bars_{symbol}_{period}.csv" raw = np.genfromtxt(path, delimiter=",", skip_header=1, dtype=str, encoding="ansi") ts = np.array([f"{r[0][:10].replace('.', '-')}T{r[0][11:]}" for r in raw], dtype="datetime64[s]") o, h, l, c = (raw[:, i].astype(float) for i in (1, 2, 3, 4)) vol = raw[:, 5].astype(float) spread_pts = raw[:, 6].astype(float) # digits == decimals the exporter wrote; point == 10^-digits digits = max(len(s.split(".")[1]) if "." in s else 0 for s in raw[:20, 4]) point = 10.0 ** (-digits) return dict(ts=ts, o=o, h=h, l=l, c=c, v=vol, cost=spread_pts * point, point=point, digits=digits, symbol=symbol) # --------------------------------------------------------------- indicators def sma(x, n): out = np.full(len(x), np.nan) cs = np.concatenate([[0.0], np.cumsum(x)]) out[n - 1:] = (cs[n:] - cs[:-n]) / n return out def rolling_std(x, n): out = np.full(len(x), np.nan) cs = np.concatenate([[0.0], np.cumsum(x)]) cs2 = np.concatenate([[0.0], np.cumsum(x * x)]) m = (cs[n:] - cs[:-n]) / n m2 = (cs2[n:] - cs2[:-n]) / n out[n - 1:] = np.sqrt(np.maximum(m2 - m * m, 0.0)) return out def atr(h, l, c, n=14): tr = np.maximum(h[1:] - l[1:], np.maximum(np.abs(h[1:] - c[:-1]), np.abs(l[1:] - c[:-1]))) tr = np.concatenate([[h[0] - l[0]], tr]) out = np.full(len(tr), np.nan) if len(tr) <= n: return out out[n - 1] = tr[:n].mean() for i in range(n, len(tr)): out[i] = (out[i - 1] * (n - 1) + tr[i]) / n return out def rsi(c, n=2): d = np.diff(c, prepend=c[0]) up = np.where(d > 0, d, 0.0) dn = np.where(d < 0, -d, 0.0) au = np.full(len(c), np.nan) ad = np.full(len(c), np.nan) if len(c) <= n: return au au[n] = up[1:n + 1].mean() ad[n] = dn[1:n + 1].mean() for i in range(n + 1, len(c)): au[i] = (au[i - 1] * (n - 1) + up[i]) / n ad[i] = (ad[i - 1] * (n - 1) + dn[i]) / n rs = au / np.where(ad > 0, ad, np.nan) out = 100 - 100 / (1 + rs) out[ad == 0] = 100.0 return out # ----------------------------------------------------------------- simulate def simulate(d, entries, side=1, exit_ma=None, max_bars=10, stop_atr=3.0, atr_n=14): """Bar-close signals, NEXT-BAR-OPEN fills. Returns per-trade log. entries : bool array; entry decided on bar i, filled at open of i+1. exit_ma : if given, exit when close crosses back to this level; else time/stop. NO LOOKAHEAD: every exit test uses bar j's own high/low/close, and the stop is checked against the bar's extreme before its close, so an intrabar stop is honoured rather than assumed away. """ o, h, l, c = d["o"], d["h"], d["l"], d["c"] a = atr(h, l, c, atr_n) cost = d["cost"] ts = d["ts"] trades = [] i = 0 n = len(c) while i < n - 2: if not entries[i] or not np.isfinite(a[i]) or a[i] <= 0: i += 1 continue fill = i + 1 entry = o[fill] if not np.isfinite(entry) or entry <= 0: i += 1 continue stop = entry - side * stop_atr * a[i] exit_px, exit_j, reason = None, None, None for j in range(fill, min(fill + max_bars, n)): # stop first: within a bar we cannot know order, so assume the worse if side > 0 and l[j] <= stop: exit_px, exit_j, reason = stop, j, "stop" break if side < 0 and h[j] >= stop: exit_px, exit_j, reason = stop, j, "stop" break if exit_ma is not None and np.isfinite(exit_ma[j]): if (side > 0 and c[j] >= exit_ma[j]) or (side < 0 and c[j] <= exit_ma[j]): exit_px, exit_j, reason = c[j], j, "target" break if exit_px is None: exit_j = min(fill + max_bars - 1, n - 1) exit_px, reason = c[exit_j], "time" gross = side * (exit_px - entry) net = gross - cost[fill] # full spread once, round trip trades.append(dict(entry_i=fill, exit_i=exit_j, t=ts[fill], ret=net / entry, gross=gross / entry, reason=reason, bars=exit_j - fill + 1)) i = exit_j + 1 # no overlapping positions return trades def metrics(trades, ts, risk_frac=0.01, stop_atr=3.0): """Equity curve at fixed fractional risk; drawdown on that curve. Each trade risks `risk_frac` of equity at the stop distance, so a trade's equity impact is risk_frac * (net return / stop distance) -- i.e. R multiples, which is how the prop limit is actually consumed. """ if not trades: return None years = (ts[-1] - ts[0]) / np.timedelta64(365, "D") months = max(years * 12.0, 1e-9) rets = np.array([t["ret"] for t in trades]) eq = [1.0] for t in trades: # R multiple: net move relative to the risked distance eq.append(eq[-1] * (1.0 + risk_frac * t["r"])) eq = np.array(eq) peak = np.maximum.accumulate(eq) dd = (peak - eq) / peak total = eq[-1] - 1.0 maxdd = dd.max() return dict(n=len(trades), per_month=len(trades) / months, exp_bp=rets.mean() * 1e4, hit=float((rets > 0).mean()), total=total, maxdd=maxdd, ret_dd=(total / maxdd if maxdd > 1e-9 else np.nan), cagr=(eq[-1] ** (1 / max(years, 1e-9)) - 1.0), years=years) def add_r(trades, d, stop_atr=3.0, atr_n=14): """Attach the R multiple to each trade (net return / risked distance).""" a = atr(d["h"], d["l"], d["c"], atr_n) for t in trades: i = t["entry_i"] - 1 risk = stop_atr * a[i] / d["o"][t["entry_i"]] t["r"] = t["ret"] / risk if risk > 0 else 0.0 return trades