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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +203 -384
app.py CHANGED
@@ -2,21 +2,15 @@ import gradio as gr
2
  import pandas as pd
3
  import yfinance as yf
4
  import plotly.graph_objects as go
 
5
  import numpy as np
6
 
7
- # Functions for calculating indicators
8
- def calculate_sma(df, window):
9
- return df['Close'].rolling(window=window).mean()
10
 
11
- def calculate_ema(df, window):
12
- return df['Close'].ewm(span=window, adjust=False).mean()
13
-
14
- def calculate_macd(df):
15
- short_ema = df['Close'].ewm(span=12, adjust=False).mean()
16
- long_ema = df['Close'].ewm(span=26, adjust=False).mean()
17
- macd = short_ema - long_ema
18
- signal = macd.ewm(span=9, adjust=False).mean()
19
- return macd, signal
20
 
21
  def calculate_rsi(df):
22
  delta = df['Close'].diff()
@@ -28,8 +22,9 @@ def calculate_rsi(df):
28
 
29
  def calculate_bollinger_bands(df):
30
  middle_bb = df['Close'].rolling(window=20).mean()
31
- upper_bb = middle_bb + 2 * df['Close'].rolling(window=20).std()
32
- lower_bb = middle_bb - 2 * df['Close'].rolling(window=20).std()
 
33
  return middle_bb, upper_bb, lower_bb
34
 
35
  def calculate_stochastic_oscillator(df):
@@ -44,373 +39,222 @@ def calculate_cmf(df, window=20):
44
  cmf = mfv.rolling(window=window).sum() / df['Volume'].rolling(window=window).sum()
45
  return cmf
46
 
47
- def calculate_cci(df, window=20):
48
- typical_price = (df['High'] + df['Low'] + df['Close']) / 3
49
- sma = typical_price.rolling(window=window).mean()
50
- mean_deviation = (typical_price - sma).abs().rolling(window=window).mean()
51
- cci = (typical_price - sma) / (0.015 * mean_deviation)
52
- return cci
53
-
54
- # Function to adjust thresholds based on sensitivity
55
- def adjust_thresholds_by_sensitivity(sensitivity):
56
- """
57
- Convert a single sensitivity value (1-10) to appropriate thresholds
58
- 1 = Most sensitive (more signals)
59
- 10 = Least sensitive (fewer, stronger signals)
60
- """
61
- # Map sensitivity to thresholds
62
- if sensitivity == 1: # Most sensitive
63
- return {
64
- 'SMA': 5,
65
- 'RSI_lower': 30,
66
- 'RSI_upper': 70,
67
- 'BB': 0.5,
68
- 'Stochastic_lower': 20,
69
- 'Stochastic_upper': 80,
70
- 'CMF': 0.1,
71
- 'CCI': 100
72
- }
73
- elif sensitivity == 10: # Least sensitive
74
- return {
75
- 'SMA': 50,
76
- 'RSI_lower': 5,
77
- 'RSI_upper': 95,
78
- 'BB': 5,
79
- 'Stochastic_lower': 5,
80
- 'Stochastic_upper': 95,
81
- 'CMF': 0.6,
82
- 'CCI': 300
83
- }
84
- else:
85
- # Linear interpolation between extremes
86
- factor = (sensitivity - 1) / 9 # 0 to 1
87
- return {
88
- 'SMA': int(5 + (50 - 5) * factor),
89
- 'RSI_lower': int(30 - (30 - 5) * factor),
90
- 'RSI_upper': int(70 + (95 - 70) * factor),
91
- 'BB': 0.5 + (5 - 0.5) * factor,
92
- 'Stochastic_lower': int(20 - (20 - 5) * factor),
93
- 'Stochastic_upper': int(80 + (95 - 80) * factor),
94
- 'CMF': 0.1 + (0.6 - 0.1) * factor,
95
- 'CCI': int(100 + (300 - 100) * factor)
96
- }
97
 
98
  def generate_trading_signals(df, thresholds, enabled_signals):
99
- # Calculate various indicators
100
- df['SMA_30'] = calculate_sma(df, 30)
101
- df['SMA_100'] = calculate_sma(df, 100)
102
- df['EMA_12'] = calculate_ema(df, 12)
103
- df['EMA_26'] = calculate_ema(df, 26)
104
  df['RSI'] = calculate_rsi(df)
105
  df['MiddleBB'], df['UpperBB'], df['LowerBB'] = calculate_bollinger_bands(df)
106
  df['SlowK'], df['SlowD'] = calculate_stochastic_oscillator(df)
