Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Supported statistics/indicators are:

**Momentum**: RSI, StochRSI, MACD, PPO, KDJ, ROC, CMO, KST, Coppock, AO, BOP, CTI, Inertia, PSL

**Trend**: Supertrend, Aroon, Ichimoku, CR, DMI (+DI/-DI/ADX/ADXR), TRIX, WT
**Trend**: Supertrend, Aroon, Ichimoku, CR, DMI (+DI/-DI/ADX/ADXR), TRIX, WT, SSL Channel

**Volatility**: Bollinger Bands, ATR, TR, CCI, WR, CHOP, KER, Z-Score, MAD, PGO

Expand Down Expand Up @@ -174,6 +174,7 @@ will be re-evaluated the next time you access them.
| DMI | `pdi`, `ndi`, `adx` | 14 | Directional Movement Index |
| TRIX | `trix` | 12 | Triple Exponential Average |
| WT | `wt1`, `wt2` | 10, 21 | Wave Trend |
| SSL | `ssl`, `ssl_up`, `ssl_down` | 10 | SSL Channel |
| Bollinger | `boll` | 20 | Bollinger Bands |
| ATR | `atr` | 14 | Average True Range |
| TR | `tr` | - | True Range |
Expand Down Expand Up @@ -661,6 +662,26 @@ Retrieve the LazyBear's Wave Trend with `df['wt1']` and `df['wt2']`.
Wave trend uses two parameters. You can tune them with
`set_dft_window('wt', (10, 21))`.

#### [SSL Channel](https://www.tradingview.com/script/xzIoaIJC-SSL-channel/)

The SSL Channel (Securement Stop Loss Channel), created by Erwin Beckers, is a
trend-following indicator that calculates dynamic support and resistance bands
using the simple moving averages of High and Low prices. The channel lines flip
positions based on whether the closing price crosses above the SMA of High
(bullish) or below the SMA of Low (bearish).

It has a parameter for window size. The default window is 10. Change it with
`set_dft_window('ssl', n)`.

Examples:
* `df['ssl']` retrieves the 10-period SSL Channel line (defaults to SSL Up)
* `df['ssl_up']` retrieves the 10-period SSL Up line
* `df['ssl_down']` retrieves the 10-period SSL Down line
* `df['ssl_up_5']` retrieves the 5-period SSL Up line
* `df['ssl_down_5']` retrieves the 5-period SSL Down line
* `df['ssl_up_xu_ssl_down']` detects bullish crossover (Buy signal)
* `df['ssl_up_xd_ssl_down']` detects bearish crossunder (Sell signal)

---

### Volatility
Expand Down
78 changes: 78 additions & 0 deletions stockstats.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ class StockStatsError(Exception):
"rsv": 9,
"rvgi": 14,
"stochrsi": 14,
"ssl": 10,
"ssl_down": 10,
"ssl_up": 10,
"supertrend": 14,
"tema": 5,
"trix": 12,
Expand Down Expand Up @@ -739,6 +742,30 @@ def _get_supertrend(self, meta: _Meta):
self[f"{meta.name}_ub"] = final_ub
self[f"{meta.name}_lb"] = final_lb

def _get_ssl(self, meta: _Meta):
"""SSL Channel (Securement Stop Loss Channel)

https://www.tradingview.com/script/xzIoaIJC-SSL-channel/
Created by Erwin Beckers.

Calculates dynamic support and resistance bands based on the
SMA of High and Low prices and the close price relative position.
"""
window = meta.as_int
ssl_up, ssl_down = self.ssl_channel(
self.high, self.low, self.close, window)

suffix = f"_{meta.windows}" if meta._windows is not None else ""
self[f"ssl{suffix}"] = ssl_up
self[f"ssl_up{suffix}"] = ssl_up
self[f"ssl_down{suffix}"] = ssl_down

def _get_ssl_up(self, meta: _Meta):
self._get_ssl(meta)

def _get_ssl_down(self, meta: _Meta):
self._get_ssl(meta)

@staticmethod
def _rolling_arg_index(arr, window, mode="max"):
n = len(arr)
Expand Down Expand Up @@ -1219,6 +1246,56 @@ def hma(cls, series: pd.Series, window: int) -> pd.Series:
return pd.Series(np.zeros(n), index=series.index,
name=series.name)

