GSMSB commited on
Commit
f15861f
·
1 Parent(s): a78209f

Implement 6-axis peer comparison and refined metrics

Browse files
backend/__pycache__/config.cpython-313.pyc CHANGED
Binary files a/backend/__pycache__/config.cpython-313.pyc and b/backend/__pycache__/config.cpython-313.pyc differ
 
backend/__pycache__/main.cpython-313.pyc CHANGED
Binary files a/backend/__pycache__/main.cpython-313.pyc and b/backend/__pycache__/main.cpython-313.pyc differ
 
backend/agents/__pycache__/fundamental_analyzer.cpython-313.pyc CHANGED
Binary files a/backend/agents/__pycache__/fundamental_analyzer.cpython-313.pyc and b/backend/agents/__pycache__/fundamental_analyzer.cpython-313.pyc differ
 
backend/agents/__pycache__/news_analyzer.cpython-313.pyc CHANGED
Binary files a/backend/agents/__pycache__/news_analyzer.cpython-313.pyc and b/backend/agents/__pycache__/news_analyzer.cpython-313.pyc differ
 
backend/agents/__pycache__/peer_comparator.cpython-313.pyc CHANGED
Binary files a/backend/agents/__pycache__/peer_comparator.cpython-313.pyc and b/backend/agents/__pycache__/peer_comparator.cpython-313.pyc differ
 
backend/agents/__pycache__/signal_generator.cpython-313.pyc CHANGED
Binary files a/backend/agents/__pycache__/signal_generator.cpython-313.pyc and b/backend/agents/__pycache__/signal_generator.cpython-313.pyc differ
 
backend/agents/peer_comparator.py CHANGED
@@ -5,6 +5,7 @@ import logging
5
  from backend.config import GEMINI_API_KEY
6
  from backend.models.schemas import PeerComparison, FundamentalMetrics
7
  from backend.utils.ai_helper import generate_content_with_retry
 
8
 
9
  logger = logging.getLogger(__name__)
10
  genai.configure(api_key=GEMINI_API_KEY)
@@ -19,6 +20,25 @@ class PeerComparator:
19
  from backend.utils.ticker_db import get_ticker_db
20
  db = get_ticker_db()
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  peer_data_list = []
23
  peer_names = []
24
 
@@ -64,25 +84,46 @@ class PeerComparator:
64
  p_name = p_data.get('Name')
65
  peer_names.append(p_name)
66
 
 
 
 
67
  # Convert CSV dict to FundamentalMetrics object
68
  # Note: We only fill quantitative, qualitative will be defaults
