pranit144 commited on
Commit
9df2687
·
verified ·
1 Parent(s): 8b3dfff

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +260 -0
  2. requirements.txt +9 -0
  3. templates/index.html +798 -0
app.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from flask import Flask, render_template, request, jsonify, send_file
3
+ import json
4
+ import requests
5
+ import pandas as pd
6
+ import numpy as np
7
+ import io
8
+ import base64
9
+ from bs4 import BeautifulSoup
10
+ import matplotlib
11
+
12
+ matplotlib.use('Agg') # Use non-interactive backend
13
+ import matplotlib.pyplot as plt
14
+ import seaborn as sns
15
+ import plotly
16
+ import plotly.express as px
17
+ import plotly.graph_objects as go
18
+ from datetime import datetime, timedelta
19
+ import random
20
+
21
+ app = Flask(__name__)
22
+
23
+
24
+ def parse_company_essentials(html_text: str):
25
+ soup = BeautifulSoup(html_text, 'html.parser')
26
+ container = soup.find('div', id='companyessentials')
27
+ if not container:
28
+ return []
29
+ items = []
30
+ for block in container.select('#mainContent_updAddRatios .compess'):
31
+ small = block.find('small')
32
+ if not small:
33
+ continue
34
+ # extract label
35
+ label_text = ''
36
+ for elem in small.contents:
37
+ if elem.name == 'span' and 'infolink' in elem.get('class', []):
38
+ break
39
+ label_text += elem.get_text() if not isinstance(elem, str) else elem
40
+ label = label_text.strip()
41
+ # tooltip
42
+ info = small.find('span', class_='infolink')
43
+ tooltip = info.get('data-original-title', '').strip() if info else ''
44
+ # value
45
+ p = block.find('p')
46
+ value = ' '.join(p.stripped_strings) if p else ''
47
+ items.append({
48
+ "label": label,
49
+ "tooltip": tooltip,
50
+ "value": value
51
+ })
52
+ return items
53
+
54
+
55
+ def get_company_name(html_text: str):
56
+ soup = BeautifulSoup(html_text, 'html.parser')
57
+ title_element = soup.find('h1')
58
+ if title_element:
59
+ return title_element.text.strip()
60
+ return "Unknown Company"
61
+
62
+
63
+ def get_historical_data(ticker):
64
+ """
65
+ Generate sample historical data for stock prices.
66
+ In a production app, you would fetch real data from an API.
67
+ """
68
+ # Create a date range for the last 30 days
69
+ end_date = datetime.now()
70
+ start_date = end_date - timedelta(days=180)
71
+ date_range = pd.date_range(start=start_date, end=end_date, freq='B') # 'B' for business days
72
+
73
+ # Generate sample prices with a general trend and some randomness
74
+ base_price = 100 + random.randint(-20, 20) # Random starting point
75
+
76
+ # Create a trend component (upward, downward, or sideways)
77
+ trend_type = random.choice(['up', 'down', 'sideways'])
78
+ if trend_type == 'up':
79
+ trend = np.linspace(0, 15, len(date_range))
80
+ elif trend_type == 'down':
81
+ trend = np.linspace(15, 0, len(date_range))
82
+ else:
83
+ trend = np.linspace(0, 0, len(date_range))
84
+
85
+ # Add randomness
86
+ noise = np.random.normal(0, 2, len(date_range))
87
+
88
+ # Calculate prices
89
+ prices = base_price + trend + np.cumsum(noise) * 0.5
90
+
91
+ # Ensure no negative prices
92
+ prices = np.maximum(prices, 1)
93
+
94
+ # Create DataFrame
95
+ df = pd.DataFrame({
96
+ 'Date': date_range,
97
+ 'Close': prices,
98
+ 'Volume': np.random.randint(100000, 1000000, len(date_range))
99
+ })
100
+
101
+ # Calculate moving averages
102
+ df['MA50'] = df['Close'].rolling(window=50).mean()
103
+ df['MA20'] = df['Close'].rolling(window=20).mean()
104
+
105
+ return df
106
+
107
+
108
+ def generate_seaborn_price_chart(ticker):
109
+ """Generate a Seaborn price chart and return as base64 encoded image"""
110
+ df = get_historical_data(ticker)
111
+
112
+ # Create a Seaborn figure
113
+ plt.figure(figsize=(10, 6))
114
+ sns.set_style("darkgrid")
115
+
116
+ # Plot price and moving averages
117
+ ax = sns.lineplot(x='Date', y='Close', data=df, label='Price', linewidth=2)
118
+ sns.lineplot(x='Date', y='MA20', data=df, label='20-Day MA', linewidth=1.5)
119
+ sns.lineplot(x='Date', y='MA50', data=df, label='50-Day MA', linewidth=1.5)
120
+
121
+ # Set labels and title
122
+ plt.title(f'{ticker} - Price History (Last 6 Months)', fontsize=16)
123
+ plt.ylabel('Price (₹)', fontsize=12)
124
+ plt.xlabel('', fontsize=12)
125
+ plt.xticks(rotation=45)
126
+ plt.tight_layout()
127
+
128
+ # Save to bytes buffer
129
+ buffer = io.BytesIO()
130
+ plt.savefig(buffer, format='png', dpi=100)
131
+ buffer.seek(0)
132
+
133
+ # Encode as base64 string
134
+ image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
135
+ plt.close()
136
+
137
+ return image_base64
138
+
139
+
140
+ def generate_plotly_charts(ticker):
141
+ """Generate Plotly charts and return as JSON"""
142
+ df = get_historical_data(ticker)
143
+
144
+ # Price chart with candlesticks
145
+ # For simplicity, we'll generate OHLC data from our close prices
146
+ df['Open'] = df['Close'].shift(1)
147
+ df['High'] = df['Close'] * (1 + np.random.uniform(0, 0.02, len(df)))
148
+ df['Low'] = df['Close'] * (1 - np.random.uniform(0, 0.02, len(df)))
149
+ df = df.dropna()
150
+
151
+ # Candlestick chart
152
+ candlestick = go.Figure(data=[go.Candlestick(
153
+ x=df['Date'],
154
+ open=df['Open'],
155
+ high=df['High'],
156
+ low=df['Low'],
157
+ close=df['Close'],
158
+ increasing_line_color='green',
159
+ decreasing_line_color='red'
160
+ )])
161
+ candlestick.update_layout(
162
+ title=f'{ticker} - OHLC Chart',
163
+ xaxis_title='Date',
164
+ yaxis_title='Price (₹)',
165
+ xaxis_rangeslider_visible=False,
166
+ height=500
167
+ )
168
+
169
+ # Volume chart
170
+ volume = px.bar(
171
+ df,
172
+ x='Date',
173
+ y='Volume',
174
+ title=f'{ticker} - Volume Chart'
175
+ )
176
+ volume.update_layout(
177
+ xaxis_title='Date',
178
+ yaxis_title='Volume',
179
+ height=300
180
+ )
181
+
182
+ # Price Histogram
183
+ histogram = px.histogram(
184
+ df,
185
+ x='Close',
186
+ nbins=20,
187
+ title=f'{ticker} - Price Distribution'
188
+ )
189
+ histogram.update_layout(
190
+ xaxis_title='Price (₹)',
191
+ yaxis_title='Frequency',
192
+ height=300
193
+ )
194
+
195
+ # Return JSON for each chart
196
+ return {
197
+ 'candlestick': json.loads(candlestick.to_json()),
198
+ 'volume': json.loads(volume.to_json()),
199
+ 'histogram': json.loads(histogram.to_json())
200
+ }
201
+
202
+
203
+ @app.route('/')
204
+ def index():
205
+ return render_template('index.html')
206
+
207
+
208
+ @app.route('/search', methods=['POST'])
209
+ def search():
210
+ ticker = request.form.get('ticker', '')
211
+ if not ticker:
212
+ return jsonify({'error': 'No ticker provided'})
213
+
214
+ url = f'https://ticker.finology.in/company/{ticker}'
215
+ headers = {
216
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
217
+ }
218
+
219
+ try:
220
+ resp = requests.get(url, headers=headers, timeout=10)
221
+ resp.raise_for_status()
222
+ html = resp.text
223
+
224
+ company_name = get_company_name(html)
225
+ data = parse_company_essentials(html)
226
+
227
+ # Generate seaborn chart
228
+ seaborn_chart = generate_seaborn_price_chart(ticker)
229
+
230
+ # Generate plotly charts
231
+ plotly_charts = generate_plotly_charts(ticker)
232
+
233
+ # Sample 52-week high/low data
234
+ high_52w = round(random.uniform(110, 150), 2)
235
+ low_52w = round(random.uniform(70, 95), 2)
236
+ avg_vol = f"{random.randint(100, 999)},{random.randint(100, 999)}"
237
+
238
+ return jsonify({
239
+ 'company_name': company_name,
240
+ 'data': data,
241
+ 'charts': {
242
+ 'seaborn_base64': seaborn_chart,
243
+ 'plotly': plotly_charts
244
+ },
245
+ 'performance': {
246
+ 'high_52week': f"₹{high_52w}",
247
+ 'low_52week': f"₹{low_52w}",
248
+ 'avg_volume': avg_vol
249
+ },
250
+ 'success': True
251
+ })
252
+ except requests.exceptions.RequestException as e:
253
+ return jsonify({
254
+ 'error': f'Error fetching data: {str(e)}',
255
+ 'success': False
256
+ })
257
+
258
+
259
+ if __name__ == '__main__':
260
+ app.run(debug=True, port=5002)
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ flask
2
+ gunicorn
3
+ pandas
4
+ numpy
5
+ requests
6
+ beautifulsoup4
7
+ matplotlib
8
+ seaborn
9
+ plotly
templates/index.html ADDED
@@ -0,0 +1,798 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>📊 StockInsight Pro</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/plotly.js/2.26.0/plotly.min.js"></script>
10
+ <style>
11
+ :root {
12
+ --primary-color: #4361ee;
13
+ --secondary-color: #3f37c9;
14
+ --success-color: #4cc9f0;
15
+ --danger-color: #f72585;
16
+ --light-bg: #f8f9fa;
17
+ --dark-bg: #212529;
18
+ --gold-color: #ffd700;
19
+ }
20
+
21
+ @font-face {
22
+ font-family: 'Font Awesome 5 Free';
23
+ font-style: normal;
24
+ font-weight: 900;
25
+ font-display: block;
26
+ src: url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/webfonts/fa-solid-900.woff2") format("woff2");
27
+ }
28
+
29
+ .fa-coin:before {
30
+ content: "\f51e"; /* using fa-coins icon code */
31
+ font-family: 'Font Awesome 5 Free';
32
+ }
33
+
34
+ body {
35
+ background-color: var(--light-bg);
36
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
37
+ padding: 0;
38
+ min-height: 100vh;
39
+ }
40
+
41
+ .hero-section {
42
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
43
+ color: white;
44
+ padding: 30px 0;
45
+ margin-bottom: 30px;
46
+ border-bottom: 4px solid var(--accent-color);
47
+ }
48
+
49
+ .app-logo {
50
+ display: inline-block;
51
+ background-color: rgba(255, 255, 255, 0.15);
52
+ width: 80px;
53
+ height: 80px;
54
+ border-radius: 50%;
55
+ line-height: 80px;
56
+ text-align: center;
57
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
58
+ animation: pulse 2s infinite;
59
+ }
60
+
61
+ @keyframes pulse {
62
+ 0% {
63
+ box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.4);
64
+ }
65
+ 70% {
66
+ box-shadow: 0 0 0 10px rgba(255, 255, 255, 0);
67
+ }
68
+ 100% {
69
+ box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
70
+ }
71
+ }
72
+
73
+ .search-container {
74
+ max-width: 600px;
75
+ margin: 0 auto;
76
+ position: relative;
77
+ transform: translateY(50%);
78
+ }
79
+
80
+ .search-form {
81
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
82
+ border-radius: 30px;
83
+ overflow: hidden;
84
+ }
85
+
86
+ .search-input {
87
+ border: none;
88
+ box-shadow: none;
89
+ padding-left: 20px;
90
+ font-size: 1.1rem;
91
+ height: 55px;
92
+ }
93
+
94
+ .search-btn {
95
+ border-radius: 0 30px 30px 0;
96
+ padding: 0 25px;
97
+ background-color: var(--primary-color);
98
+ border-color: var(--primary-color);
99
+ transition: all 0.3s;
100
+ }
101
+
102
+ .search-btn:hover {
103
+ background-color: var(--secondary-color);
104
+ border-color: var(--secondary-color);
105
+ }
106
+
107
+ .recent-searches {
108
+ margin-top: 10px;
109
+ display: flex;
110
+ flex-wrap: wrap;
111
+ gap: 8px;
112
+ }
113
+
114
+ .recent-search-item {
115
+ background-color: rgba(255, 255, 255, 0.2);
116
+ border-radius: 20px;
117
+ padding: 4px 12px;
118
+ font-size: 0.85rem;
119
+ cursor: pointer;
120
+ transition: all 0.2s;
121
+ }
122
+
123
+ .recent-search-item:hover {
124
+ background-color: rgba(255, 255, 255, 0.3);
125
+ }
126
+
127
+ .content-section {
128
+ margin-top: 3rem;
129
+ }
130
+
131
+ .company-header {
132
+ background: linear-gradient(135deg, #3a0ca3 0%, #4361ee 100%);
133
+ color: white;
134
+ padding: 2rem;
135
+ border-radius: 10px;
136
+ margin-bottom: 2rem;
137
+ display: none;
138
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
139
+ position: relative;
140
+ overflow: hidden;
141
+ }
142
+
143
+ .company-header::after {
144
+ content: '';
145
+ position: absolute;
146
+ top: 0;
147
+ right: 0;
148
+ width: 150px;
149
+ height: 100%;
150
+ background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="white" opacity="0.1"><path d="M3 3v18h18V3H3zm16 16H5V5h14v14zM7 7h2v2H7V7zm0 4h2v2H7v-2zm0 4h2v2H7v-2zm4-8h6v2h-6V7zm0 4h6v2h-6v-2zm0 4h6v2h-6v-2z"/></svg>') repeat;
151
+ opacity: 0.05;
152
+ }
153
+
154
+ .stock-badges {
155
+ display: flex;
156
+ flex-wrap: wrap;
157
+ gap: 8px;
158
+ }
159
+
160
+ .badge {
161
+ font-weight: normal;
162
+ padding: 8px 12px;
163
+ border-radius: 20px;
164
+ }
165
+
166
+ .company-info {
167
+ display: flex;
168
+ align-items: center;
169
+ }
170
+
171
+ .company-logo {
172
+ width: 60px;
173
+ height: 60px;
174
+ background-color: rgba(255, 255, 255, 0.2);
175
+ border-radius: 50%;
176
+ display: flex;
177
+ align-items: center;
178
+ justify-content: center;
179
+ margin-right: 15px;
180
+ font-size: 1.8rem;
181
+ position: relative;
182
+ overflow: hidden;
183
+ }
184
+
185
+ .company-logo::after {
186
+ content: '';
187
+ position: absolute;
188
+ top: -15px;
189
+ right: -15px;
190
+ width: 30px;
191
+ height: 30px;
192
+ background-color: rgba(255, 255, 255, 0.1);
193
+ border-radius: 50%;
194
+ }
195
+
196
+ .company-details h2 {
197
+ margin-bottom: 5px;
198
+ font-weight: 600;
199
+ }
200
+
201
+ .company-sector {
202
+ font-size: 1rem;
203
+ opacity: 0.8;
204
+ }
205
+
206
+ .card {
207
+ border: none;
208
+ margin-bottom: 20px;
209
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
210
+ border-radius: 10px;
211
+ transition: transform 0.3s, box-shadow 0.3s;
212
+ overflow: hidden;
213
+ }
214
+
215
+ .card:hover {
216
+ transform: translateY(-5px);
217
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
218
+ }
219
+
220
+ .card-header {
221
+ background-color: rgba(67, 97, 238, 0.1);
222
+ font-weight: 600;
223
+ color: var(--primary-color);
224
+ border-bottom: none;
225
+ padding: 15px;
226
+ }
227
+
228
+ .card-body {
229
+ padding: 20px;
230
+ }
231
+
232
+ .stock-card-value {
233
+ font-size: 1.5rem;
234
+ font-weight: 700;
235
+ color: #333;
236
+ margin-bottom: 0;
237
+ }
238
+
239
+ .card-indicator {
240
+ font-size: 0.9rem;
241
+ margin-top: 5px;
242
+ }
243
+
244
+ .indicator-up {
245
+ color: #2ecc71;
246
+ }
247
+
248
+ .indicator-down {
249
+ color: #e74c3c;
250
+ }
251
+
252
+ .tooltip-icon {
253
+ cursor: help;
254
+ margin-left: 5px;
255
+ color: #6c757d;
256
+ transition: color 0.2s;
257
+ }
258
+
259
+ .tooltip-icon:hover {
260
+ color: var(--primary-color);
261
+ }
262
+
263
+ .loading {
264
+ display: none;
265
+ text-align: center;
266
+ padding: 40px 0;
267
+ }
268
+
269
+ .spinner-container {
270
+ display: flex;
271
+ flex-direction: column;
272
+ align-items: center;
273
+ gap: 15px;
274
+ }
275
+
276
+ .spinner-border {
277
+ width: 3rem;
278
+ height: 3rem;
279
+ }
280
+
281
+ .error-message {
282
+ display: none;
283
+ color: #fff;
284
+ padding: 15px;
285
+ border-radius: 10px;
286
+ background-color: var(--danger-color);
287
+ margin: 20px 0;
288
+ box-shadow: 0 4px 12px rgba(247, 37, 133, 0.3);
289
+ }
290
+
291
+ #results-container {
292
+ display: none;
293
+ }
294
+
295
+ .info-tabs {
296
+ margin-bottom: 20px;
297
+ }
298
+
299
+ .nav-tabs {
300
+ border-bottom: none;
301
+ margin-bottom: 20px;
302
+ }
303
+
304
+ .nav-tabs .nav-link {
305
+ border: none;
306
+ border-radius: 20px;
307
+ padding: 8px 20px;
308
+ margin-right: 10px;
309
+ color: #495057;
310
+ font-weight: 500;
311
+ transition: all 0.3s;
312
+ }
313
+
314
+ .nav-tabs .nav-link:hover {
315
+ background-color: rgba(67, 97, 238, 0.1);
316
+ }
317
+
318
+ .nav-tabs .nav-link.active {
319
+ background-color: var(--primary-color);
320
+ color: white;
321
+ }
322
+
323
+ .info-section {
324
+ margin-bottom: 3rem;
325
+ }
326
+
327
+ .info-section-header {
328
+ margin-bottom: 1rem;
329
+ color: #333;
330
+ font-weight: 600;
331
+ }
332
+
333
+ .comparison-stats {
334
+ margin-top: 2rem;
335
+ background: white;
336
+ border-radius: 10px;
337
+ padding: 20px;
338
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
339
+ }
340
+
341
+ .stat-row {
342
+ display: flex;
343
+ justify-content: space-between;
344
+ padding: 10px 0;
345
+ border-bottom: 1px solid #f1f1f1;
346
+ }
347
+
348
+ .stat-row:last-child {
349
+ border-bottom: none;
350
+ }
351
+
352
+ .stat-label {
353
+ font-weight: 500;
354
+ color: #555;
355
+ }
356
+
357
+ .stat-value {
358
+ font-weight: 600;
359
+ color: #333;
360
+ }
361
+
362
+ .footer-note {
363
+ margin-top: 2rem;
364
+ padding: 1rem;
365
+ background: white;
366
+ border-radius: 10px;
367
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
368
+ font-size: 0.9rem;
369
+ color: #6c757d;
370
+ }
371
+
372
+ .chart-container {
373
+ background: white;
374
+ border-radius: 10px;
375
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
376
+ padding: 20px;
377
+ margin-bottom: 20px;
378
+ }
379
+
380
+ .seaborn-chart {
381
+ width: 100%;
382
+ border-radius: 10px;
383
+ margin-bottom: 20px;
384
+ }
385
+
386
+ .plotly-chart {
387
+ width: 100%;
388
+ border-radius: 8px;
389
+ margin-bottom: 20px;
390
+ min-height: 400px;
391
+ }
392
+
393
+ @media (max-width: 767px) {
394
+ .hero-section {
395
+ padding: 2rem 0;
396
+ }
397
+
398
+ .search-container {
399
+ transform: translateY(30%);
400
+ }
401
+
402
+ .company-logo {
403
+ width: 45px;
404
+ height: 45px;
405
+ font-size: 1.4rem;
406
+ }
407
+
408
+ .company-details h2 {
409
+ font-size: 1.5rem;
410
+ }
411
+
412
+ .nav-tabs .nav-link {
413
+ padding: 6px 12px;
414
+ font-size: 0.9rem;
415
+ }
416
+ }
417
+ </style>
418
+ </head>
419
+ <body>
420
+ <div class="hero-section">
421
+ <div class="container">
422
+ <div class="text-center mb-4">
423
+ <div class="app-logo mb-3">
424
+ <i class="fas fa-chart-line fa-3x"></i>
425
+ </div>
426
+ <h1 class="display-4 fw-bold">StockInsight Pro</h1>
427
+ <p class="lead">Instantly access financial metrics for Indian stocks</p>
428
+ </div>
429
+
430
+ <div class="search-container">
431
+ <form id="search-form" class="search-form d-flex">
432
+ <input type="text" id="ticker-input" class="form-control search-input"
433
+ placeholder="Enter ticker (e.g., ITC, RELIANCE, TCS)" required autocomplete="off">
434
+ <button type="submit" class="btn btn-primary search-btn">
435
+ <i class="fas fa-search"></i>
436
+ </button>
437
+ </form>
438
+
439
+ <div class="recent-searches" id="recent-searches">
440
+ <!-- Will be populated by JS -->
441
+ </div>
442
+ </div>
443
+ </div>
444
+ </div>
445
+
446
+ <div class="container content-section">
447
+ <div id="loading" class="loading">
448
+ <div class="spinner-container">
449
+ <div class="spinner-border text-primary" role="status">
450
+ <span class="visually-hidden">Loading...</span>
451
+ </div>
452
+ <p class="text-muted">Fetching latest stock data...</p>
453
+ </div>
454
+ </div>
455
+
456
+ <div id="error-message" class="error-message">
457
+ <i class="fas fa-exclamation-triangle me-2"></i>
458
+ <span id="error-text">Error message will appear here</span>
459
+ </div>
460
+
461
+ <div id="results-container">
462
+ <div id="company-header" class="company-header">
463
+ <div class="company-info">
464
+ <div class="company-logo" id="company-logo-container">
465
+ <i class="fas fa-chart-line"></i>
466
+ </div>
467
+ <div class="company-details">
468
+ <h2 id="company-name">Company Name</h2>
469
+ <div class="company-sector" id="company-sector">
470
+ <i class="fas fa-industry me-2"></i>
471
+ <span id="sector-text">Industry / Sector</span>
472
+ </div>
473
+ </div>
474
+ </div>
475
+ <div class="stock-badges mt-3">
476
+ <span class="badge bg-light text-dark me-2">
477
+ <i class="fas fa-globe me-1"></i> BSE/NSE
478
+ </span>
479
+ <span class="badge bg-light text-dark me-2">
480
+ <i class="fas fa-clock me-1"></i> Updated <span id="last-updated">today</span>
481
+ </span>
482
+ </div>
483
+ </div>
484
+
485
+ <div class="info-tabs">
486
+ <ul class="nav nav-tabs" id="stockInfoTabs" role="tablist">
487
+ <li class="nav-item" role="presentation">
488
+ <button class="nav-link active" id="essentials-tab" data-bs-toggle="tab" data-bs-target="#essentials" type="button" role="tab" aria-controls="essentials" aria-selected="true">
489
+ <i class="fas fa-star me-2"></i>Key Metrics
490
+ </button>
491
+ </li>
492
+ <li class="nav-item" role="presentation">
493
+ <button class="nav-link" id="performance-tab" data-bs-toggle="tab" data-bs-target="#performance" type="button" role="tab" aria-controls="performance" aria-selected="false">
494
+ <i class="fas fa-chart-line me-2"></i>Performance
495
+ </button>
496
+ </li>
497
+ <li class="nav-item" role="presentation">
498
+ <button class="nav-link" id="charts-tab" data-bs-toggle="tab" data-bs-target="#charts" type="button" role="tab" aria-controls="charts" aria-selected="false">
499
+ <i class="fas fa-chart-bar me-2"></i>Charts
500
+ </button>
501
+ </li>
502
+ <li class="nav-item" role="presentation">
503
+ <button class="nav-link" id="news-tab" data-bs-toggle="tab" data-bs-target="#news" type="button" role="tab" aria-controls="news" aria-selected="false">
504
+ <i class="fas fa-newspaper me-2"></i>News
505
+ </button>
506
+ </li>
507
+ </ul>
508
+
509
+ <div class="tab-content" id="stockInfoTabsContent">
510
+ <div class="tab-pane fade show active" id="essentials" role="tabpanel" aria-labelledby="essentials-tab">
511
+ <div class="info-section">
512
+ <h3 class="info-section-header"><i class="fas fa-info-circle me-2"></i>Company Essentials</h3>
513
+ <div id="essentials-container" class="row"></div>
514
+ </div>
515
+ </div>
516
+
517
+ <div class="tab-pane fade" id="performance" role="tabpanel" aria-labelledby="performance-tab">
518
+ <div class="info-section">
519
+ <h3 class="info-section-header"><i class="fas fa-chart-bar me-2"></i>Performance Metrics</h3>
520
+
521
+ <!-- Seaborn Static Chart -->
522
+ <div class="chart-container mb-4">
523
+ <h4 class="mb-3"><i class="fas fa-chart-line me-2"></i>Price History (Last 6 Months)</h4>
524
+ <img id="seaborn-chart" class="seaborn-chart img-fluid" src="" alt="Price History Chart">
525
+ </div>
526
+
527
+ <div class="comparison-stats">
528
+ <div class="stat-row">
529
+ <span class="stat-label"><i class="fas fa-arrow-up text-success me-2"></i>52-Week High</span>
530
+ <span class="stat-value" id="high-52week">--</span>
531
+ </div>
532
+ <div class="stat-row">
533
+ <span class="stat-label"><i class="fas fa-arrow-down text-danger me-2"></i>52-Week Low</span>
534
+ <span class="stat-value" id="low-52week">--</span>
535
+ </div>
536
+ <div class="stat-row">
537
+ <span class="stat-label"><i class="fas fa-exchange-alt me-2"></i>Volume (Avg)</span>
538
+ <span class="stat-value" id="avg-volume">--</span>
539
+ </div>
540
+ </div>
541
+ </div>
542
+ </div>
543
+
544
+ <div class="tab-pane fade" id="charts" role="tabpanel" aria-labelledby="charts-tab">
545
+ <div class="info-section">
546
+ <h3 class="info-section-header"><i class="fas fa-chart-bar me-2"></i>Interactive Charts</h3>
547
+
548
+ <!-- Plotly Interactive Charts -->
549
+ <div class="chart-container mb-4">
550
+ <h4 class="mb-3"><i class="fas fa-chart-line me-2"></i>OHLC Price Chart</h4>
551
+ <div id="candlestick-chart" class="plotly-chart"></div>
552
+ </div>
553
+
554
+ <div class="row">
555
+ <div class="col-md-6">
556
+ <div class="chart-container">
557
+ <h4 class="mb-3"><i class="fas fa-chart-bar me-2"></i>Volume Analysis</h4>
558
+ <div id="volume-chart" class="plotly-chart"></div>
559
+ </div>
560
+ </div>
561
+ <div class="col-md-6">
562
+ <div class="chart-container">
563
+ <h4 class="mb-3"><i class="fas fa-chart-area me-2"></i>Price Distribution</h4>
564
+ <div id="histogram-chart" class="plotly-chart"></div>
565
+ </div>
566
+ </div>
567
+ </div>
568
+ </div>
569
+ </div>
570
+
571
+ <div class="tab-pane fade" id="news" role="tabpanel" aria-labelledby="news-tab">
572
+ <div class="info-section">
573
+ <h3 class="info-section-header"><i class="fas fa-newspaper me-2"></i>Latest News</h3>
574
+ <div class="alert alert-info">
575
+ <i class="fas fa-info-circle me-2"></i>
576
+ The news section is not available in the current version. Check back soon for updates!
577
+ </div>
578
+ </div>
579
+ </div>
580
+ </div>
581
+ </div>
582
+
583
+ <div class="footer-note">
584
+ <p class="mb-0"><i class="fas fa-info-circle me-2"></i> Disclaimer: All information is for educational purposes only. Do not use this data for making investment decisions.</p>
585
+ </div>
586
+ </div>
587
+ </div>
588
+
589
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
590
+ <script>
591
+ document.addEventListener('DOMContentLoaded', function() {
592
+ const searchForm = document.getElementById('search-form');
593
+ const tickerInput = document.getElementById('ticker-input');
594
+ const loadingContainer = document.getElementById('loading');
595
+ const errorMessage = document.getElementById('error-message');
596
+ const errorText = document.getElementById('error-text');
597
+ const resultsContainer = document.getElementById('results-container');
598
+ const companyHeader = document.getElementById('company-header');
599
+ const companyName = document.getElementById('company-name');
600
+ const recentSearchesContainer = document.getElementById('recent-searches');
601
+
602
+ // Recent searches array from localStorage
603
+ let recentSearches = JSON.parse(localStorage.getItem('recentSearches')) || [];
604
+
605
+ // Display recent searches
606
+ function displayRecentSearches() {
607
+ recentSearchesContainer.innerHTML = '';
608
+
609
+ // Get latest 5 searches
610
+ const latestSearches = recentSearches.slice(0, 5);
611
+
612
+ latestSearches.forEach(ticker => {
613
+ const searchItem = document.createElement('div');
614
+ searchItem.className = 'recent-search-item';
615
+ searchItem.textContent = ticker;
616
+ searchItem.addEventListener('click', () => {
617
+ tickerInput.value = ticker;
618
+ searchForm.dispatchEvent(new Event('submit'));
619
+ });
620
+ recentSearchesContainer.appendChild(searchItem);
621
+ });
622
+ }
623
+
624
+ // Add ticker to recent searches
625
+ function addToRecentSearches(ticker) {
626
+ // Remove ticker if already exists
627
+ recentSearches = recentSearches.filter(item => item !== ticker);
628
+
629
+ // Add to beginning of array
630
+ recentSearches.unshift(ticker);
631
+
632
+ // Keep only latest 10 searches
633
+ recentSearches = recentSearches.slice(0, 10);
634
+
635
+ // Save to localStorage
636
+ localStorage.setItem('recentSearches', JSON.stringify(recentSearches));
637
+
638
+ // Update displayed searches
639
+ displayRecentSearches();
640
+ }
641
+
642
+ // Display initial recent searches
643
+ displayRecentSearches();
644
+
645
+ // Handle form submission
646
+ searchForm.addEventListener('submit', function(e) {
647
+ e.preventDefault();
648
+
649
+ const ticker = tickerInput.value.trim().toUpperCase();
650
+
651
+ if (!ticker) {
652
+ return;
653
+ }
654
+
655
+ // Show loading
656
+ loadingContainer.style.display = 'block';
657
+ errorMessage.style.display = 'none';
658
+ resultsContainer.style.display = 'none';
659
+
660
+ // Make fetch request to the server
661
+ fetch('/search', {
662
+ method: 'POST',
663
+ headers: {
664
+ 'Content-Type': 'application/x-www-form-urlencoded',
665
+ },
666
+ body: `ticker=${encodeURIComponent(ticker)}`
667
+ })
668
+ .then(response => response.json())
669
+ .then(data => {
670
+ // Hide loading
671
+ loadingContainer.style.display = 'none';
672
+
673
+ if (data.success) {
674
+ // Add to recent searches
675
+ addToRecentSearches(ticker);
676
+
677
+ // Display results
678
+ displayResults(data, ticker);
679
+ } else {
680
+ // Show error
681
+ errorText.textContent = data.error;
682
+ errorMessage.style.display = 'block';
683
+ }
684
+ })
685
+ .catch(error => {
686
+ // Hide loading
687
+ loadingContainer.style.display = 'none';
688
+
689
+ // Show error
690
+ errorText.textContent = `An error occurred: ${error.message}`;
691
+ errorMessage.style.display = 'block';
692
+ });
693
+ });
694
+
695
+ // Display results
696
+ function displayResults(data, ticker) {
697
+ // Set company name
698
+ companyName.textContent = data.company_name || ticker;
699
+
700
+ // Set sector text (sample data)
701
+ document.getElementById('sector-text').textContent = 'Stock Analysis Data';
702
+
703
+ // Set last updated
704
+ document.getElementById('last-updated').textContent = new Date().toLocaleDateString();
705
+
706
+ // Display company header
707
+ companyHeader.style.display = 'block';
708
+
709
+ // Display company essentials
710
+ const essentialsContainer = document.getElementById('essentials-container');
711
+ essentialsContainer.innerHTML = '';
712
+
713
+ if (data.data && data.data.length > 0) {
714
+ data.data.forEach(item => {
715
+ const cardDiv = document.createElement('div');
716
+ cardDiv.className = 'col-lg-3 col-md-4 col-sm-6 mb-4';
717
+
718
+ let tooltipHtml = '';
719
+ if (item.tooltip) {
720
+ tooltipHtml = `<span class="tooltip-icon" data-bs-toggle="tooltip" data-bs-placement="top" title="${item.tooltip}">
721
+ <i class="fas fa-info-circle"></i>
722
+ </span>`;
723
+ }
724
+
725
+ cardDiv.innerHTML = `
726
+ <div class="card h-100">
727
+ <div class="card-header d-flex justify-content-between align-items-center">
728
+ ${item.label} ${tooltipHtml}
729
+ </div>
730
+ <div class="card-body d-flex flex-column justify-content-center">
731
+ <p class="stock-card-value">${item.value}</p>
732
+ </div>
733
+ </div>
734
+ `;
735
+
736
+ essentialsContainer.appendChild(cardDiv);
737
+ });
738
+
739
+ // Initialize tooltips
740
+ var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
741
+ tooltipTriggerList.map(function (tooltipTriggerEl) {
742
+ return new bootstrap.Tooltip(tooltipTriggerEl);
743
+ });
744
+ }
745
+
746
+ // Set performance metrics
747
+ if (data.performance) {
748
+ document.getElementById('high-52week').textContent = data.performance.high_52week;
749
+ document.getElementById('low-52week').textContent = data.performance.low_52week;
750
+ document.getElementById('avg-volume').textContent = data.performance.avg_volume;
751
+ }
752
+
753
+ // Display Seaborn chart
754
+ if (data.charts && data.charts.seaborn_base64) {
755
+ document.getElementById('seaborn-chart').src = `data:image/png;base64,${data.charts.seaborn_base64}`;
756
+ }
757
+
758
+ // Display Plotly charts
759
+ if (data.charts && data.charts.plotly) {
760
+ // Candlestick chart
761
+ if (data.charts.plotly.candlestick) {
762
+ Plotly.newPlot('candlestick-chart', data.charts.plotly.candlestick.data, data.charts.plotly.candlestick.layout);
763
+ }
764
+
765
+ // Volume chart
766
+ if (data.charts.plotly.volume) {
767
+ Plotly.newPlot('volume-chart', data.charts.plotly.volume.data, data.charts.plotly.volume.layout);
768
+ }
769
+
770
+ // Histogram chart
771
+ if (data.charts.plotly.histogram) {
772
+ Plotly.newPlot('histogram-chart', data.charts.plotly.histogram.data, data.charts.plotly.histogram.layout);
773
+ }
774
+ }
775
+
776
+ // Show results container
777
+ resultsContainer.style.display = 'block';
778
+
779
+ // Reset zoom on charts when switching tabs
780
+ const chartTab = document.getElementById('charts-tab');
781
+ chartTab.addEventListener('shown.bs.tab', function (e) {
782
+ if (data.charts && data.charts.plotly) {
783
+ if (data.charts.plotly.candlestick) {
784
+ Plotly.relayout('candlestick-chart', {});
785
+ }
786
+ if (data.charts.plotly.volume) {
787
+ Plotly.relayout('volume-chart', {});
788
+ }
789
+ if (data.charts.plotly.histogram) {
790
+ Plotly.relayout('histogram-chart', {});
791
+ }
792
+ }
793
+ });
794
+ }
795
+ });
796
+ </script>
797
+ </body>
798
+ </html>