JayLacoma commited on
Commit
e1b31ca
·
verified ·
1 Parent(s): 1bd0f1a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +163 -217
app.py CHANGED
@@ -4,12 +4,23 @@ import yfinance as yf
4
  import plotly.graph_objects as go
5
  import plotly.express as px
6
  import numpy as np
 
7
 
8
- # Use a consistent 8-color palette
9
- COLORS = px.colors.qualitative.Plotly # 10 colors, we use first 8
 
 
 
 
 
 
 
 
 
 
10
 
11
  # ======================
12
- # Indicator Calculations
13
  # ======================
14
 
15
  def calculate_rsi(df):
@@ -21,301 +32,236 @@ def calculate_rsi(df):
21
  return rsi
22
 
23
  def calculate_bollinger_bands(df):
24
- middle_bb = df['Close'].rolling(window=20).mean()
25
- std = df['Close'].rolling(window=20).std()
26
- upper_bb = middle_bb + 2 * std
27
- lower_bb = middle_bb - 2 * std
28
- return middle_bb, upper_bb, lower_bb
29
 
30
  def calculate_stochastic_oscillator(df):
31
- lowest_low = df['Low'].rolling(window=14).min()
32
- highest_high = df['High'].rolling(window=14).max()
33
- slowk = ((df['Close'] - lowest_low) / (highest_high - lowest_low)) * 100
34
- slowd = slowk.rolling(window=3).mean()
35
- return slowk, slowd
36
 
37
  def calculate_cmf(df, window=20):
38
  mfv = ((df['Close'] - df['Low']) - (df['High'] - df['Close'])) / (df['High'] - df['Low']) * df['Volume']
39
- cmf = mfv.rolling(window=window).sum() / df['Volume'].rolling(window=window).sum()
40
- return cmf
41
 
42
  # ======================
43
- # Signal Generation
44
  # ======================
45
 
46
- def generate_trading_signals(df, thresholds, enabled_signals):
47
- # Calculate indicators
48
  df['RSI'] = calculate_rsi(df)
49
  df['MiddleBB'], df['UpperBB'], df['LowerBB'] = calculate_bollinger_bands(df)
50
  df['SlowK'], df['SlowD'] = calculate_stochastic_oscillator(df)
51
  df['CMF'] = calculate_cmf(df)
52
 
53
- # Initialize signal columns
54
- for col in ['RSI_Signal', 'BB_Signal', 'Stochastic_Signal', 'CMF_Signal']:
55
- df[col] = 0
56
-
57
- # RSI Signal
58
- if 'RSI' in enabled_signals:
59
- rsi_lower = thresholds['RSI_lower']
60
- rsi_upper = thresholds['RSI_upper']
61
- df['RSI_Signal'] = np.where(df['RSI'] < rsi_lower, 1, 0)
62
- df['RSI_Signal'] = np.where(df['RSI'] > rsi_upper, -1, df['RSI_Signal'])
63
-
64
- # Bollinger Bands Signal
65
- if 'BB' in enabled_signals:
66
- bb_buffer_pct = thresholds['BB'] / 100 # Convert % to decimal
67
- below_lower = df['Close'] < df['LowerBB'] * (1 - bb_buffer_pct)
68
- above_upper = df['Close'] > df['UpperBB'] * (1 + bb_buffer_pct)
69
- # Require 2 consecutive days for confirmation (optional, reduces noise)
70
- df['BB_Signal'] = np.where(below_lower & below_lower.shift(1), 1, 0)
71
- df['BB_Signal'] = np.where(above_upper & above_upper.shift(1), -1, df['BB_Signal'])
72
-
73
- # Stochastic Signal
74
- if 'Stochastic' in enabled_signals:
75
- stoch_lower = thresholds['Stochastic_lower']
76
- stoch_upper = thresholds['Stochastic_upper']
77
- df['Stochastic_Signal'] = np.where((df['SlowK'] < stoch_lower) & (df['SlowD'] < stoch_lower), 1, 0)
78
- df['Stochastic_Signal'] = np.where((df['SlowK'] > stoch_upper) & (df['SlowD'] > stoch_upper), -1, df['Stochastic_Signal'])
79
-
80
- # CMF Signal
81
- if 'CMF' in enabled_signals:
82
- cmf_thresh = thresholds['CMF']
83
- df['CMF_Signal'] = np.where(df['CMF'] > cmf_thresh, -1, np.where(df['CMF'] < -cmf_thresh, 1, 0))
84
-
85
  return df
