File size: 15,763 Bytes
e69a18d
99c5ed6
 
e69a18d
41fda59
e69a18d
 
4f6ae5e
22b93f7
 
a3e24e1
41fda59
 
d07bded
 
99c5ed6
4f6ae5e
e69a18d
4f6ae5e
e69a18d
 
4f6ae5e
 
 
bbf3501
4f6ae5e
 
bbf3501
4f6ae5e
 
 
 
 
 
41fda59
 
 
 
 
 
 
 
 
 
 
 
22b93f7
 
 
 
 
 
 
41fda59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3994932
22b93f7
 
 
 
 
 
f4079e2
3994932
22b93f7
 
3994932
f4079e2
 
3994932
f4079e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3994932
f4079e2
 
 
 
 
22b93f7
4f6ae5e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbf3501
4f6ae5e
 
 
 
 
 
 
f4079e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41fda59
f4079e2
 
 
 
41fda59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4f6ae5e
 
 
 
 
 
 
 
 
 
 
0d0cec0
 
a07c1a8
0d0cec0
 
c5a48ec
 
 
0d0cec0
 
c5a48ec
 
 
 
 
 
 
 
 
 
 
 
 
 
0d0cec0
c5a48ec
 
a07c1a8
c5a48ec
 
 
 
 
a07c1a8
c5a48ec
 
 
 
 
 
a07c1a8
c5a48ec
 
 
 
a07c1a8
 
 
 
c5a48ec
 
 
a07c1a8
 
c5a48ec
 
a07c1a8
 
 
 
c5a48ec
 
 
 
 
0d0cec0
a3e24e1
41fda59
 
 
 
 
 
a3e24e1
41fda59
 
 
a3e24e1
 
41fda59
 
a3e24e1
 
41fda59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a3e24e1
41fda59
a3e24e1
22b93f7
 
 
 
d07bded
22b93f7
 
4f6ae5e
d07bded
22b93f7
 
4f6ae5e
d07bded
22b93f7
41fda59
d07bded
 
 
 
4f6ae5e
d07bded
a3e24e1
 
 
d07bded
 
 
 
a3e24e1
d07bded
 
 
 
 
 
 
 
 
 
be3d7a9
 
 
41fda59
be3d7a9
 
 
 
 
 
 
41fda59
be3d7a9
 
 
 
d07bded
be3d7a9
 
 
d07bded
be3d7a9
 
 
 
 
 
41fda59
 
4f6ae5e
 
be3d7a9
4f6ae5e
c5a48ec
41fda59
4f6ae5e
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
import logging
import gradio as gr
import pandas as pd
import torch
import numpy as np
from GoogleNews import GoogleNews
from transformers import pipeline
import yfinance as yf
import requests
from fuzzywuzzy import process
import statistics
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
import warnings
warnings.filterwarnings("ignore", category=UserWarning, module="fuzzywuzzy")

# Set up logging
logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)

SENTIMENT_ANALYSIS_MODEL = (
    "mrm8488/distilroberta-finetuned-financial-news-sentiment-analysis"
)

DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
logging.info(f"Using device: {DEVICE}")

logging.info("Initializing sentiment analysis model...")
sentiment_analyzer = pipeline(
    "sentiment-analysis", model=SENTIMENT_ANALYSIS_MODEL, device=DEVICE
)
logging.info("Model initialized successfully")

# Technical Analysis Parameters
TA_CONFIG = {
    'rsi_window': 14,
    'macd_fast': 12,
    'macd_slow': 26,
    'macd_signal': 9,
    'bollinger_window': 20,
    'sma_windows': [20, 50, 200],
    'ema_windows': [12, 26],
    'volatility_window': 30
}

EXCHANGE_SUFFIXES = {
    "NSE": ".NS",
    "BSE": ".BO",
    "NYSE": "",
    "NASDAQ": "",
}