107
  df['CMF'] = calculate_cmf(df)
108
- df['CCI'] = calculate_cci(df)
109
 
110
- # Initialize all signals as 0 (no signal)
111
- signal_columns = ['SMA_Signal', 'MACD_Signal', 'RSI_Signal', 'BB_Signal',
112
- 'Stochastic_Signal', 'CMF_Signal', 'CCI_Signal']
113
- for col in signal_columns:
114
  df[col] = 0
115
-
116
- # Only generate signals for enabled indicators
117
-
118
- # SMA Signal
119
- if 'SMA' in enabled_signals:
120
- sma_threshold = thresholds['SMA']
121
- df['SMA_Diff_Pct'] = (df['SMA_30'] - df['SMA_100']) / df['SMA_100'] * 100
122
- df['SMA_Signal'] = np.where(df['SMA_Diff_Pct'] > sma_threshold, 1, 0)
123
- df['SMA_Signal'] = np.where(df['SMA_Diff_Pct'] < -sma_threshold, -1, df['SMA_Signal'])
124
-
125
- # MACD Signal
126
- if 'MACD' in enabled_signals:
127
- macd, signal = calculate_macd(df)
128
- df['MACD'] = macd
129
- df['MACD_Signal_Line'] = signal
130
- df['MACD_Signal'] = np.select([(macd > signal) & (macd.shift(1) <= signal.shift(1)),
131
- (macd < signal) & (macd.shift(1) >= signal.shift(1))], [1, -1], default=0)
132
-
133
- # RSI Signals
134
  if 'RSI' in enabled_signals:
135
  rsi_lower = thresholds['RSI_lower']
136
  rsi_upper = thresholds['RSI_upper']
137
  df['RSI_Signal'] = np.where(df['RSI'] < rsi_lower, 1, 0)
138
  df['RSI_Signal'] = np.where(df['RSI'] > rsi_upper, -1, df['RSI_Signal'])
139
-
140
- # Bollinger Bands
141
  if 'BB' in enabled_signals:
142
- bb_buffer = thresholds['BB'] / 100 # Convert percentage to decimal
143
- df['BB_Signal'] = np.where(
144
- (df['Close'] < df['LowerBB'] * (1 - bb_buffer)) &
145
- (df['Close'].shift(1) < df['LowerBB'].shift(1) * (1 - bb_buffer)) &
146
- (df['Close'].shift(2) < df['LowerBB'].shift(2) * (1 - bb_buffer)), 1, 0
147
- )
148
- df['BB_Signal'] = np.where(
149
- (df['Close'] > df['UpperBB'] * (1 + bb_buffer)) &
150
- (df['Close'].shift(1) > df['UpperBB'].shift(1) * (1 + bb_buffer)) &
151
- (df['Close'].shift(2) > df['UpperBB'].shift(2) * (1 + bb_buffer)), -1, df['BB_Signal']
152
- )
153
-
154
- # Stochastic signals
155
  if 'Stochastic' in enabled_signals:
156
  stoch_lower = thresholds['Stochastic_lower']
157
  stoch_upper = thresholds['Stochastic_upper']
158
  df['Stochastic_Signal'] = np.where((df['SlowK'] < stoch_lower) & (df['SlowD'] < stoch_lower), 1, 0)
159
  df['Stochastic_Signal'] = np.where((df['SlowK'] > stoch_upper) & (df['SlowD'] > stoch_upper), -1, df['Stochastic_Signal'])
160
-
161
- # CMF Signals
162
  if 'CMF' in enabled_signals:
163
- cmf_threshold = thresholds['CMF']
164
- df['CMF_Signal'] = np.where(df['CMF'] > cmf_threshold, -1, np.where(df['CMF'] < -cmf_threshold, 1, 0))
165
-
166
- # CCI Signals
167
- if 'CCI' in enabled_signals:
168
- cci_threshold = thresholds['CCI']
169
- df['CCI_Signal'] = np.where(df['CCI'] < -cci_threshold, 1, 0)
170
- df['CCI_Signal'] = np.where(df['CCI'] > cci_threshold, -1, df['CCI_Signal'])
171
 
172
  return df
173
 
174
- def plot_simplified_signals(df, ticker, enabled_signals):
175
- # Create a figure with improved styling
 
 
 
176
  fig = go.Figure()