86
 
87
  # ======================
88
- # Plotting Function
89
  # ======================
90
 
91
- def plot_multi_ticker_signals(data_dict, enabled_signals, show_bollinger):
92
  fig = go.Figure()
93
- buy_hovers_all = []
94
- sell_hovers_all = []
95
-
96
- for idx, (ticker, df) in enumerate(data_dict.items()):
97
- color = COLORS[idx % len(COLORS)]
98
-
99
- # Plot price
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  fig.add_trace(go.Scatter(
101
- x=df.index,
102
- y=df['Close'],
103
  mode='lines',
104
- name=f'{ticker}',
105
- line=dict(color=color, width=2),
106
- legendgroup=ticker
107
  ))
108
 
109
- # Optionally plot Bollinger Bands (only if enabled AND toggle is on)
110
- if show_bollinger and 'BB' in enabled_signals:
111
  fig.add_trace(go.Scatter(
112
  x=df.index, y=df['UpperBB'],
113
- mode='lines',
114
- line=dict(color=color, width=1, dash='dot'),
115
- name=f'{ticker} Upper BB',
116
- legendgroup=ticker,
117
- showlegend=False
118
  ))
119
  fig.add_trace(go.Scatter(
120
  x=df.index, y=df['LowerBB'],
121
- mode='lines',
122
- line=dict(color=color, width=1, dash='dot'),
123
- fill='tonexty',
124
- fillcolor=f'rgba{color[3:-1]},0.1)', # Light fill
125
- name=f'{ticker} Lower BB',
126
- legendgroup=ticker,
127
- showlegend=False
128
  ))
129
 
130
- # Collect signals for markers
131
- signal_cols = [col for col in df.columns if col.endswith('_Signal')]
132
  for date in df.index:
133
- total_signal = sum(df.loc[date, col] for col in signal_cols)
134
- if total_signal > 0: # Buy
135
- active = [col.replace('_Signal', '') for col in signal_cols if df.loc[date, col] == 1]
136
- hover = f"<b>{ticker}</b><br>Buy: {', '.join(active)}<br>{date.strftime('%Y-%m-%d')}<br>${df.loc[date, 'Close']:.2f}"
137
- buy_hovers_all.append((date, df.loc[date, 'Close'] * 0.995, hover, color))
138
- elif total_signal < 0: # Sell
139
- active = [col.replace('_Signal', '') for col in signal_cols if df.loc[date, col] == -1]
140
- hover = f"<b>{ticker}</b><br>Sell: {', '.join(active)}<br>{date.strftime('%Y-%m-%d')}<br>${df.loc[date, 'Close']:.2f}"
141
- sell_hovers_all.append((date, df.loc[date, 'Close'] * 1.005, hover, color))
142
-
143
- # Add buy markers
144
- if buy_hovers_all:
145
- x, y, text, colors = zip(*buy_hovers_all)
 
 
 
 
 
 
146
  fig.add_trace(go.Scatter(
147
  x=x, y=y,
148
  mode='markers',
149
- marker=dict(symbol='triangle-up', size=9, color=colors, line=dict(color='white', width=0.8)),
150
- name='Buy Signals',
151
- hoverinfo='text',
152
  hovertext=text,
153
- showlegend=True
 
154
  ))
155
 
156
- # Add sell markers
157
- if sell_hovers_all:
158
- x, y, text, colors = zip(*sell_hovers_all)
159
  fig.add_trace(go.Scatter(
160
  x=x, y=y,
161
  mode='markers',
162
- marker=dict(symbol='triangle-down', size=9, color=colors, line=dict(color='white', width=0.8)),
163
- name='Sell Signals',
164
- hoverinfo='text',
165
  hovertext=text,
166
- showlegend=True
 
167
  ))
168
 
 
169
  fig.update_layout(
170
- title="Multi-Ticker Technical Signals (RSI, BB, Stochastic, CMF)",
171
- xaxis_title="Date",
172
- yaxis_title="Price",
173
- plot_bgcolor='#1e1e1e',
174
- paper_bgcolor='#1e1e1e',
175
- font=dict(color='white'),
176
  legend=dict(
177
  orientation='h',
178
  yanchor='bottom',
179
  y=1.02,
180
  xanchor='center',
181
  x=0.5,
182
- bgcolor='rgba(30,30,30,0.8)'
 
183
  ),
184
- height=800,
185
- width=1200,
186
- hovermode='closest'
 
187
  )