def calculate_technical_indicators(history):
    """Calculate various technical indicators from historical price data"""
    ta_results = {}
    
    # RSI
    delta = history['Close'].diff()
    gain = delta.where(delta > 0, 0)
    loss = -delta.where(delta < 0, 0)
    
    avg_gain = gain.rolling(TA_CONFIG['rsi_window']).mean()
    avg_loss = loss.rolling(TA_CONFIG['rsi_window']).mean()
    rs = avg_gain / avg_loss
    ta_results['rsi'] = 100 - (100 / (1 + rs)).iloc[-1]
    
    # MACD
    ema_fast = history['Close'].ewm(span=TA_CONFIG['macd_fast'], adjust=False).mean()
    ema_slow = history['Close'].ewm(span=TA_CONFIG['macd_slow'], adjust=False).mean()
    macd = ema_fast - ema_slow
    signal = macd.ewm(span=TA_CONFIG['macd_signal'], adjust=False).mean()
    ta_results['macd'] = macd.iloc[-1]
    ta_results['macd_signal'] = signal.iloc[-1]
    
    # Bollinger Bands
    sma = history['Close'].rolling(TA_CONFIG['bollinger_window']).mean()
    std = history['Close'].rolling(TA_CONFIG['bollinger_window']).std()
    ta_results['bollinger_upper'] = (sma + 2 * std).iloc[-1]
    ta_results['bollinger_lower'] = (sma - 2 * std).iloc[-1]
    
    # Moving Averages
    for window in TA_CONFIG['sma_windows']:
        ta_results[f'sma_{window}'] = history['Close'].rolling(window).mean().iloc[-1]
    for window in TA_CONFIG['ema_windows']:
        ta_results[f'ema_{window}'] = history['Close'].ewm(span=window, adjust=False).mean().iloc[-1]
    
    # Volatility
    returns = history['Close'].pct_change().dropna()
    ta_results['volatility_30d'] = returns.rolling(TA_CONFIG['volatility_window']).std().iloc[-1] * np.sqrt(252)
    
    return ta_results

def generate_price_chart(history):
    """Generate interactive price chart with technical indicators"""
    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
    
    # Price and Moving Averages
    history['Close'].plot(ax=ax1, label='Price')
    for window in TA_CONFIG['sma_windows']:
        history['Close'].rolling(window).mean().plot(ax=ax1, label=f'SMA {window}')
    ax1.set_title('Price and Moving Averages')
    ax1.legend()
    
    # RSI
    delta = history['Close'].diff()
    gain = delta.where(delta > 0, 0)
    loss = -delta.where(delta < 0, 0)
    avg_gain = gain.rolling(TA_CONFIG['rsi_window']).mean()
    avg_loss = loss.rolling(TA_CONFIG['rsi_window']).mean()
    rs = avg_gain / avg_loss
    rsi = 100 - (100 / (1 + rs))
    
    rsi.plot(ax=ax2, label='RSI')
    ax2.axhline(70, color='red', linestyle='--')
    ax2.axhline(30, color='green', linestyle='--')
    ax2.set_title('Relative Strength Index (RSI)')
    ax2.legend()
    
    plt.tight_layout()
    return fig

def resolve_ticker_symbol(query: str) -> str:
    """
    Convert company names/partial symbols to valid Yahoo Finance tickers.
    Example: "Kalyan Jewellers" → "KALYANKJIL.NS"
    """
    url = "https://query2.finance.yahoo.com/v1/finance/search"
    headers = {"User-Agent": "Mozilla/5.0"}  # Avoid blocking
    params = {"q": query, "quotesCount": 5, "country": "India"}

    response = requests.get(url, headers=headers, params=params)
    data = response.json()

    if not data.get("quotes"):
        raise ValueError(f"No ticker found for: {query}")

    # Extract quotes data
    quotes = data["quotes"]
    tickers = [quote["symbol"] for quote in quotes]
    names = [quote.get("longname") or quote.get("shortname", "") for quote in quotes]

    # Fuzzy match the query with company names
    best_match, score = process.extractOne(query, names)
    if not best_match:
        raise ValueError(f"No matching ticker found for: {query}")

    index = names.index(best_match)
    best_quote = quotes[index]
    resolved_ticker = best_quote["symbol"]
    exchange_code = best_quote.get("exchange", "").upper()

    # Map exchange codes to suffixes
    exchange_suffix_map = {
        "NSI": ".NS",  # NSE
        "BOM": ".BO",  # BSE
        "BSE": ".BO",
        "NSE": ".NS",
    }
    suffix = exchange_suffix_map.get(exchange_code, ".NS")  # Default to NSE

    # Append suffix only if not already present
    if not resolved_ticker.endswith(suffix):
        resolved_ticker += suffix

    return resolved_ticker

def fetch_articles(query):
    try:
        logging.info(f"Fetching articles for query: '{query}'")
        googlenews = GoogleNews(lang="en")
        googlenews.search(query)
        articles = googlenews.result()
        logging.info(f"Fetched {len(articles)} articles")
        return articles
    except Exception as e:
        logging.error(
            f"Error while searching articles for query: '{query}'. Error: {e}"
        )
        raise gr.Error(
            f"Unable to search articles for query: '{query}'. Try again later...",
            duration=5,
        )

def analyze_article_sentiment(article):
    logging.info(f"Analyzing sentiment for article: {article['title']}")
    sentiment = sentiment_analyzer(article["desc"])[0]
    article["sentiment"] = sentiment
    return article