177
-
178
- # Use a line chart instead of candlestick for simplicity
179
- fig.add_trace(go.Scatter(
180
- x=df.index,
181
- y=df['Close'],
182
- mode='lines',
183
- name='Price',
184
- line=dict(color='#26a69a', width=2),
185
- opacity=0.9
186
- ))
187
-
188
- # Add SMA lines
189
- fig.add_trace(go.Scatter(
190
- x=df.index, y=df['SMA_30'],
191
- mode='lines',
192
- name='SMA 30',
193
- line=dict(color='#42a5f5', width=1.5, dash='dot')
194
- ))
195
-
196
- fig.add_trace(go.Scatter(
197
- x=df.index, y=df['SMA_100'],
198
- mode='lines',
199
- name='SMA 100',
200
- line=dict(color='#5e35b1', width=1.5, dash='dot')
201
- ))
202
-
203
- # Add bollinger bands with lighter appearance
204
- if 'BB' in enabled_signals:
205
  fig.add_trace(go.Scatter(
206
- x=df.index, y=df['UpperBB'],
 
207
  mode='lines',
208
- name='Upper BB',
209
- line=dict(color='rgba(250, 250, 250, 0.3)', width=1),
210
- showlegend=True
211
- ))
212
-
213
- fig.add_trace(go.Scatter(
214
- x=df.index, y=df['LowerBB'],
215
- mode='lines',
216
- name='Lower BB',
217
- line=dict(color='rgba(250, 250, 250, 0.3)', width=1),
218
- fill='tonexty',
219
- fillcolor='rgba(173, 216, 230, 0.1)',
220
- showlegend=True
221
  ))
222
 
223
- # Group signals by type to reduce legend clutter
224
- buy_signals_df = pd.DataFrame(index=df.index)
225
- sell_signals_df = pd.DataFrame(index=df.index)
226
-
227
- signal_names = [f"{signal}_Signal" for signal in enabled_signals]
228
-
229
- # Collect all buy and sell signals
230
- for signal in signal_names:
231
- buy_signals_df[signal] = np.where(df[signal] == 1, df['Close'], np.nan)
232
- sell_signals_df[signal] = np.where(df[signal] == -1, df['Close'], np.nan)
233
-
234
- # Add hover data
235
- buy_hovers = []
236
- for idx in buy_signals_df.index:
237
- signals_on_day = [col.split('_')[0] for col in buy_signals_df.columns
238
- if not pd.isna(buy_signals_df.loc[idx, col])]
239
- if signals_on_day:
240
- hover_text = f"Buy Signals: {', '.join(signals_on_day)}<br>Date: {idx.strftime('%Y-%m-%d')}<br>Price: ${df.loc[idx, 'Close']:.2f}"
241
- buy_hovers.append((idx, df.loc[idx, 'Close'], hover_text))
242
-
243
- sell_hovers = []
244
- for idx in sell_signals_df.index:
245
- signals_on_day = [col.split('_')[0] for col in sell_signals_df.columns
246
- if not pd.isna(sell_signals_df.loc[idx, col])]
247
- if signals_on_day:
248
- hover_text = f"Sell Signals: {', '.join(signals_on_day)}<br>Date: {idx.strftime('%Y-%m-%d')}<br>Price: ${df.loc[idx, 'Close']:.2f}"
249
- sell_hovers.append((idx, df.loc[idx, 'Close'], hover_text))
250
-
251
- # Add buy signals (single trace for all buy signals)
252
- if buy_hovers:
253
- buy_x, buy_y, buy_texts = zip(*buy_hovers)
 
 
 
 
 
 
254
  fig.add_trace(go.Scatter(
255
- x=buy_x,
256
- y=[y * 0.995 for y in buy_y], # Position slightly below price for visibility
257
- mode='markers',
258
- marker=dict(symbol='triangle-up', size=10, color='#00e676', line=dict(color='white', width=1)),
259
  name='Buy Signals',
260
  hoverinfo='text',
261
- hovertext=buy_texts
 
262
  ))
263
 
264
- # Add sell signals (single trace for all sell signals)
265
- if sell_hovers:
266
- sell_x, sell_y, sell_texts = zip(*sell_hovers)
267
  fig.add_trace(go.Scatter(
268
- x=sell_x,
269
- y=[y * 1.005 for y in sell_y], # Position slightly above price for visibility
270
- mode='markers',
271
- marker=dict(symbol='triangle-down', size=10, color='#ff5252', line=dict(color='white', width=1)),
272
  name='Sell Signals',
273
  hoverinfo='text',
274
- hovertext=sell_texts
 
275
  ))
276
 
