supertrend-signal-alerts/tests/reference.py
132 lines
5.1 KiB
Python
# SPDX-License-Identifier: MPL-2.0
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not
# distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/
# SuperTrend: method by Olivier Seban; TradingView "SuperTrend" script by KivancOzbilgic.
"""Independent Python reference for SuperTrend Signal Alerts, written from the TradingView Pine script's semantics
(KivancOzbilgic's "SuperTrend"), not translated from the MQL5 code:
src = hl2
atr = changeATR ? ta.atr(Periods) : ta.sma(ta.tr, Periods) # ta.tr: first bar = high - low
up = src - Multiplier * atr; up1 = nz(up[1], up)
up := close[1] > up1 ? max(up, up1) : up
dn = src + Multiplier * atr; dn1 = nz(dn[1], dn)
dn := close[1] < dn1 ? min(dn, dn1) : dn
trend = nz(trend[1], 1)
trend := trend == -1 and close > dn1 ? 1 : trend == 1 and close < up1 ? -1 : trend
line = trend == 1 ? up : dn
buy = trend == 1 and trend[1] == -1; sell = trend == -1 and trend[1] == 1
ta.atr is Wilder's RMA seeded with the SMA of the first `period` true ranges (as on TradingView).
Additions in the MT5 product: optional EMA trend filter (EMA of the close, SMA-seeded: buy only close > EMA, sell
only close < EMA) and RSI filter (ta.rsi: RMA of gains/losses seeded with the SMA of the first `period` changes;
buy only RSI > 50, sell only RSI < 50)."""
import numpy as np
NAN = np.nan
def above(a, b):
"""Strict a > b ignoring floating-point noise (a band can tie a close exactly in real arithmetic); same as MQL5."""
return a - b > 1e-10 * abs(b)
def below(a, b):
return b - a > 1e-10 * abs(b)
def true_range(h, l, c):
tr = h - l
tr[1:] = np.maximum(tr[1:], np.maximum(np.abs(h[1:] - c[:-1]), np.abs(l[1:] - c[:-1])))
return tr
def sma(x, n):
"""Plain SMA. Each window is summed left to right (like Pine's ta.sma and the MQL5 loop); a running cumsum
difference would carry ~1e-13 noise, and SuperTrend compares close against bands that can tie exactly."""
out = np.full(len(x), NAN)
for i in range(n - 1, len(x)):
s = 0.0
for v in x[i - n + 1:i + 1]:
s += v
out[i] = s / n
return out
def rma(x, n):
"""RMA seeded with the SMA of x[:n]; x may carry leading NaN (then seeded at the first full window)."""
out = np.full(len(x), NAN)
first = int(np.argmax(~np.isnan(x))) if (~np.isnan(x)).any() else len(x)
s = first + n - 1
if s >= len(x):
return out
out[s] = x[first:first + n].mean()
for i in range(s + 1, len(x)):
out[i] = (out[i - 1] * (n - 1) + x[i]) / n
return out
def ema(x, n):
out = np.full(len(x), NAN)
if len(x) < n:
return out
out[n - 1] = x[:n].mean()
k = 2.0 / (n + 1)
for i in range(n, len(x)):
out[i] = out[i - 1] + k * (x[i] - out[i - 1])
return out
def rsi(c, n):
ch = np.concatenate(([NAN], np.diff(c)))
up, dn = rma(np.where(np.isnan(ch), NAN, np.maximum(ch, 0)), n), rma(np.where(np.isnan(ch), NAN, np.maximum(-ch, 0)), n)
out = np.full(len(c), NAN)
for i in range(len(c)):
if np.isnan(up[i]):
continue
out[i] = 100.0 if dn[i] == 0 else 0.0 if up[i] == 0 else 100.0 - 100.0 / (1.0 + up[i] / dn[i])
return out
def supertrend(o, h, l, c, period=10, mult=3.0, rma_atr=True, use_ema=False, ema_period=200,
use_rsi=False, rsi_period=14):
"""Returns (line, buy, sell) arrays; buy/sell hold the arrow price (low/high) or NaN.
The last bar is treated as still forming: no signal on it."""
n = len(c)
src = (h + l) / 2.0
tr = true_range(h, l, c)
atr = rma(tr, period) if rma_atr else sma(tr, period)
e, r = ema(c, ema_period), rsi(c, rsi_period)
up, dn, trend = np.full(n, NAN), np.full(n, NAN), np.full(n, NAN)
line, buy, sell = np.full(n, NAN), np.full(n, NAN), np.full(n, NAN)
for i in range(n):
if np.isnan(atr[i]):
continue
u, d = src[i] - mult * atr[i], src[i] + mult * atr[i]
u1 = up[i - 1] if i > 0 and not np.isnan(up[i - 1]) else u # nz(up[1], up)
d1 = dn[i - 1] if i > 0 and not np.isnan(dn[i - 1]) else d
if i > 0 and above(c[i - 1], u1):
u = max(u, u1)
if i > 0 and below(c[i - 1], d1):
d = min(d, d1)
t_prev = trend[i - 1] if i > 0 and not np.isnan(trend[i - 1]) else 1.0
t = t_prev
if t == -1 and above(c[i], d1):
t = 1.0
elif t == 1 and below(c[i], u1):
t = -1.0
up[i], dn[i], trend[i] = u, d, t
line[i] = u if t == 1 else d
if i == 0 or i == n - 1 or np.isnan(trend[i - 1]):
continue
b, s = t == 1 and trend[i - 1] == -1, t == -1 and trend[i - 1] == 1
if use_ema:
b = b and not np.isnan(e[i]) and c[i] > e[i]
s = s and not np.isnan(e[i]) and c[i] < e[i]
if use_rsi:
b = b and not np.isnan(r[i]) and r[i] > 50
s = s and not np.isnan(r[i]) and r[i] < 50
if b:
buy[i] = l[i]
if s:
sell[i] = h[i]
return line, buy, sell