def fetch_yfinance_data(ticker):
    """Enhanced Yahoo Finance data fetching with technical analysis"""
    try:
        logging.info(f"Fetching Yahoo Finance data for: {ticker}")
        stock = yf.Ticker(ticker)
        history = stock.history(period="1y", interval="1d")

        if history.empty:
            logging.error(f"No data found for {ticker}")
            return {"error": f"No data found for {ticker}"}

        # Calculate technical indicators
        ta_data = calculate_technical_indicators(history)

        # Current price data
        current_price = history['Close'].iloc[-1]
        prev_close = history['Close'].iloc[-2] if len(history) > 1 else 0
        price_change = current_price - prev_close
        percent_change = (price_change / prev_close) * 100 if prev_close != 0 else 0

        # Generate price chart
        chart = generate_price_chart(history[-120:])  # Last 120 days

        return {
            'current_price': current_price,
            'price_change': price_change,
            'percent_change': percent_change,
            'chart': chart,
            'technical_indicators': ta_data,
            'fundamentals': stock.info
        }

    except Exception as e:
        logging.error(f"Error fetching Yahoo Finance data for {ticker}: {str(e)}")
        return {"error": f"Failed to fetch data for {ticker}: {str(e)}"}
    
def time_weighted_sentiment(articles):
    """Apply time-based weighting to sentiment scores"""
    now = datetime.now()
    weighted_scores = []
    
    for article in articles:
        try:
            article_date = datetime.strptime(article['date'], '%Y-%m-%d %H:%M:%S')
            days_old = (now - article_date).days
            weight = max(0, 1 - (days_old / 7))  # Linear decay over 7 days
        except:
            weight = 0.5  # Default weight if date parsing fails
            
        sentiment = article['sentiment']['label']
        score = 1 if sentiment == 'positive' else -1 if sentiment == 'negative' else 0
        weighted_scores.append(score * weight)
    
    return weighted_scores

def _format_number(num):
    """Helper to format large numbers with suffixes"""
    if isinstance(num, (int, float)):
        for unit in ['','K','M','B','T']:
            if abs(num) < 1000:
                return f"{num:,.2f}{unit}"
            num /= 1000
        return f"{num:,.2f}P"
    return num

def convert_to_dataframe(analyzed_articles):
    df = pd.DataFrame(analyzed_articles)

    def sentiment_badge(sentiment):
        colors = {
            "negative": "#ef4444",
            "neutral": "#64748b",
            "positive": "#22c55e",
        }
        color = colors.get(sentiment, "grey")
        return (
            f'<div style="display: inline-flex; align-items: center; gap: 0.5rem;">'
            f'<div style="width: 0.75rem; height: 0.75rem; background-color: {color}; border-radius: 50%;"></div>'
            f'<span style="text-transform: capitalize; font-weight: 500; color: {color}">{sentiment}</span>'
            f'</div>'
        )

    df["Sentiment"] = df["sentiment"].apply(lambda x: sentiment_badge(x["label"].lower()))
    df["Title"] = df.apply(
        lambda row: f'<a href="{row["link"]}" target="_blank" style="text-decoration: none; color: #2563eb;">{row["title"]}</a>',
        axis=1,
    )
    df["Description"] = df["desc"].apply(lambda x: f'<div style="font-size: 0.9rem; color: #4b5563;">{x}</div>')
    df["Date"] = df["date"].apply(lambda x: f'<div style="font-size: 0.8rem; color: #6b7280;">{x}</div>')

    # Convert to HTML table
    html_table = df[["Sentiment", "Title", "Description", "Date"]].to_html(
        escape=False,
        index=False,
        border=0,
        classes="gradio-table",
        justify="start"
    )

    # Add custom styling
    styled_html = f"""
    <style>
    .gradio-table {{
        width: 100%;
        border-collapse: collapse;
        margin-bottom: 1rem;
    }}
    .gradio-table th {{
        text-align: left;
        padding: 0.75rem;
        background-color: #f3f4f6;
        border-bottom: 2px solid #d1d5db;
        color: #1f2937;
        font-weight: 600;
    }}
    .gradio-table td {{
        padding: 0.75rem;
        border-bottom: 1px solid #e5e7eb;
        background-color: #ffffff;
    }}
    .gradio-table tr:hover td {{
        background-color: #f9fafb;
    }}
    .gradio-table tr:nth-child(even) td {{
        background-color: #f9fafb;
    }}
    </style>
    {html_table}
    """
    return styled_html