277
- # Improve the layout with larger dimensions
278
  fig.update_layout(
279
- title=dict(
280
- text=f'{ticker}: Technical Analysis & Trading Signals',
281
- font=dict(size=24, color='white'),
282
- x=0.5
283
- ),
284
- xaxis=dict(
285
- title='Date',
286
- gridcolor='rgba(255, 255, 255, 0.1)',
287
- linecolor='rgba(255, 255, 255, 0.2)'
288
- ),
289
- yaxis=dict(
290
- title='Price',
291
- side='right',
292
- gridcolor='rgba(255, 255, 255, 0.1)',
293
- linecolor='rgba(255, 255, 255, 0.2)',
294
- tickprefix='$'
295
- ),
296
  plot_bgcolor='#1e1e1e',
297
  paper_bgcolor='#1e1e1e',
298
  font=dict(color='white'),
299
- hovermode='closest',
300
  legend=dict(
301
- bgcolor='rgba(30, 30, 30, 0.8)',
302
- bordercolor='rgba(255, 255, 255, 0.2)',
303
- borderwidth=1,
304
- font=dict(color='white', size=10),
305
  orientation='h',
306
  yanchor='bottom',
307
  y=1.02,
308
  xanchor='center',
309
- x=0.5
 
310
  ),
311
- margin=dict(l=50, r=50, b=100, t=100, pad=4),
312
- height=800, # Increased height
313
- width=1200 # Increased width
314
  )
315
-
316
- # Add range selector for better time navigation
317
- fig.update_xaxes(
318
- rangeslider_visible=True,
319
- rangeselector=dict(
320
- buttons=list([
321
- dict(count=1, label="1m", step="month", stepmode="backward"),
322
- dict(count=3, label="3m", step="month", stepmode="backward"),
323
- dict(count=6, label="6m", step="month", stepmode="backward"),
324
- dict(count=1, label="YTD", step="year", stepmode="todate"),
325
- dict(count=1, label="1y", step="year", stepmode="backward"),
326
- dict(step="all")
327
- ]),
328
- bgcolor='rgba(30, 30, 30, 0.8)',
329
- activecolor='#536dfe',
330
- font=dict(color='white')
331
- )
332
- )
333
-
334
  return fig
335
 
336
- def stock_analysis(ticker, start_date, end_date,
337
- sensitivity, # New simplified parameter
338
- use_sma, use_macd, use_rsi, use_bb,
339
- use_stoch, use_cmf, use_cci):
 
 
 
 
 
 
 
 
 
 
 
 
340
  try:
341
- # Download stock data from Yahoo Finance
342
- df = yf.download(ticker, start=start_date, end=end_date)
343
-
344
- # Check if data was retrieved
345
- if df.empty:
346
- fig = go.Figure()
347
- fig.add_annotation(
348
- text="No data found for this ticker and date range",
349
- xref="paper", yref="paper",
350
- x=0.5, y=0.5,
351
- showarrow=False,
352
- font=dict(color="white", size=16)
353
- )
354
- fig.update_layout(
355
- plot_bgcolor='#1e1e1e',
356
- paper_bgcolor='#1e1e1e',
357
- height=800,
358
- width=1200
359
- )
360
- return fig
361
-
362
- # If the DataFrame has a MultiIndex for columns, handle it
363
- if isinstance(df.columns, pd.MultiIndex):
364
- df.columns = df.columns.droplevel(1) if len(df.columns.levels) > 1 else df.columns
365
-
366
- # Create list of enabled signals
367
- enabled_signals = []
368
- if use_sma: enabled_signals.append('SMA')
369
- if use_macd: enabled_signals.append('MACD')
370
- if use_rsi: enabled_signals.append('RSI')
371
- if use_bb: enabled_signals.append('BB')
372
- if use_stoch: enabled_signals.append('Stochastic')
373
- if use_cmf: enabled_signals.append('CMF')
374
- if use_cci: enabled_signals.append('CCI')
375
-
376
- # If no signals are enabled, enable all by default
377
- if not enabled_signals:
378
- enabled_signals = ['SMA', 'MACD', 'RSI', 'BB', 'Stochastic', 'CMF', 'CCI']
379
-
380
- # Get thresholds from sensitivity
381
- thresholds = adjust_thresholds_by_sensitivity(sensitivity)
382
-
383
- # Generate signals
384
- df = generate_trading_signals(df, thresholds, enabled_signals)
385
-
386
- # Last 360 days for plotting (or all data if less than 360 days)
387
- df_last_360 = df.tail(min(360, len(df)))
388
-
389
- # Plot simplified signals
390
- fig = plot_simplified_signals(df_last_360, ticker, enabled_signals)
391
-
392
- return fig
393
-
394
  except Exception as e:
395
- # Create error figure
396
  fig = go.Figure()
397
  fig.add_annotation(
398
  text=f"Error: {str(e)}",
399
- xref="paper", yref="paper",
400
- x=0.5, y=0.5,
401
  showarrow=False,
402
- font=dict(color="#ff5252", size=16)
403
  )
404
  fig.update_layout(
405
  plot_bgcolor='#1e1e1e',
406
  paper_bgcolor='#1e1e1e',
407
- font=dict(color='white'),
408
  height=800,
409
  width=1200
410
  )
411
  return fig
412
 
413
- # Define Gradio interface with improved styling
 
 
 
414
  custom_theme = gr.themes.Monochrome(
415
  primary_hue="blue",
416
  secondary_hue="purple",
@@ -420,83 +264,58 @@ custom_theme = gr.themes.Monochrome(
420
  )
421
 
422
  with gr.Blocks(theme=custom_theme) as demo:
423
- gr.Markdown("# Technical Analysis")
424
- gr.Markdown("This app helps you analyze stocks with technical indicators and generates trading signals.")
425
 
426
  with gr.Row():
427
  with gr.Column(scale=1):
428
  ticker_input = gr.Textbox(
429
- label="Stock Ticker Symbol",
430
- placeholder="e.g., AAPL, NVDA, MSFT",
431
- value="NVDA"
432
- )
433
- start_date_input = gr.Textbox(
434
- label="Start Date",
435
- placeholder="YYYY-MM-DD",
436
- value="2022-01-01"
437
- )
438
- end_date_input = gr.Textbox(
439
- label="End Date",
440
- placeholder="YYYY-MM-DD",
441
- value="2026-01-01" # Updated to current date
442
  )
443
-
444
- gr.Markdown("### Choose Indicators")
445
- with gr.Row():
446
- use_sma = gr.Checkbox(label="SMA", value=True)
447
- use_macd = gr.Checkbox(label="MACD", value=True)
448
- use_rsi = gr.Checkbox(label="RSI", value=True)
449
- use_bb = gr.Checkbox(label="Bollinger", value=True)
450
- use_stoch = gr.Checkbox(label="Stochastic", value=True)
451
- use_cmf = gr.Checkbox(label="CMF", value=True)
452
- use_cci = gr.Checkbox(label="CCI", value=True)
453
-
454
- gr.Markdown("### Signal Sensitivity")
455
- with gr.Row():
456
- sensitivity = gr.Slider(
457
- label="Signal Sensitivity",
458
- minimum=1,
459
- maximum=10,
460
- step=1,
461
- value=5,
462
- info="1 = (sensitive), 10 = (strict)"
463
- )
464
-
465
- # Create a submit button with styling
466
- button = gr.Button("Analyze Stock", variant="primary")
467
-
468
- # Output: Signals plot with increased height
469
- signals_output = gr.Plot(label="Technical Analysis & Trading Signals")
470
-
471
- # Link button to function with updated parameters
472
- button.click(
473
- stock_analysis,
474
  inputs=[
475
- ticker_input, start_date_input, end_date_input,
476
- sensitivity, # Single threshold parameter
477
- use_sma, use_macd, use_rsi, use_bb,
478
- use_stoch, use_cmf, use_cci
479
- ],
480
- outputs=[signals_output]
 
 
 
 
 
 
481
  )
482
 
483
  gr.Markdown("""
484
- ## 📈 Trading Signals Legend
485
- - **Green Triangle Up (▲)** indicates Buy signals
486
- - **Red Triangle Down (▼)** indicates Sell signals
487
- - Hover over signals to see which indicators triggered them
488
-
489
- ## 🔍 Signal Sensitivity Explained
490
- - **Lower values (1-3)**: More frequent signals, good for short-term trading
491
- - **Medium values (4-6)**: Balanced approach, moderate number of signals
492
- - **Higher values (7-10)**: Fewer but potentially stronger signals, good for long-term investors
493
-
494
- ## 🛠️ Trading Strategy Tips
495
- - **Day Trading**: Use lower sensitivity with multiple indicators
496
- - **Swing Trading**: Use medium sensitivity with 3-4 indicators
497
- - **Long-term Investing**: Use higher sensitivity focusing on trend indicators
498
- - **Combine**: Using multiple indicators helps confirm signals and reduce false positives
499
  """)
500
 
501
- # Launch the interface
502
- demo.launch()
 
 
2
  import pandas as pd
3
  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):
16
  delta = df['Close'].diff()
 
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):
 
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",
 
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()