188
- fig.update_xaxes(rangeslider_visible=True)
189
  return fig
190
 
191
  # ======================
192
- # Main Analysis Function
193
  # ======================
194
 
195
- def stock_analysis(
196
- ticker,
197
- start_date,
198
- end_date,
199
- rsi_lower,
200
- rsi_upper,
201
- bb_buffer,
202
- stoch_lower,
203
- stoch_upper,
204
- cmf_threshold,
205
- show_bollinger
206
- ):
207
  try:
208
- tickers = [t.strip().upper() for t in ticker.split(',') if t.strip()][:8]
209
  if not tickers:
210
- raise ValueError("No valid ticker provided")
211
 
212
- enabled_signals = ['RSI', 'BB', 'Stochastic', 'CMF']
213
- thresholds = {
214
- 'RSI_lower': rsi_lower,
215
- 'RSI_upper': rsi_upper,
216
- 'BB': bb_buffer,
217
- 'Stochastic_lower': stoch_lower,
218
- 'Stochastic_upper': stoch_upper,
219
- 'CMF': cmf_threshold
220
- }
221
-
222
- data_dict = {}
223
  for t in tickers:
224
  df = yf.download(t, start=start_date, end=end_date)
225
- if df.empty:
226
- continue
227
- if isinstance(df.columns, pd.MultiIndex):
228
- df.columns = df.columns.droplevel(1)
229
- df = generate_trading_signals(df, thresholds, enabled_signals)
230
- df_last = df.tail(min(360, len(df)))
231
- data_dict[t] = df_last
232
 
233
- if not data_dict:
234
- raise ValueError("No data retrieved for any ticker")
235
 
236
- return plot_multi_ticker_signals(data_dict, enabled_signals, show_bollinger)
237
 
238
  except Exception as e:
239
  fig = go.Figure()
240
- fig.add_annotation(
241
- text=f"Error: {str(e)}",
242
- x=0.5, y=0.5,
243
- showarrow=False,
244
- font=dict(color="red", size=16)
245
- )
246
- fig.update_layout(
247
- plot_bgcolor='#1e1e1e',
248
- paper_bgcolor='#1e1e1e',
249
- height=800,
250
- width=1200
251
- )
252
  return fig
253
 
254
  # ======================
255
- # Gradio Interface
256
  # ======================
257
 
258
- custom_theme = gr.themes.Monochrome(
259
- primary_hue="blue",
260
- secondary_hue="purple",
261
- neutral_hue="gray",
262
- radius_size=gr.themes.sizes.radius_sm,
263
- font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
264
- )
265
-
266
- with gr.Blocks(theme=custom_theme) as demo:
267
- gr.Markdown("# 📈 Multi-Ticker Signal Analyzer")
268
- gr.Markdown("Analyze up to 8 stocks using **RSI, Bollinger Bands, Stochastic, and CMF** with custom thresholds.")
269
-
270
  with gr.Row():
271
  with gr.Column(scale=1):
272
- ticker_input = gr.Textbox(
273
  label="Tickers (comma-separated, max 8)",
274
- placeholder="e.g., AAPL, MSFT, GOOGL, NVDA, TSLA, AMD, META, AMZN",
275
- value="NVDA, AAPL, MSFT"
276
  )
277
- start_date_input = gr.Textbox(label="Start Date", value="2022-01-01")
278
- end_date_input = gr.Textbox(label="End Date", value="2026-01-01")
279
-
280
- gr.Markdown("### 🔧 Signal Thresholds")
281
- rsi_lower = gr.Slider(5, 40, value=20, label="RSI Buy Threshold (Lower)")
282
- rsi_upper = gr.Slider(60, 95, value=80, label="RSI Sell Threshold (Upper)")
283
- stoch_lower = gr.Slider(5, 40, value=20, label="Stochastic Buy Threshold")
284
- stoch_upper = gr.Slider(60, 95, value=80, label="Stochastic Sell Threshold")
285
- cmf_threshold = gr.Slider(0.05, 0.8, value=0.3, label="CMF Signal Threshold (abs)")
286
- bb_buffer = gr.Slider(0.5, 10, value=3.0, label="Bollinger Band Buffer (%)")
287
-
288
- show_bollinger = gr.Checkbox(label="Show Bollinger Bands on Chart", value=False)
289
-
290
- analyze_btn = gr.Button("Analyze", variant="primary")
291
-
292
- signals_output = gr.Plot(label="Trading Signals")
293
-
294
- analyze_btn.click(
295
- stock_analysis,
296
- inputs=[
297
- ticker_input,
298
- start_date_input,
299
- end_date_input,
300
- rsi_lower,
301
- rsi_upper,
302
- bb_buffer,
303
- stoch_lower,
304
- stoch_upper,
305
- cmf_threshold,
306
- show_bollinger
307
- ],
308
- outputs=signals_output
309
  )