def generate_stock_recommendation(articles, finance_data):
    """Enhanced recommendation system with technical analysis"""
    # Time-weighted sentiment analysis
    sentiment_scores = time_weighted_sentiment(articles)
    positive_score = sum(s for s in sentiment_scores if s > 0)
    negative_score = abs(sum(s for s in sentiment_scores if s < 0))
    total_score = positive_score - negative_score
    
    # Technical indicators
    ta = finance_data.get('technical_indicators', {})
    rec = {
        'recommendation': 'HOLD',
        'confidence': 'Medium',
        'reasons': [],
        'risk_factors': []
    }
    
    # Sentiment-based factors
    if total_score > 3:
        rec['recommendation'] = 'BUY'
        rec['reasons'].append("Strong positive sentiment trend")
    elif total_score < -3:
        rec['recommendation'] = 'SELL'
        rec['reasons'].append("Significant negative sentiment")
        
    # Technical analysis factors
    if ta.get('rsi', 50) > 70:
        rec['risk_factors'].append("RSI indicates overbought condition")
    elif ta.get('rsi', 50) < 30:
        rec['reasons'].append("RSI suggests oversold opportunity")
        
    if ta.get('macd', 0) > ta.get('macd_signal', 0):
        rec['reasons'].append("Bullish MACD crossover")
    else:
        rec['risk_factors'].append("Bearish MACD trend")
        
    # Volatility analysis
    if ta.get('volatility_30d', 0) > 0.4:
        rec['risk_factors'].append("High volatility detected")
        
    # Combine factors
    if len(rec['reasons']) > len(rec['risk_factors']):
        rec['confidence'] = 'High'
    elif len(rec['risk_factors']) > 2:
        rec['recommendation'] = 'SELL' if rec['recommendation'] == 'HOLD' else rec['recommendation']
        rec['confidence'] = 'Low'
        
    # Format output
    output = f"Recommendation: {rec['recommendation']} ({rec['confidence']} Confidence)\n\n"
    output += "Supporting Factors:\n" + "\n".join(f"- {r}" for r in rec['reasons']) + "\n\n"
    output += "Risk Factors:\n" + "\n".join(f"- {r}" for r in rec['risk_factors']) + "\n\n"
    output += f"Sentiment Score: {total_score:.2f}\n"
    output += f"30-Day Volatility: {ta.get('volatility_30d', 0):.2%}"
    
    return output

def analyze_asset_sentiment(asset_input):
    logging.info(f"Starting sentiment analysis for asset: {asset_input}")

    try:
        # Resolve ticker symbol
        ticker = resolve_ticker_symbol(asset_input)
        logging.info(f"Resolved '{asset_input}' to ticker: {ticker}")

        # Fetch and analyze articles
        articles = fetch_articles(asset_input)
        analyzed_articles = [analyze_article_sentiment(article) for article in articles]

        # Fetch financial data and technical indicators
        finance_data = fetch_yfinance_data(ticker)
        
        # Extract chart and ensure it's removed from financial data
        price_chart = finance_data.get('chart')
        if 'chart' in finance_data:
            del finance_data['chart']

        # Generate recommendation
        recommendation = generate_stock_recommendation(analyzed_articles, finance_data)

        return (
            convert_to_dataframe(analyzed_articles),  # Articles dataframe
            finance_data,                             # Financial data (without chart)
            recommendation,                           # Text recommendation
            price_chart                               # Matplotlib figure
        )

    except Exception as e:
        logging.error(f"Error in analysis: {str(e)}")
        return (
            pd.DataFrame(), 
            {"error": str(e)}, 
            "Analysis failed", 
            None
        )
        
# Update the Gradio interface (change the output component type)
with gr.Blocks(theme=gr.themes.Default()) as iface:
    gr.Markdown("# Advanced Trading Analytics Suite")
    
    with gr.Row():
        input_asset = gr.Textbox(
            label="Asset Name/Ticker",
            placeholder="Enter stock name or symbol...",
            max_lines=1
        )
        analyze_btn = gr.Button("Analyze", variant="primary")
    
    with gr.Tabs():
        with gr.TabItem("Sentiment Analysis"):
            gr.Markdown("## News Sentiment Analysis")
            articles_output = gr.HTML(label="Analyzed News Articles")  # Changed to HTML component

        with gr.TabItem("Technical Analysis"):
            price_chart = gr.Plot(label="Price Analysis")
            ta_json = gr.JSON(label="Technical Indicators")

        with gr.TabItem("Recommendation"):
            recommendation_output = gr.Textbox(
                lines=8,
                label="Analysis Summary",
                interactive=False
            )
    
    analyze_btn.click(
        analyze_asset_sentiment,
        inputs=[input_asset],
        outputs=[articles_output, ta_json, recommendation_output, price_chart]
    )
    
logging.info("Launching enhanced Gradio interface")
iface.queue().launch()