File size: 3,525 Bytes
c3aab0c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247775e
 
 
 
 
 
 
 
 
c3aab0c
247775e
c3aab0c
247775e
c3aab0c
247775e
c3aab0c
 
 
 
 
 
 
 
 
 
247775e
 
 
 
 
 
 
 
 
 
 
 
 
c3aab0c
 
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

import pandas as pd
from analytics.attribution import AttributionEngine

def test_attribution_logic():
    print("Testing Attribution Logic...")
    
    # Mock Data
    # Scenario: 
    # - AAPL: Overweight (Held 5%, Bench 4%). Return +10%. Should be Contributor.
    # - MSFT: Excluded (Held 0%, Bench 6%). Return +10%. Should be Detractor (Missed Rally).
    # - GOOG: Neutral (Held 2%, Bench 2%). Return -5%. Active Contrib 0.
    portfolio_weights = {"AAPL": 0.05, "MSFT": 0.0, "GOOG": 0.02}
    benchmark_weights = {"AAPL": 0.04, "MSFT": 0.06, "GOOG": 0.02}
    
    # Returns for the period
    returns_data = {"AAPL": 0.10, "MSFT": 0.10, "GOOG": -0.05}
    asset_returns = pd.Series(returns_data)
    
    sector_map = {
        "AAPL": "Technology",
        "MSFT": "Technology", 
        "GOOG": "Communication Services"
    }
    
    engine = AttributionEngine()
    report = engine.generate_attribution_report(
        portfolio_weights, 
        benchmark_weights, 
        asset_returns, 
        sector_map
    )
    
    print("\n--- Attribution Report Generated ---")
    print(f"Total Active Return: {report.total_active_return:.4f}")
    
    print("\n[Top Contributors]")
    for item in report.top_contributors:
        print(item)
        
    print("\n[Top Detractors]")
    for item in report.top_detractors:
        print(item)
        
    # Validation Logic
    # MSFT Active Weight = 0 - 0.06 = -0.06
    # MSFT Active Contrib = -0.06 * 0.10 = -0.006 (Detractor)
    
    msft = next((x for x in report.top_detractors if x['Ticker'] == 'MSFT'), None)
    if msft:
        # Signage Logic Verification
        # MSFT is Excluded and Return is +10%. This is BAD. Should be 'Drag' or 'Missed Rally'
        print(f"MSFT Reasoning: {msft.get('Reasoning', 'N/A')}")
        
        if "Drag" in msft.get('Reasoning', '') or "Missed" in msft.get('Reasoning', ''):
             print("SUCCESS: MSFT correctly identified as Missed Rally (Drag).")
        else:
             print(f"FAILURE: MSFT reasoning wrong: {msft}")
             
        if msft['Status'] == "Excluded" and float(msft['Active_Contribution']) < 0:
            print("SUCCESS: MSFT correctly identified as Excluded Detractor.")
        else:
            print(f"FAILURE: MSFT status/logic wrong: {msft}")
    else:
        print("FAILURE: MSFT not found in detractors.")

    # AAPL Active Weight = 0.05 - 0.04 = +0.01
    # AAPL Active Contrib = +0.01 * 0.10 = +0.001 (Contributor)
    aapl = next((x for x in report.top_contributors if x['Ticker'] == 'AAPL'), None)
    current_return = float(aapl['Active_Contribution']) if aapl else 0
    if aapl and current_return > 0:
         print("SUCCESS: AAPL correctly identified as Overweight Contributor.")
    else:
         print(f"FAILURE: AAPL logic wrong. {aapl}")

    # VERIFICATION: Sector Exposure Truth Table
    print("\n[Sector Exposure Truth Table]")
    tech_exposure = next((x for x in report.sector_exposure if x['Sector'] == 'Technology'), None)
    if tech_exposure:
        print(f"Technology Exposure: {tech_exposure}")
        # We hold AAPL (5%) vs Bench AAPL+MSFT (10%) -> Underweight
        if tech_exposure['Status'] == "Underweight":
            print("SUCCESS: Technology correctly identified as UNDERWEIGHT (not Excluded).")
        else:
             print(f"FAILURE: Technology status wrong: {tech_exposure['Status']}")
    else:
        print("FAILURE: Technology sector missing from report.")

if __name__ == "__main__":
    test_attribution_logic()