Отслеживать
1
0
Ответвление
У вас уже есть ответвление Warrior_EA
2
ответвлён от animatedread/Warrior_EA
Warrior_EA/research/backtest.py
AnimateDread ff46cfd57e research(dipz): the vol-gated dip-buy on four indices - screens, bear test, reconciliation
Recovered live config (z20 <= -1.5, exit SMA20 / 10 bars, 3xATR) plus a
Garman-Klass vol-regime gate. Expectancy is monotone in the vol regime in
IS, OOS and full sample, 4/4 indices; the gate reverses on USDJPY/XAUUSD.
D1 2008-2026 survives 2008/2020/2022 (maxDD 2.8%, ret/DD 7.74); the gate
halves trades, so it belongs on H4, never D1. reconcile.py matches the EA
to the backtest trade by trade; combine_charts.py rebuilds the account
curve from per-chart tester runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 13:24:57 -04:00

186 строки
7,2 КиБ
Python

"""
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