File size: 4,900 Bytes
9831ced
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import numpy as np
import pandas as pd

def sma(series: pd.Series, window: int) -> pd.Series:
    return series.rolling(window=window).mean()

def ema(series: pd.Series, window: int) -> pd.Series:
    return series.ewm(span=window, adjust=False).mean()

def _wilder_rma(values: pd.Series, window: int) -> pd.Series:
    if isinstance(window, bool) or not isinstance(window, (int, np.integer)) or window < 1:
        raise ValueError('Wilder period must be a positive integer.')
    arr = values.to_numpy(dtype=float)
    n = len(arr)
    out = np.full(n, np.nan)
    seed, prev = [], None
    for i, value in enumerate(arr):
        if not np.isfinite(value):
            seed, prev = [], None
            continue
        if prev is None:
            seed.append(value)
            if len(seed) == window:
                prev = float(np.mean(seed))
                out[i] = prev
        else:
            prev = (prev * (window - 1) + value) / window
            out[i] = prev
    return pd.Series(out, index=values.index)

def rsi(series: pd.Series, window: int=14) -> pd.Series:
    delta = series.diff()
    gain = delta.clip(lower=0)
    loss = -delta.clip(upper=0)
    avg_gain = _wilder_rma(gain, window)
    avg_loss = _wilder_rma(loss, window)
    rs = avg_gain / avg_loss.replace(0, np.nan)
    out = 100 - 100 / (1 + rs)
    out = out.mask((avg_loss == 0) & (avg_gain > 0), 100.0)
    return out.mask((avg_loss == 0) & (avg_gain == 0), 50.0)

def macd(series: pd.Series, fast: int=12, slow: int=26, signal: int=9):
    fast_ema = ema(series, fast)
    slow_ema = ema(series, slow)
    macd_line = fast_ema - slow_ema
    signal_line = ema(macd_line, signal)
    histogram = macd_line - signal_line
    return (macd_line, signal_line, histogram)

def bollinger_bands(series: pd.Series, window: int=20, num_std: float=2.0):
    mid = sma(series, window)
    std = series.rolling(window=window).std(ddof=0)
    return (mid + num_std * std, mid, mid - num_std * std)

def stochastic_oscillator(high: pd.Series, low: pd.Series, close: pd.Series, k_window: int=14, d_window: int=3):
    lowest_low = low.rolling(window=k_window).min()
    highest_high = high.rolling(window=k_window).max()
    span = highest_high - lowest_low
    percent_k = 100 * (close - lowest_low) / span.replace(0, np.nan)
    percent_k = percent_k.mask(span == 0, 50.0)
    percent_d = percent_k.rolling(window=d_window).mean()
    return (percent_k, percent_d)

def add_all_indicators(df: pd.DataFrame) -> pd.DataFrame:
    out = df.copy()
    out['SMA_20'] = sma(out['Close'], 20)
    out['EMA_20'] = ema(out['Close'], 20)
    out['RSI_14'] = rsi(out['Close'], 14)
    macd_line, signal_line, hist = macd(out['Close'])
    out['MACD'] = macd_line
    out['MACD_Signal'] = signal_line
    out['MACD_Hist'] = hist
    upper, mid, lower = bollinger_bands(out['Close'])
    out['BB_Upper'] = upper
    out['BB_Mid'] = mid
    out['BB_Lower'] = lower
    k, d = stochastic_oscillator(out['High'], out['Low'], out['Close'])
    out['Stoch_%K'] = k
    out['Stoch_%D'] = d
    return out

def _round(value, digits):
    return round(float(value), digits) if pd.notna(value) else 'n/a'

def latest_signals(df_with_indicators: pd.DataFrame) -> dict:
    if df_with_indicators.empty:
        raise ValueError('No candles available for latest indicators.')
    last = df_with_indicators.iloc[-1]
    signals = {}
    rsi_value = last['RSI_14']
    signals['RSI (14)'] = {'value': _round(rsi_value, 2), 'signal': 'Insufficient data' if pd.isna(rsi_value) else 'Overbought' if rsi_value > 70 else 'Oversold' if rsi_value < 30 else 'Neutral'}
    macd_ready = pd.notna(last['MACD']) and pd.notna(last['MACD_Signal'])
    signals['MACD'] = {'value': _round(last['MACD'], 5), 'signal': 'Insufficient data' if not macd_ready else 'Bullish (above signal)' if last['MACD'] > last['MACD_Signal'] else 'Bearish (below signal)' if last['MACD'] < last['MACD_Signal'] else 'Neutral'}
    bb_ready = pd.notna(last['BB_Upper']) and pd.notna(last['BB_Lower'])
    signals['Bollinger Bands'] = {'value': _round(last['Close'], 5), 'signal': 'Insufficient data' if not bb_ready else 'Above Upper Band' if last['Close'] > last['BB_Upper'] else 'Below Lower Band' if last['Close'] < last['BB_Lower'] else 'Inside Bands'}
    sma_ready = pd.notna(last['SMA_20'])
    signals['Moving Averages'] = {'value': f"SMA20={_round(last['SMA_20'], 5)}, EMA20={_round(last['EMA_20'], 5)}", 'signal': 'Insufficient data for SMA' if not sma_ready else 'Price Above MA (Uptrend)' if last['Close'] > last['SMA_20'] else 'Price Below MA (Downtrend)' if last['Close'] < last['SMA_20'] else 'Neutral'}
    stoch_value = last['Stoch_%K']
    signals['Stochastic (14,3)'] = {'value': _round(stoch_value, 2), 'signal': 'Insufficient data' if pd.isna(stoch_value) else 'Overbought' if stoch_value > 80 else 'Oversold' if stoch_value < 20 else 'Neutral'}
    return signals