| # 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/
|
| # UT Bot: idea by Yo_adriiiiaan (from an original idea by HPotter); TradingView "UT Bot Alerts" by QuantNomad.
|
| """Independent Python reference for UT Bot Alerts, written from the TradingView Pine script's semantics
|
| (QuantNomad's "UT Bot Alerts"), not translated from the MQL5 code:
|
|
|
| xATR = ta.atr(c) # Wilder's RMA of the true range, SMA-seeded
|
| nLoss = a * xATR
|
| trail = src > trail[1] and src[1] > trail[1] ? max(trail[1], src - nLoss)
|
| : src < trail[1] and src[1] < trail[1] ? min(trail[1], src + nLoss)
|
| : src > trail[1] ? src - nLoss : src + nLoss
|
| buy = src > trail and crossover(src, trail) # crossover: src > trail and src[1] <= trail[1]
|
| sell = src < trail and crossunder(src, trail)
|
|
|
| Additions in the MT5 product: optional Heikin Ashi close as src; optional EMA(200) of the close as a trend filter
|
| (buy only above it, sell only below), EMA seeded with the SMA of the first `period` closes."""
|
| import numpy as np
|
|
|
| NAN = np.nan
|
|
|
|
|
| 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 rma(x, n):
|
| out = np.full(len(x), NAN)
|
| if len(x) < n:
|
| return out
|
| out[n - 1] = x[:n].mean()
|
| for i in range(n, 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 heikin_close(o, h, l, c):
|
| return (o + h + l + c) / 4.0
|
|
|
|
|
| def utbot(o, h, l, c, key=1.0, atr_period=10, heikin=False, use_ema=False, ema_period=200):
|
| """Returns (trail, 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 = heikin_close(o, h, l, c) if heikin else c.copy()
|
| atr = rma(true_range(h, l, c), atr_period)
|
| e = ema(c, ema_period)
|
| trail = np.full(n, NAN)
|
| buy, sell = np.full(n, NAN), np.full(n, NAN)
|
| for i in range(n):
|
| if np.isnan(atr[i]):
|
| continue
|
| prev = trail[i - 1] if i > 0 and not np.isnan(trail[i - 1]) else 0.0
|
| s, s1, nl = src[i], (src[i - 1] if i > 0 else src[i]), key * atr[i]
|
| if s > prev and s1 > prev:
|
| trail[i] = max(prev, s - nl)
|
| elif s < prev and s1 < prev:
|
| trail[i] = min(prev, s + nl)
|
| else:
|
| trail[i] = s - nl if s > prev else s + nl
|
| if i == 0 or i == n - 1 or np.isnan(trail[i - 1]):
|
| continue
|
| up = s > trail[i] and s1 <= trail[i - 1]
|
| dn = s < trail[i] and s1 >= trail[i - 1]
|
| if use_ema:
|
| up = up and not np.isnan(e[i]) and c[i] > e[i]
|
| dn = dn and not np.isnan(e[i]) and c[i] < e[i]
|
| if up:
|
| buy[i] = l[i]
|
| if dn:
|
| sell[i] = h[i]
|
| return trail, buy, sell
|