310
 
311
  gr.Markdown("""
312
- ### 📌 Notes
313
- - **Buy**: Green | **Sell**: Red ▼
314
- - Hover to see which indicators triggered the signal
315
- - Higher thresholds = fewer, stronger signals
316
- - Bollinger Bands can be toggled on/off for clarity
317
  """)
318
 
319
- # Launch
320
  if __name__ == "__main__":
321
  demo.launch()
 
4
  import plotly.graph_objects as go
5
  import plotly.express as px
6
  import numpy as np
7
+ from datetime import datetime
8
 
9
+ # Fixed high thresholds (strict = fewer, stronger signals)
10
+ THRESHOLDS = {
11
+ 'RSI_lower': 15, # Buy only if RSI < 15
12
+ 'RSI_upper': 85, # Sell only if RSI > 85
13
+ 'Stochastic_lower': 15, # Buy only if Stochastic < 15
14
+ 'Stochastic_upper': 85, # Sell only if Stochastic > 85
15
+ 'CMF': 0.4, # Buy if CMF < -0.4, Sell if CMF > 0.4
16
+ 'BB': 5.0 # Require price 5% beyond Bollinger Band
17
+ }
18
+
19
+ # 8 distinct colors for tickers
20
+ COLORS = px.colors.qualitative.Plotly
21
 
22
  # ======================
23
+ # Indicator Functions
24
  # ======================
25
 
26
  def calculate_rsi(df):
 
32
  return rsi
33
 
34
  def calculate_bollinger_bands(df):
35
+ ma = df['Close'].rolling(20).mean()
36
+ std = df['Close'].rolling(20).std()
37
+ return ma, ma + 2*std, ma - 2*std
 
 
38
 
39
  def calculate_stochastic_oscillator(df):
40
+ ll = df['Low'].rolling(14).min()
41
+ hh = df['High'].rolling(14).max()
42
+ k = ((df['Close'] - ll) / (hh - ll)) * 100
43
+ d = k.rolling(3).mean()
44
+ return k, d
45
 
46
  def calculate_cmf(df, window=20):
47
  mfv = ((df['Close'] - df['Low']) - (df['High'] - df['Close'])) / (df['High'] - df['Low']) * df['Volume']
48
+ return mfv.rolling(window).sum() / df['Volume'].rolling(window).sum()
 
49
 
50
  # ======================
51
+ # Signal Logic (Fixed Thresholds)
52
  # ======================
53
 
54
+ def generate_signals(df):
 
55
  df['RSI'] = calculate_rsi(df)
56
  df['MiddleBB'], df['UpperBB'], df['LowerBB'] = calculate_bollinger_bands(df)
57
  df['SlowK'], df['SlowD'] = calculate_stochastic_oscillator(df)
58
  df['CMF'] = calculate_cmf(df)
59
 
60
+ t = THRESHOLDS
61
+ df['RSI_Signal'] = np.where(df['RSI'] < t['RSI_lower'], 1,
62
+ np.where(df['RSI'] > t['RSI_upper'], -1, 0))
63
+
64
+ bb_buffer = t['BB'] / 100
65
+ below = df['Close'] < df['LowerBB'] * (1 - bb_buffer)
66
+ above = df['Close'] > df['UpperBB'] * (1 + bb_buffer)
67
+ df['BB_Signal'] = np.where(below & below.shift(1), 1,
68
+ np.where(above & above.shift(1), -1, 0))
69
+
70
+ df['Stochastic_Signal'] = np.where((df['SlowK'] < t['Stochastic_lower']) & (df['SlowD'] < t['Stochastic_lower']), 1,
71
+ np.where((df['SlowK'] > t['Stochastic_upper']) & (df['SlowD'] > t['Stochastic_upper']), -1, 0))
72
+
73
+ df['CMF_Signal'] = np.where(df['CMF'] < -t['CMF'], 1,
74
+ np.where(df['CMF'] > t['CMF'], -1, 0))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  return df
76
 
