File size: 3,559 Bytes
f15861f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import pandas as pd
from typing import Dict, Any

def safe_float(val):
    if val is None or val == '':
        return 0.0
    try:
        return float(val)
    except:
        return 0.0

def normalize_linear(value: float, min_val: float, max_val: float) -> float:
    """
    Linear normalization to 0-100 scale
    """
    if value is None or pd.isna(value):
        return 50.0  # Neutral if missing
    
    if value <= min_val:
        return 0.0
    elif value >= max_val:
        return 100.0
    else:
        return ((value - min_val) / (max_val - min_val)) * 100.0

def calculate_normalized_scores_v2(raw_data: Dict[str, Any]) -> Dict[str, float]:
    """
    Enhanced normalization with Dividend Yield
    """
    
    # 1. GROWTH (5Y Returns)
    # Scale: -20% = 0, 0% = 40, 100% = 100
    returns_5y = raw_data.get('returns_5y')
    if returns_5y is not None:
        growth_score = normalize_linear(returns_5y, min_val=-20, max_val=100)
    else:
        growth_score = 50.0
    
    # 2. PROFITABILITY (ROE)
    # Scale: 0% = 0, 15% = 50, 30%+ = 100
    roe = raw_data.get('roe')
    if roe is not None:
        profitability_score = normalize_linear(roe, min_val=0, max_val=30)
    else:
        profitability_score = 50.0
    
    # 3. EFFICIENCY (ROCE)
    # Scale: 0% = 0, 15% = 50, 30%+ = 100
    roce = raw_data.get('roce')
    if roce is not None:
        efficiency_score = normalize_linear(roce, min_val=0, max_val=30)
    else:
        efficiency_score = 50.0
    
    # 4. VALUATION (P/E vs Industry P/E)
    # Closer to industry P/E = better
    pe_ratio = raw_data.get('pe_ratio')
    industry_pe = raw_data.get('industry_pe')
    
    if pe_ratio and industry_pe and industry_pe > 0:
        # Calculate deviation percentage
        deviation = abs(pe_ratio - industry_pe) / industry_pe * 100
        
        # 0% deviation = 100 score
        # 50%+ deviation = 0 score
        valuation_score = max(0, 100 - (deviation * 2))
        
        # Bonus: Slight preference for undervalued (P/E < Industry)
        if pe_ratio < industry_pe:
            valuation_score = min(100, valuation_score * 1.1)
    else:
        # If P/E is valid but industry PE is missing, maybe assume fair?
        # Or if PE is 0 (loss making), low score?
        if pe_ratio and pe_ratio < 0: valuation_score = 20 # Loss making
        else: valuation_score = 50.0
    
    # 5. DIVIDEND YIELD (NEW)
    # Use pre-calculated yield if present, else calculate
    dividend_yield = raw_data.get('dividend_yield')
    if dividend_yield is None:
        dividend = raw_data.get('dividend', 0.0)
        current_price = raw_data.get('current_price', 0.0)
        
        if dividend and current_price and current_price > 0:
            dividend_yield = (dividend / current_price) * 100
        else:
            dividend_yield = 0.0
            
    # Scale: 0% = 0, 2% = 50, 5%+ = 100
    dividend_score = normalize_linear(dividend_yield, min_val=0, max_val=5)
    
    # 6. MOMENTUM (1Y Returns)
    # Scale: -50% = 0, 0% = 50, 100% = 100
    returns_1y = raw_data.get('returns_1y')
    if returns_1y is not None:
        momentum_score = normalize_linear(returns_1y, min_val=-50, max_val=100)
    else:
        momentum_score = 50.0
    
    return {
        "Growth": round(growth_score, 1),
        "Profitability": round(profitability_score, 1),
        "Efficiency": round(efficiency_score, 1),
        "Valuation": round(valuation_score, 1),
        "Dividend Yield": round(dividend_score, 1),
        "Momentum": round(momentum_score, 1)
    }