69
  p_metrics = FundamentalMetrics(
70
- market_cap=p_data.get('Market Cap (Cr.)', 0.0),
71
- pe_ratio=p_data.get('PE Ratio', 0.0),
72
- industry_pe=p_data.get('Industry PE', 0.0),
73
- roe=p_data.get('ROE', 0.0),
74
- roce=p_data.get('ROCE', 0.0),
75
- eps=p_data.get('EPS', 0.0),
76
- pb_ratio=p_data.get('PB Ratio', 0.0),
77
- dividend_yield=p_data.get('Dividend', 0.0),
78
 
79
- returns_1y=p_data.get('1 Yr Returns', 0.0),
80
- # Add other returns if needed for comparison view
81
 
82
  health_score=5, # Default since we don't analyze peers deeply
83
  strengths=[],
84
  concerns=[]
85
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  peer_metrics_map[p_name] = p_metrics
87
 
88
  print(f"[Peer Comparator] Found peers: {peer_names}")
 
5
  from backend.config import GEMINI_API_KEY
6
  from backend.models.schemas import PeerComparison, FundamentalMetrics
7
  from backend.utils.ai_helper import generate_content_with_retry
8
+ from backend.utils.peer_comparison import calculate_normalized_scores_v2
9
 
10
  logger = logging.getLogger(__name__)
11
  genai.configure(api_key=GEMINI_API_KEY)
 
20
  from backend.utils.ticker_db import get_ticker_db
21
  db = get_ticker_db()
22
 
23
+ # --- 0. Calculate Scores for TARGET Company ---
24
+ # We need to construct a raw dict for the utility
25
+ try:
26
+ target_raw = {
27
+ 'roe': target_metrics.roe,
28
+ 'roce': target_metrics.roce,
29
+ 'pe_ratio': target_metrics.pe_ratio,
30
+ 'industry_pe': target_metrics.industry_pe,
31
+ 'dividend_yield': target_metrics.dividend_yield, # If already calculated
32
+ 'returns_5y': target_metrics.returns_5y,
33
+ 'returns_1y': target_metrics.returns_1y,
34
+ # 'current_price' might not be in target_metrics, but maybe we don't need it if div yield is there
35
+ 'current_price': 0.0 # Placeholder
36
+ }
37
+ target_metrics.normalized_scores = calculate_normalized_scores_v2(target_raw)
38
+ print(f"[Peer Comparator] Calculated Target Scores: {target_metrics.normalized_scores}")
39
+ except Exception as e:
40
+ print(f"Error calculating target scores: {e}")
41
+
42
  peer_data_list = []
43
  peer_names = []
44
 
 
84
  p_name = p_data.get('Name')
85
  peer_names.append(p_name)
86
 
87
+ # Helper to safely get float
88
+ def sf(k): return float(p_data.get(k, 0.0) or 0.0)
89
+
90
  # Convert CSV dict to FundamentalMetrics object
91
  # Note: We only fill quantitative, qualitative will be defaults
92
  p_metrics = FundamentalMetrics(
93
+ market_cap=sf('Market Cap (Cr.)'),
94
+ pe_ratio=sf('PE Ratio'),
95
+ industry_pe=sf('Industry PE'),
96
+ roe=sf('ROE'),
97
+ roce=sf('ROCE'),
98
+ eps=sf('EPS'),
99
+ pb_ratio=sf('PB Ratio'),
100
+ dividend_yield=sf('Dividend'), # CSV often has Dividend (Rs) or Yield? Assuming Yield or use logic
101
 
102
+ returns_1y=sf('1 Yr Returns'),
103
+ returns_5y=sf('5 Yr Returns'),
104
 
105
  health_score=5, # Default since we don't analyze peers deeply
106
  strengths=[],
107
  concerns=[]
108
  )
109
+
110
+ # Calculate Normalized Scores for Peer
111
+ try:
112
+ # Map CSV keys to expected format for calc
113
+ peer_raw = {
114
+ 'roe': sf('ROE'),
115
+ 'roce': sf('ROCE'),
116
+ 'pe_ratio': sf('PE Ratio'),
117
+ 'industry_pe': sf('Industry PE'),
118
+ 'dividend': sf('Dividend'),
119
+ 'current_price': sf('LTP'),
120
+ 'returns_1y': sf('1 Yr Returns'),
121
+ 'returns_5y': sf('5 Yr Returns')
122
+ }
123
+ p_metrics.normalized_scores = calculate_normalized_scores_v2(peer_raw)
124
+ except Exception as e:
125
+ print(f"Error calculating scores for {p_name}: {e}")
126
+
127
  peer_metrics_map[p_name] = p_metrics
128
 
129
  print(f"[Peer Comparator] Found peers: {peer_names}")
backend/models/__pycache__/schemas.cpython-313.pyc CHANGED
Binary files a/backend/models/__pycache__/schemas.cpython-313.pyc and b/backend/models/__pycache__/schemas.cpython-313.pyc differ
 
backend/models/schemas.py CHANGED
@@ -56,6 +56,9 @@ class FundamentalMetrics(BaseModel):
56
  revenue_growth: float = 0.0
57
  profit_margin: float = 0.0
58
 
 
 
 
59
  # Raw math fields (Hidden)
60
  revenue_current: float = 0.0
61
  revenue_prior: float = 0.0
 
56
  revenue_growth: float = 0.0
57
  profit_margin: float = 0.0
58
 
59
+ # Normalized Scores for Radar Chart (Growth, Profitability, Efficiency, Valuation, Dividend, Momentum)
60
+ normalized_scores: Optional[Dict[str, float]] = None
61
+
62
  # Raw math fields (Hidden)
63
  revenue_current: float = 0.0
64
  revenue_prior: float = 0.0
backend/utils/__pycache__/ai_helper.cpython-313.pyc CHANGED
Binary files a/backend/utils/__pycache__/ai_helper.cpython-313.pyc and b/backend/utils/__pycache__/ai_helper.cpython-313.pyc differ
 
backend/utils/__pycache__/api_clients.cpython-313.pyc CHANGED
Binary files a/backend/utils/__pycache__/api_clients.cpython-313.pyc and b/backend/utils/__pycache__/api_clients.cpython-313.pyc differ
 
backend/utils/__pycache__/pdf_parser.cpython-313.pyc CHANGED
Binary files a/backend/utils/__pycache__/pdf_parser.cpython-313.pyc and b/backend/utils/__pycache__/pdf_parser.cpython-313.pyc differ
 
backend/utils/__pycache__/rag.cpython-313.pyc CHANGED
Binary files a/backend/utils/__pycache__/rag.cpython-313.pyc and b/backend/utils/__pycache__/rag.cpython-313.pyc differ
 
backend/utils/__pycache__/table_extractor.cpython-313.pyc CHANGED
Binary files a/backend/utils/__pycache__/table_extractor.cpython-313.pyc and b/backend/utils/__pycache__/table_extractor.cpython-313.pyc differ
 
backend/utils/peer_comparison.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ from typing import Dict, Any
3
+
4
+ def safe_float(val):
5
+ if val is None or val == '':
6
+ return 0.0
7
+ try:
8
+ return float(val)
9
+ except:
10
+ return 0.0
11
+
12
+ def normalize_linear(value: float, min_val: float, max_val: float) -> float:
13
+ """
14
+ Linear normalization to 0-100 scale
15
+ """
16
+ if value is None or pd.isna(value):
17
+ return 50.0 # Neutral if missing
18
+
19
+ if value <= min_val:
20
+ return 0.0
21
+ elif value >= max_val:
22
+ return 100.0
23
+ else:
24
+ return ((value - min_val) / (max_val - min_val)) * 100.0
25
+
26
+ def calculate_normalized_scores_v2(raw_data: Dict[str, Any]) -> Dict[str, float]:
27
+ """
28
+ Enhanced normalization with Dividend Yield
29
+ """
30
+
31
+ # 1. GROWTH (5Y Returns)
32
+ # Scale: -20% = 0, 0% = 40, 100% = 100
33
+ returns_5y = raw_data.get('returns_5y')
34
+ if returns_5y is not None:
35
+ growth_score = normalize_linear(returns_5y, min_val=-20, max_val=100)
36
+ else:
37
+ growth_score = 50.0
38
+
39
+ # 2. PROFITABILITY (ROE)
40
+ # Scale: 0% = 0, 15% = 50, 30%+ = 100
41
+ roe = raw_data.get('roe')
42
+ if roe is not None:
43
+ profitability_score = normalize_linear(roe, min_val=0, max_val=30)
44
+ else:
45
+ profitability_score = 50.0
46
+
47
+ # 3. EFFICIENCY (ROCE)
48
+ # Scale: 0% = 0, 15% = 50, 30%+ = 100
49
+ roce = raw_data.get('roce')
50
+ if roce is not None:
51
+ efficiency_score = normalize_linear(roce, min_val=0, max_val=30)
52
+ else:
53
+ efficiency_score = 50.0
54
+
55
+ # 4. VALUATION (P/E vs Industry P/E)
56
+ # Closer to industry P/E = better
57
+ pe_ratio = raw_data.get('pe_ratio')
58
+ industry_pe = raw_data.get('industry_pe')
59
+
60
+ if pe_ratio and industry_pe and industry_pe > 0:
61
+ # Calculate deviation percentage
62
+ deviation = abs(pe_ratio - industry_pe) / industry_pe * 100
63
+
64
+ # 0% deviation = 100 score
65
+ # 50%+ deviation = 0 score
66
+ valuation_score = max(0, 100 - (deviation * 2))
67
+
68
+ # Bonus: Slight preference for undervalued (P/E < Industry)
69
+ if pe_ratio < industry_pe:
70
+ valuation_score = min(100, valuation_score * 1.1)
71
+ else:
72
+ # If P/E is valid but industry PE is missing, maybe assume fair?
73
+ # Or if PE is 0 (loss making), low score?
74
+ if pe_ratio and pe_ratio < 0: valuation_score = 20 # Loss making
75
+ else: valuation_score = 50.0
76
+
77
+ # 5. DIVIDEND YIELD (NEW)
78
+ # Use pre-calculated yield if present, else calculate
79
+ dividend_yield = raw_data.get('dividend_yield')
80
+ if dividend_yield is None:
81
+ dividend = raw_data.get('dividend', 0.0)
82
+ current_price = raw_data.get('current_price', 0.0)
83
+
84
+ if dividend and current_price and current_price > 0:
85
+ dividend_yield = (dividend / current_price) * 100
86
+ else:
87
+ dividend_yield = 0.0
88
+
89
+ # Scale: 0% = 0, 2% = 50, 5%+ = 100
90
+ dividend_score = normalize_linear(dividend_yield, min_val=0, max_val=5)
91
+
92
+ # 6. MOMENTUM (1Y Returns)
93
+ # Scale: -50% = 0, 0% = 50, 100% = 100
94
+ returns_1y = raw_data.get('returns_1y')
95
+ if returns_1y is not None:
96
+ momentum_score = normalize_linear(returns_1y, min_val=-50, max_val=100)
97
+ else:
98
+ momentum_score = 50.0
99
+
100
+ return {
101
+ "Growth": round(growth_score, 1),
102
+ "Profitability": round(profitability_score, 1),
103
+ "Efficiency": round(efficiency_score, 1),
104
+ "Valuation": round(valuation_score, 1),
105
+ "Dividend Yield": round(dividend_score, 1),
106
+ "Momentum": round(momentum_score, 1)
107
+ }
frontend/static/js/results.js CHANGED
@@ -172,19 +172,22 @@ document.addEventListener('DOMContentLoaded', async () => {
172
 
173
  } catch (e) { console.error("Error rendering News:", e); }
174
 
175
- // --- Peer Comparison (Radar SVG) ---
176
  try {
177
- // Draw a polygon based on the 5 metrics:
178
- // 1. Growth, 2. Margin, 3. ROE, 4. Debt (inverted), 5. Strength
 
 
179
 
180
  // Normalize values 0-100 for the chart radius (max 80px)
181
- const p1 = Math.min(Math.max(fundamentals.revenue_growth * 2, 20), 100); // Growth
182
- const p2 = Math.min(Math.max(fundamentals.profit_margin * 2, 20), 100); // Margin
183
- const p3 = Math.min(Math.max(fundamentals.roe * 2, 20), 100); // ROE
184
- const p4 = 100 - Math.min(Math.max(parseFloat(fundamentals.debt_to_equity) * 20, 0), 100); // Low Debt = High Score
185
- const p5 = fundamentals.health_score * 10; // Strength
186
-
187
- // Angles: 0(Top), 72, 144, 216, 288
 
188
  // Center (100, 100), Max Radius 80.
189
  function getPoint(val, angleDeg) {
190
  const angleRad = (angleDeg - 90) * Math.PI / 180;
@@ -196,10 +199,11 @@ document.addEventListener('DOMContentLoaded', async () => {
196
 
197
  const points = [
198
  getPoint(p1, 0),
199
- getPoint(p2, 72),
200
- getPoint(p3, 144),
201
- getPoint(p4, 216),
202
- getPoint(p5, 288)
 
203
  ].join(' ');
204
 
205
  const poly = document.getElementById('peerRadarPolygon');
@@ -210,37 +214,53 @@ document.addEventListener('DOMContentLoaded', async () => {
210
  } catch (e) { console.error("Error rendering Peer Radar:", e); }
211
 
212
 
213
-
214
  // --- NEW: Detailed Metrics Grid ---
215
  try {
216
- const f = fundamentals;
217
- const setTxt = (id, val, suffix = '') => {
218
- const el = document.getElementById(id);
219
- if (el) el.textContent = val !== undefined && val !== null ? val + suffix : '--';
220
- };
221
- const setCol = (id, val) => {
222
- const el = document.getElementById(id);
223
- if (el && typeof val === 'number') {
224
- if (val > 0) el.className = "font-mono font-bold text-lg text-primary";
225
- else if (val < 0) el.className = "font-mono font-bold text-lg text-danger";
226
- else el.className = "font-mono font-bold text-lg text-white";
227
- }
228
- };
229
-
230
- setTxt('d_marketCap', f.market_cap ? f.market_cap.toLocaleString() : '--', ' Cr');
231
- setTxt('d_pe', f.pe_ratio);
232
- setTxt('d_indPe', f.industry_pe);
233
- setTxt('d_pb', f.pb_ratio);
234
- setTxt('d_div', f.dividend_yield, '%');
235
- setTxt('d_eps', f.eps);
236
- setTxt('d_roe', f.roe, '%');
237
- setTxt('d_roce', f.roce, '%');
238
-
239
- setTxt('d_ret1y', f.returns_1y, '%'); setCol('d_ret1y', f.returns_1y);
240
- setTxt('d_ret3y', f.returns_3y, '%'); setCol('d_ret3y', f.returns_3y);
241
- setTxt('d_ret5y', f.returns_5y, '%'); setCol('d_ret5y', f.returns_5y);
242
-
243
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
 
245
  } catch (e) { console.error("Error rendering Detailed Metrics:", e); }
246
 
@@ -250,9 +270,6 @@ document.addEventListener('DOMContentLoaded', async () => {
250
  tbody.innerHTML = '';
251
 
252
  const peerMap = peers.peer_metrics || {};
253
- // Add Target Company first for comparison? Or just peers. Let's add peers.
254
- // Be nice to show Target vs Peers.
255
- // Let's add Target as the first row highlighted
256
  const rows = [];
257
 
258
  // Target Row
@@ -263,6 +280,7 @@ document.addEventListener('DOMContentLoaded', async () => {
263
  roe: fundamentals.roe,
264
  roce: fundamentals.roce,
265
  ret: fundamentals.returns_1y,
 
266
  isTarget: true
267
  });
268
 
@@ -275,6 +293,7 @@ document.addEventListener('DOMContentLoaded', async () => {
275
  roe: pM.roe,
276
  roce: pM.roce,
277
  ret: pM.returns_1y,
 
278
  isTarget: false
279
  });
280
  }
 
172
 
173
  } catch (e) { console.error("Error rendering News:", e); }
174
 
175
+ // --- Peer Comparison (Radar SVG - 6 Axes) ---
176
  try {
177
+ // Axes: Growth(0), Profitability(60), Efficiency(120), Valuation(180), Dividend(240), Momentum(300)
178
+ const scores = fundamentals.normalized_scores || {
179
+ "Growth": 50, "Profitability": 50, "Efficiency": 50, "Valuation": 50, "Dividend Yield": 0, "Momentum": 50
180
+ };
181
 
182
  // Normalize values 0-100 for the chart radius (max 80px)
183
+ const p1 = Math.min(Math.max(scores["Growth"] || 0, 10), 100);
184
+ const p2 = Math.min(Math.max(scores["Profitability"] || 0, 10), 100);
185
+ const p3 = Math.min(Math.max(scores["Efficiency"] || 0, 10), 100);
186
+ const p4 = Math.min(Math.max(scores["Valuation"] || 0, 10), 100);
187
+ const p5 = Math.min(Math.max(scores["Dividend Yield"] || 0, 10), 100);
188
+ const p6 = Math.min(Math.max(scores["Momentum"] || 0, 10), 100);
189
+
190
+ // Angles: 0, 60, 120, 180, 240, 300
191
  // Center (100, 100), Max Radius 80.
192
  function getPoint(val, angleDeg) {
193
  const angleRad = (angleDeg - 90) * Math.PI / 180;
 
199
 
200
  const points = [
201
  getPoint(p1, 0),
202
+ getPoint(p2, 60),
203
+ getPoint(p3, 120),
204
+ getPoint(p4, 180),
205
+ getPoint(p5, 240),
206
+ getPoint(p6, 300)
207
  ].join(' ');
208
 
209
  const poly = document.getElementById('peerRadarPolygon');
 
214
  } catch (e) { console.error("Error rendering Peer Radar:", e); }
215
 
216
 
 
217
  // --- NEW: Detailed Metrics Grid ---
218
  try {
219
+ const grid = document.getElementById('metricsGrid');
220
+ if (grid) {
221
+ grid.innerHTML = ''; // Clear loading state
222
+
223
+ const createCard = (title, value, sub = '') => {
224
+ const card = document.createElement('div');
225
+ card.className = "metric-card";
226
+
227
+ let valColor = "text-white";
228
+ if (title.includes('Return') || title.includes('Growth')) {
229
+ const num = parseFloat(value);
230
+ if (num > 0) valColor = "text-primary";
231
+ else if (num < 0) valColor = "text-danger";
232
+ }
233
+
234
+ card.innerHTML = `
235
+ <span class="metric-title">${title}</span>
236
+ <div class="flex items-baseline gap-1">
237
+ <span class="metric-value ${valColor}">${value !== undefined && value !== null ? value : '--'}</span>
238
+ <span class="metric-sub">${sub}</span>
239
+ </div>
240
+ `;
241
+ return card;
242
+ };
 
 
 
243
 
244
+ const f = fundamentals;
245
+
246
+ // Row 1: Valuation
247
+ grid.appendChild(createCard("Market Cap", f.market_cap ? f.market_cap.toLocaleString() : '--', " Cr"));
248
+ grid.appendChild(createCard("P/E Ratio", f.pe_ratio));
249
+ grid.appendChild(createCard("Ind. P/E", f.industry_pe));
250
+ grid.appendChild(createCard("P/B Ratio", f.pb_ratio));
251
+
252
+ // Row 2: Profitability
253
+ grid.appendChild(createCard("Div. Yield", f.dividend_yield, "%"));
254
+ grid.appendChild(createCard("EPS", f.eps));
255
+ grid.appendChild(createCard("ROE", f.roe, "%"));
256
+ grid.appendChild(createCard("ROCE", f.roce, "%"));
257
+
258
+ // Row 3: Returns
259
+ grid.appendChild(createCard("1Y Return", f.returns_1y, "%"));
260
+ grid.appendChild(createCard("3Y Return", f.returns_3y, "%"));
261
+ grid.appendChild(createCard("5Y Return", f.returns_5y, "%"));
262
+ // Debt/Eq removed per user request
263
+ }
264
 
265
  } catch (e) { console.error("Error rendering Detailed Metrics:", e); }
266
 
 
270
  tbody.innerHTML = '';
271
 
272
  const peerMap = peers.peer_metrics || {};
 
 
 
273
  const rows = [];
274
 
275
  // Target Row
 
280
  roe: fundamentals.roe,
281
  roce: fundamentals.roce,
282
  ret: fundamentals.returns_1y,
283
+ dy: fundamentals.dividend_yield,
284
  isTarget: true
285
  });
286
 
 
293
  roe: pM.roe,
294
  roce: pM.roce,
295
  ret: pM.returns_1y,
296
+ dy: pM.dividend_yield,
297
  isTarget: false
298
  });
299
  }
frontend/templates/results.html CHANGED
@@ -50,6 +50,42 @@
50
  color: #ffffff;
51
  }
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  .bento-card {
54
  background: linear-gradient(145deg, #0a0f1c 0%, #0d1221 100%);
55
  border: 1px solid rgba(255, 255, 255, 0.08);
@@ -237,7 +273,7 @@
237
  class="absolute top-0 left-0 h-full bg-gradient-to-r from-green-900 to-primary w-[0%] transition-all duration-1000 group-hover:brightness-110">
238
  </div>
239
  <span id="newsPosText"
240
- class="absolute top-1/2 left-4 -translate-y-1/2 text-xs font-bold text-black z-10 mix-blend-screen">0%</span>
241
  </div>
242
  </div>
243
  <div class="flex items-center gap-4">
@@ -311,9 +347,7 @@
311
  <div class="flex items-center gap-1.5">
312
  <div class="w-2 h-2 rounded-full bg-primary shadow-glow-sm"></div> TARGET
313
  </div>
314
- <div class="flex items-center gap-1.5">
315
- <div class="w-2 h-2 rounded-full bg-gray-600"></div> AVG
316
- </div>
317
  </div>
318
  </div>
319
  <div class="flex-1 flex items-center justify-center relative z-10">
@@ -325,24 +359,26 @@
325
  <circle cx="100" cy="100" r="40"></circle>
326
  <circle cx="100" cy="100" r="60"></circle>
327
  <circle cx="100" cy="100" r="80"></circle>
328
- <!-- Axis Lines -->
329
- <line x1="100" x2="100" y1="100" y2="10"></line>
330
- <line x1="100" x2="185" y1="100" y2="38"></line>
331
- <line x1="100" x2="158" y1="100" y2="180"></line>
332
- <line x1="100" x2="42" y1="100" y2="180"></line>
333
- <line x1="100" x2="15" y1="100" y2="38"></line>
 
334
  </g>
335
  <!-- Labels -->
336
  <g class="text-[8px] fill-gray-400 font-mono" text-anchor="middle">
337
  <text x="100" y="8">GROWTH</text>
338
- <text x="190" y="40">MARGIN</text>
339
- <text x="160" y="195">ROE</text>
340
- <text x="40" y="195">DEBT</text>
341
- <text x="10" y="40">STRENGTH</text>
 
342
  </g>
343
  <!-- Data Polygon: Target (Green) -->
344
  <polygon id="peerRadarPolygon" class="drop-shadow-[0_0_8px_rgba(51,242,13,0.4)]"
345
- fill="rgba(51, 242, 13, 0.15)" points="100,100 100,100 100,100 100,100 100,100"
346
  stroke="#33f20d" stroke-width="2"></polygon>
347
  </svg>
348
  </div>
@@ -378,58 +414,9 @@
378
  <h3 class="text-xl font-bold text-white">Fundamental Strength</h3>
379
  </div>
380
  <!-- Adjusted grid cols for layout -->
381
- <div class="grid grid-cols-2 lg:grid-cols-4 gap-x-6 gap-y-8 text-sm h-full content-center">
382
-
383
- <!-- Line 1 -->
384
- <div class="space-y-1">
385
- <p class="text-gray-500 text-xs uppercase tracking-wider">Market Cap</p>
386
- <p class="font-mono font-bold text-lg text-white" id="d_marketCap">--</p>
387
- </div>
388
- <div class="space-y-1">
389
- <p class="text-gray-500 text-xs uppercase tracking-wider">P/E Ratio</p>
390
- <p class="font-mono font-bold text-lg text-primary" id="d_pe">--</p>
391
- </div>
392
- <div class="space-y-1">
393
- <p class="text-gray-500 text-xs uppercase tracking-wider">Ind. P/E</p>
394
- <p class="font-mono font-bold text-lg text-gray-300" id="d_indPe">--</p>
395
- </div>
396
- <div class="space-y-1">
397
- <p class="text-gray-500 text-xs uppercase tracking-wider">P/B Ratio</p>
398
- <p class="font-mono font-bold text-lg text-white" id="d_pb">--</p>
399
- </div>
400
-
401
- <!-- Line 2 -->
402
- <div class="space-y-1">
403
- <p class="text-gray-500 text-xs uppercase tracking-wider">Dividend Yield</p>
404
- <p class="font-mono font-bold text-lg text-white" id="d_div">--</p>
405
- </div>
406
- <div class="space-y-1">
407
- <p class="text-gray-500 text-xs uppercase tracking-wider">EPS</p>
408
- <p class="font-mono font-bold text-lg text-white" id="d_eps">--</p>
409
- </div>
410
- <div class="space-y-1">
411
- <p class="text-gray-500 text-xs uppercase tracking-wider">ROE</p>
412
- <p class="font-mono font-bold text-lg text-white" id="d_roe">--</p>
413
- </div>
414
- <div class="space-y-1">
415
- <p class="text-gray-500 text-xs uppercase tracking-wider">ROCE</p>
416
- <p class="font-mono font-bold text-lg text-white" id="d_roce">--</p>
417
- </div>
418
-
419
- <!-- Line 3 Returns -->
420
- <div class="space-y-1">
421
- <p class="text-gray-500 text-xs uppercase tracking-wider">1Y Return</p>
422
- <p class="font-mono font-bold text-lg" id="d_ret1y">--</p>
423
- </div>
424
- <div class="space-y-1">
425
- <p class="text-gray-500 text-xs uppercase tracking-wider">3Y Return</p>
426
- <p class="font-mono font-bold text-lg" id="d_ret3y">--</p>
427
- </div>
428
- <div class="space-y-1">
429
- <p class="text-gray-500 text-xs uppercase tracking-wider">5Y Return</p>
430
- <p class="font-mono font-bold text-lg" id="d_ret5y">--</p>
431
- </div>
432
-
433
  </div>
434
  </div>
435
 
 
50
  color: #ffffff;
51
  }
52
 
53
+ .metric-card {
54
+ background: rgba(255, 255, 255, 0.03);
55
+ border: 1px solid rgba(255, 255, 255, 0.08);
56
+ border-radius: 12px;
57
+ padding: 16px;
58
+ display: flex;
59
+ flex-direction: column;
60
+ gap: 4px;
61
+ transition: all 0.2s ease;
62
+ }
63
+
64
+ .metric-card:hover {
65
+ background: rgba(255, 255, 255, 0.05);
66
+ border-color: rgba(51, 242, 13, 0.2);
67
+ transform: translateY(-2px);
68
+ }
69
+
70
+ .metric-title {
71
+ font-size: 0.75rem;
72
+ text-transform: uppercase;
73
+ letter-spacing: 0.05em;
74
+ color: #94a3b8;
75
+ }
76
+
77
+ .metric-value {
78
+ font-family: 'JetBrains Mono', monospace;
79
+ font-size: 1.25rem;
80
+ font-weight: 700;
81
+ color: #ffffff;
82
+ }
83
+
84
+ .metric-sub {
85
+ font-size: 0.75rem;
86
+ color: #64748b;
87
+ }
88
+
89
  .bento-card {
90
  background: linear-gradient(145deg, #0a0f1c 0%, #0d1221 100%);
91
  border: 1px solid rgba(255, 255, 255, 0.08);
 
273
  class="absolute top-0 left-0 h-full bg-gradient-to-r from-green-900 to-primary w-[0%] transition-all duration-1000 group-hover:brightness-110">
274
  </div>
275
  <span id="newsPosText"
276
+ class="absolute top-1/2 left-4 -translate-y-1/2 text-xs font-bold text-white z-10 mix-blend-screen">0%</span>
277
  </div>
278
  </div>
279
  <div class="flex items-center gap-4">
 
347
  <div class="flex items-center gap-1.5">
348
  <div class="w-2 h-2 rounded-full bg-primary shadow-glow-sm"></div> TARGET
349
  </div>
350
+ <!-- Removed AVG logic for now as we focus on raw peers but chart still compares? -->
 
 
351
  </div>
352
  </div>
353
  <div class="flex-1 flex items-center justify-center relative z-10">
 
359
  <circle cx="100" cy="100" r="40"></circle>
360
  <circle cx="100" cy="100" r="60"></circle>
361
  <circle cx="100" cy="100" r="80"></circle>
362
+ <!-- Axis Lines (6 Axis) -->
363
+ <line x1="100" y1="100" x2="100" y2="10"></line> <!-- 0 deg (Top) -->
364
+ <line x1="100" y1="100" x2="178" y2="55"></line> <!-- 60 deg -->
365
+ <line x1="100" y1="100" x2="178" y2="145"></line> <!-- 120 deg -->
366
+ <line x1="100" y1="100" x2="100" y2="190"></line> <!-- 180 deg -->
367
+ <line x1="100" y1="100" x2="22" y2="145"></line> <!-- 240 deg -->
368
+ <line x1="100" y1="100" x2="22" y2="55"></line> <!-- 300 deg -->
369
  </g>
370
  <!-- Labels -->
371
  <g class="text-[8px] fill-gray-400 font-mono" text-anchor="middle">
372
  <text x="100" y="8">GROWTH</text>
373
+ <text x="190" y="55">PROFIT</text>
374
+ <text x="190" y="150">EFFICIENCY</text>
375
+ <text x="100" y="198">VALUATION</text>
376
+ <text x="10" y="150">DIVIDEND</text>
377
+ <text x="10" y="55">MOMENTUM</text>
378
  </g>
379
  <!-- Data Polygon: Target (Green) -->
380
  <polygon id="peerRadarPolygon" class="drop-shadow-[0_0_8px_rgba(51,242,13,0.4)]"
381
+ fill="rgba(51, 242, 13, 0.15)" points="100,100 100,100 100,100 100,100 100,100 100,100"
382
  stroke="#33f20d" stroke-width="2"></polygon>
383
  </svg>
384
  </div>
 
414
  <h3 class="text-xl font-bold text-white">Fundamental Strength</h3>
415
  </div>
416
  <!-- Adjusted grid cols for layout -->
417
+ <div class="grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm h-full content-center" id="metricsGrid">
418
+ <!-- Cards will be injected by JS displayDetailedMetrics() -->
419
+ <div class="col-span-full text-center text-gray-500 animate-pulse">Loading detailed metrics...</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  </div>
421
  </div>
422
 
verify_peer_logic.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from backend.utils.peer_comparison import calculate_normalized_scores_v2
2
+ import json
3
+
4
+ def test_normalization():
5
+ print("Testing Normalization Logic...")
6
+
7
+ # Case 1: High Growth, High Profit
8
+ sample_1 = {
9
+ 'returns_5y': 150.0, # Expect 100
10
+ 'roe': 35.0, # Expect 100
11
+ 'roce': 40.0, # Expect 100
12
+ 'pe_ratio': 20.0,
13
+ 'industry_pe': 25.0, # Undervalued -> Expect high score
14
+ 'dividend_yield': 1.5, # Expect ~37.5 (1.5/4 * 100 ?) No, range 0-5. 1.5/5 * 100 = 30? Wait formula is linear 0-5.
15
+ 'returns_1y': 80.0 # Expect High
16
+ }
17
+
18
+ scores_1 = calculate_normalized_scores_v2(sample_1)
19
+ print(f"\nSample 1 (High Performance):\n{json.dumps(scores_1, indent=2)}")
20
+
21
+ # Case 2: Poor Performance
22
+ sample_2 = {
23
+ 'returns_5y': -30.0, # Expect 0 (min -20)
24
+ 'roe': -5.0, # Expect 0
25
+ 'roce': 2.0, # Expect low
26
+ 'pe_ratio': 50.0,
27
+ 'industry_pe': 25.0, # Overvalued -> Expect low score
28
+ 'dividend_yield': 0.0, # Expect 0
29
+ 'returns_1y': -60.0 # Expect 0 (min -50)
30
+ }
31
+
32
+ scores_2 = calculate_normalized_scores_v2(sample_2)
33
+ print(f"\nSample 2 (Poor Performance):\n{json.dumps(scores_2, indent=2)}")
34
+
35
+ # Case 3: Average / Missing
36
+ sample_3 = {
37
+ 'pe_ratio': 25.0,
38
+ 'industry_pe': 25.0 # Fair value
39
+ # Others missing -> Expect 50
40
+ }
41
+
42
+ scores_3 = calculate_normalized_scores_v2(sample_3)
43
+ print(f"\nSample 3 (Average/Missing):\n{json.dumps(scores_3, indent=2)}")
44
+
45
+ if __name__ == "__main__":
46
+ try:
47
+ test_normalization()
48
+ print("\n✅ Verification Successful: Logic runs without errors.")
49
+ except Exception as e:
50
+ print(f"\n❌ Verification Failed: {e}")