77
  # ======================
78
+ # Ultra-Minimal Plot
79
  # ======================
80
 
81
+ def plot_chart(data_dict, show_bollinger, time_range):
82
  fig = go.Figure()
83
+
84
+ # Determine date range
85
+ all_dates = pd.concat([df.index.to_series() for df in data_dict.values()], ignore_index=True)
86
+ if all_dates.empty:
87
+ return fig
88
+ end = all_dates.max()
89
+ start = {
90
+ "1M": end - pd.DateOffset(months=1),
91
+ "3M": end - pd.DateOffset(months=3),
92
+ "6M": end - pd.DateOffset(months=6),
93
+ "1Y": end - pd.DateOffset(years=1),
94
+ "YTD": pd.to_datetime(f"{end.year}-01-01"),
95
+ "All": all_dates.min()
96
+ }[time_range]
97
+
98
+ buy_points = []
99
+ sell_points = []
100
+
101
+ for i, (ticker, df) in enumerate(data_dict.items()):
102
+ df = df[df.index >= start]
103
+ if df.empty:
104
+ continue
105
+ color = COLORS[i % len(COLORS)]
106
+
107
+ # Price line
108
  fig.add_trace(go.Scatter(
109
+ x=df.index, y=df['Close'],
 
110
  mode='lines',
111
+ line=dict(color=color, width=1.8),
112
+ name=ticker,
113
+ showlegend=True
114
  ))
115
 
116
+ # Optional Bollinger Bands (gray, subtle)
117
+ if show_bollinger:
118
  fig.add_trace(go.Scatter(
119
  x=df.index, y=df['UpperBB'],
120
+ mode='lines', line=dict(color='rgba(150,150,150,0.4)', width=1, dash='dot'),
121
+ showlegend=False, hoverinfo='skip'
 
 
 
122
  ))
123
  fig.add_trace(go.Scatter(
124
  x=df.index, y=df['LowerBB'],
125
+ mode='lines', line=dict(color='rgba(150,150,150,0.4)', width=1, dash='dot'),
126
+ fill='tonexty', fillcolor='rgba(150,150,150,0.05)',
127
+ showlegend=False, hoverinfo='skip'
 
 
 
 
128
  ))
129
 
130
+ # Collect signals
 
131
  for date in df.index:
132
+ signals = [
133
+ ('RSI', df.loc[date, 'RSI_Signal']),
134
+ ('BB', df.loc[date, 'BB_Signal']),
135
+ ('Stochastic', df.loc[date, 'Stochastic_Signal']),
136
+ ('CMF', df.loc[date, 'CMF_Signal'])
137
+ ]
138
+ total = sum(sig for _, sig in signals)
139
+ price = df.loc[date, 'Close']
140
+ active = [name for name, sig in signals if sig == (1 if total > 0 else -1)]
141
+ if total > 0:
142
+ hover = f"<b>{ticker}</b><br>Buy: {', '.join(active)}<br>{date.strftime('%Y-%m-%d')}<br>${price:.2f}"
143
+ buy_points.append((date, price * 0.997, hover))
144
+ elif total < 0:
145
+ hover = f"<b>{ticker}</b><br>Sell: {', '.join(active)}<br>{date.strftime('%Y-%m-%d')}<br>${price:.2f}"
146
+ sell_points.append((date, price * 1.003, hover))
147
+
148
+ # White ▲ Buy
149
+ if buy_points:
150
+ x, y, text = zip(*buy_points)
151
  fig.add_trace(go.Scatter(
152
  x=x, y=y,
153
  mode='markers',
154
+ marker=dict(symbol='triangle-up', size=9, color='white', line=dict(color='black', width=0.8)),
 
 
155
  hovertext=text,
156
+ hoverinfo='text',
157
+ showlegend=False
158
  ))
159
 
160
+ # Black Sell
161
+ if sell_points:
162
+ x, y, text = zip(*sell_points)
163
  fig.add_trace(go.Scatter(
164
  x=x, y=y,
165
  mode='markers',
166
+ marker=dict(symbol='triangle-down', size=9, color='black', line=dict(color='white', width=0.8)),
 
 
167
  hovertext=text,
168
+ hoverinfo='text',
169
+ showlegend=False
170
  ))