@classmethod
def ssl_channel(cls, high: pd.Series, low: pd.Series, close: pd.Series,
window: int = 10) -> tuple[pd.Series, pd.Series]:
"""SSL Channel (Securement Stop Loss Channel)

Created by Erwin Beckers. Uses SMA of High and Low prices to track
trend direction and dynamic support/resistance channel bands.

Formula:
sma_high = SMA(high, window)
sma_low = SMA(low, window)
hlv[i] = 1 if close[i] > sma_high[i] else (
-1 if close[i] < sma_low[i] else hlv[i-1])
ssl_down = sma_high if hlv < 0 else sma_low
ssl_up = sma_low if hlv < 0 else sma_high

:param high: series of high prices
:param low: series of low prices
:param close: series of close prices
:param window: lookback period (default 10)
:return: (ssl_up, ssl_down) tuple of Series
"""
window = cls.get_int_positive(window)
n = len(close)
if n == 0:
empty = pd.Series(dtype=float, index=close.index)
return empty.copy(), empty.copy()

sma_high = cls.sma(high, window).values
sma_low = cls.sma(low, window).values
close_arr = close.values

hlv = np.zeros(n, dtype=int)
hlv[0] = 1 if close_arr[0] >= sma_high[0] else (
-1 if close_arr[0] < sma_low[0] else 1)

for i in range(1, n):
if close_arr[i] > sma_high[i]:
hlv[i] = 1
elif close_arr[i] < sma_low[i]:
hlv[i] = -1
else:
hlv[i] = hlv[i - 1]

ssl_up = np.where(hlv < 0, sma_low, sma_high)
ssl_down = np.where(hlv < 0, sma_high, sma_low)

return (pd.Series(ssl_up, index=close.index, name="ssl_up"),
pd.Series(ssl_down, index=close.index, name="ssl_down"))

@classmethod
def linear_reg(cls, series, window, correlation=False):
window = cls.get_int_positive(window)
Expand Down Expand Up @@ -2143,6 +2220,7 @@ def handler(self):
("wt1", "wt2"): self._get_wt,
("supertrend", "supertrend_lb",
"supertrend_ub"): self._get_supertrend,
("ssl", "ssl_up", "ssl_down"): self._get_ssl,
("bop",): self._get_bop,
("cti",): self._get_cti,
("eribull", "eribear"): self._get_eri,
Expand Down
52 changes: 52 additions & 0 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,58 @@ def test_hma_edge_cases():
h_large = StockDataFrame.hma(series, 10)
assert_that(list(h_large), equal_to([0.0] * 6))

def test_ssl_channel(self):
stock = self.get_stock_30days()
ssl = stock["ssl"]
ssl_up = stock["ssl_up"]
ssl_down = stock["ssl_down"]
assert_that(list(ssl), equal_to(list(ssl_up)))

# Initial bullish state
assert_that(ssl_up[20110104], near_to(12.6800))
assert_that(ssl_down[20110104], near_to(12.3000))
assert_that(ssl_up[20110114], near_to(13.2322))
assert_that(ssl_down[20110114], near_to(12.8500))

# Bearish flip on 20110117 (close drops below sma_low)
assert_that(ssl_up[20110117], near_to(12.8060))
assert_that(ssl_down[20110117], near_to(13.2190))
assert_that(ssl_up[20110128], near_to(12.5660))
assert_that(ssl_down[20110128], near_to(12.9140))

# Crossunder detection (sell signal)
sell_cross = stock["ssl_up_xd_ssl_down"]
assert_that(sell_cross[20110117], equal_to(True))

# Default window equivalence
assert_that(list(stock["ssl_up_10"]), equal_to(list(ssl_up)))
assert_that(list(stock["ssl_down_10"]), equal_to(list(ssl_down)))

# Custom window
u5 = stock["ssl_up_5"]
d5 = stock["ssl_down_5"]
assert_that(u5[20110104], near_to(12.6800))
assert_that(d5[20110104], near_to(12.3000))
assert_that(u5[20110114], near_to(13.4200))
assert_that(d5[20110114], near_to(13.1080))
assert_that(u5[20110117], near_to(13.0040))
assert_that(d5[20110117], near_to(13.3720))

@staticmethod
def test_ssl_channel_edge_cases():
# Empty series
empty_up, empty_down = StockDataFrame.ssl_channel(
pd.Series(dtype=float), pd.Series(dtype=float),
pd.Series(dtype=float), 5)
assert_that(len(empty_up), equal_to(0))
assert_that(len(empty_down), equal_to(0))

# Single-row series
s_up, s_down = StockDataFrame.ssl_channel(
pd.Series([10.5]), pd.Series([9.5]), pd.Series([10.0]), 3)
assert_that(list(s_up), equal_to([10.5]))
assert_that(list(s_down), equal_to([9.5]))

def test_coppock(self):
stock = self.get_stock_90days()
c0 = stock["coppock"]
Expand Down
Loading