171
 
172
+ # Ultra-minimal black theme
173
  fig.update_layout(
174
+ xaxis=dict(showgrid=False, zeroline=False, showline=False, ticks='', showticklabels=True),
175
+ yaxis=dict(showgrid=False, zeroline=False, showline=False, ticks='', showticklabels=True, tickprefix='$'),
176
+ plot_bgcolor='black',
177
+ paper_bgcolor='black',
178
+ font=dict(color='white', size=12),
179
+ showlegend=True,
180
  legend=dict(
181
  orientation='h',
182
  yanchor='bottom',
183
  y=1.02,
184
  xanchor='center',
185
  x=0.5,
186
+ bgcolor='rgba(0,0,0,0.6)',
187
+ font=dict(size=11)
188
  ),
189
+ margin=dict(l=20, r=20, t=30, b=30),
190
+ height=700,
191
+ width=1100,
192
+ hovermode='x unified'
193
  )
 
194
  return fig
195
 
196
  # ======================
197
+ # Main Function
198
  # ======================
199
 
200
+ def run_analysis(tickers_str, start_date, end_date, show_bollinger, time_range):
 
 
 
 
 
 
 
 
 
 
 
201
  try:
202
+ tickers = [t.strip().upper() for t in tickers_str.split(',') if t.strip()][:8]
203
  if not tickers:
204
+ raise ValueError("Enter at least one ticker symbol")
205
 
206
+ data = {}
 
 
 
 
 
 
 
 
 
 
207
  for t in tickers:
208
  df = yf.download(t, start=start_date, end=end_date)
209
+ if not df.empty:
210
+ if isinstance(df.columns, pd.MultiIndex):
211
+ df.columns = df.columns.droplevel(1)
212
+ data[t] = generate_signals(df)
 
 
 
213
 
214
+ if not data:
215
+ raise ValueError("No data found for the given tickers and date range")
216
 
217
+ return plot_chart(data, show_bollinger, time_range)
218
 
219
  except Exception as e:
220
  fig = go.Figure()
221
+ fig.add_annotation(text=f"Error: {str(e)}", x=0.5, y=0.5, showarrow=False, font=dict(color="red", size=16))
222
+ fig.update_layout(plot_bgcolor='black', paper_bgcolor='black', height=700, width=1100)
 
 
 
 
 
 
 
 
 
 
223
  return fig
224
 
225
  # ======================
226
+ # Gradio UI (No Threshold Sliders!)
227
  # ======================
228
 
229
+ with gr.Blocks(theme=gr.themes.Default()) as demo:
230
+ gr.Markdown("### 🔍 Strict Multi-Ticker Signal Viewer (Fixed High Thresholds)")
231
+
 
 
 
 
 
 
 
 
 
232
  with gr.Row():
233
  with gr.Column(scale=1):
234
+ tickers_input = gr.Textbox(
235
  label="Tickers (comma-separated, max 8)",
236
+ value="NVDA, AAPL, MSFT, TSLA"
 
237
  )
238
+ start_input = gr.Textbox(label="Start Date", value="2022-01-01")
239
+ end_input = gr.Textbox(label="End Date", value="2026-01-01")
240
+
241
+ show_bb = gr.Checkbox(label="Show Bollinger Bands", value=False)
242
+ time_range = gr.Radio(
243
+ choices=["1M", "3M", "6M", "1Y", "YTD", "All"],
244
+ value="1Y",
245
+ label="Time Range"
246
+ )
247
+
248
+ btn = gr.Button("Analyze", variant="primary")
249
+
250
+ chart = gr.Plot()
251
+
252
+ btn.click(
253
+ run_analysis,
254
+ inputs=[tickers_input, start_input, end_input, show_bb, time_range],
255
+ outputs=chart
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  )
257
 
258
  gr.Markdown("""
259
+ - **▲ White** = Buy | **▼ Black** = Sell
260
+ - Indicators: **RSI, Bollinger Bands, Stochastic, CMF**
261
+ - Thresholds are **fixed and strict** (e.g., RSI buy < 15, sell > 85)
262
+ - Toggle Bollinger Bands for context
263
+ - Hover over signals to see details
264
  """)
265
 
 
266
  if __name__ == "__main__":
267
  demo.launch()