AJAY KASU commited on
Commit
c3aab0c
·
1 Parent(s): cafdd88

Feat: Max Weight Constraint & NLP Logic

Browse files
.DS_Store ADDED
Binary file (8.2 kB). View file
 
ai/ai_reporter.py CHANGED
@@ -36,6 +36,8 @@ class AIReporter:
36
  # Get current date in a specific format (e.g., "February 03, 2026")
37
  current_date = datetime.now().strftime("%B %d, %Y")
38
 
 
 
39
  # Format the user prompt
40
  # We assume ATTRIBUTION_PROMPT_TEMPLATE handles the rest, but we force the date in context
41
  user_prompt = f"""
@@ -46,8 +48,8 @@ INSTRUCTION: Start your commentary exactly with the header: "Market Commentary -
46
  total_active_return=attribution_report.total_active_return * 100, # Convert to %
47
  allocation_effect=attribution_report.allocation_effect * 100,
48
  selection_effect=attribution_report.selection_effect * 100,
49
- top_contributors=", ".join(attribution_report.top_contributors),
50
- top_detractors=", ".join(attribution_report.top_detractors),
51
  current_date=current_date # Pass date to template
52
  )
53
 
 
36
  # Get current date in a specific format (e.g., "February 03, 2026")
37
  current_date = datetime.now().strftime("%B %d, %Y")
38
 
39
+ import json
40
+
41
  # Format the user prompt
42
  # We assume ATTRIBUTION_PROMPT_TEMPLATE handles the rest, but we force the date in context
43
  user_prompt = f"""
 
48
  total_active_return=attribution_report.total_active_return * 100, # Convert to %
49
  allocation_effect=attribution_report.allocation_effect * 100,
50
  selection_effect=attribution_report.selection_effect * 100,
51
+ top_contributors=json.dumps(attribution_report.top_contributors, indent=2),
52
+ top_detractors=json.dumps(attribution_report.top_detractors, indent=2),
53
  current_date=current_date # Pass date to template
54
  )
55
 
ai/prompts.py CHANGED
@@ -1,4 +1,5 @@
1
  # System Prompt for the Portfolio Manager Persona
 
2
  SYSTEM_PROMPT = """You are a Senior Portfolio Manager at a top-tier Asset Management firm (e.g., Goldman Sachs, BlackRock).
3
  Your goal is to write a concise, professional, and insightful performance commentary for a High Net Worth Application.
4
  Your tone should be:
@@ -6,6 +7,14 @@ Your tone should be:
6
  2. Mathematically precise (cite the numbers).
7
  3. Explanatory (explain 'why' something happened).
8
 
 
 
 
 
 
 
 
 
9
  Avoid generic financial advice. Focus strictly on the attribution data provided.
10
  """
11
 
@@ -18,21 +27,27 @@ Write a "Trailing 30-Day Risk & Performance Attribution" report relative to the
18
 
19
  ## Brinson-Fachler Attribution Data (Trailing 30 Days)
20
  - Total Active Return (Alpha): {total_active_return:.2f}%
21
- - Allocation Effect (Impact of Exclusions): {allocation_effect:.2f}%
22
  - Selection Effect (Impact of Stock Picking): {selection_effect:.2f}%
23
 
24
- ## Attribution Detail
25
- - Top Active Contributors: {top_contributors}
26
- - Top Active Detractors: {top_detractors}
 
 
 
27
 
28
  ## Guidelines for the Narrative:
29
- 1. **Timeframe**: Use the EXACT date provided. Write "For the trailing 30-day period ending {current_date}..." DO NOT generalize to "the month of...".
30
- 2. **Ticker Validation (CRITICAL)**: Always verify tickers. ExxonMobil is XOM, Chevron is CVX. Do NOT swap them.
31
  3. **Attribution Logic**:
32
- - If a sector is excluded (0% weight), attribute ALL gains/losses to the **Allocation Effect**.
33
- - Do NOT mention 'Selection Effect' for sectors where we hold 0% (e.g., if Energy is excluded, you didn't "select" bad Energy stocks, you just didn't own the sector).
34
- 4. **Detractor Clarity**:
35
- - If an EXCLUDED stock (like AMZN, XOM, CVX) is listed as a "Top Detractor", explicitly state: "We suffered a drag because the portfolio missed out on the rally in [Stock] due to exclusion constraints."
 
 
 
36
 
37
  Write a professional, concise 3-paragraph commentary.
38
  """
 
1
  # System Prompt for the Portfolio Manager Persona
2
+ # System Prompt for the Portfolio Manager Persona
3
  SYSTEM_PROMPT = """You are a Senior Portfolio Manager at a top-tier Asset Management firm (e.g., Goldman Sachs, BlackRock).
4
  Your goal is to write a concise, professional, and insightful performance commentary for a High Net Worth Application.
5
  Your tone should be:
 
7
  2. Mathematically precise (cite the numbers).
8
  3. Explanatory (explain 'why' something happened).
9
 
10
+ ## GOLDMAN RULES (STRICT COMPLIANCE)
11
+ 1. **The Exclusion Rule**: If a stock or sector has "Status": "Excluded", NEVER refer to it as a "Holding". We don't own it. Its negative contribution is a "Missed Opportunity" or "Drag from Benchbark Rally".
12
+ 2. **The Active Return Rule**: Only call a stock a "Contributor" if its "Active_Contribution" is POSITIVE.
13
+ - If we don't own a stock (Weight = 0%) and it went UP, it is a DETRACTOR (Active Contribution is NEGATIVE).
14
+ - If we don't own a stock and it went DOWN, it is a CONTRIBUTOR (Active Contribution is POSITIVE).
15
+ 3. **The GICS Rule**: Adhere strictly to the "Sector" field provided in the input JSON. Do not hallucinate sectors. (e.g. AMZN is Consumer Discretionary, XOM is Energy).
16
+ 4. **Data Grounding**: Do not cite any data not present in the provided JSON "Truth Tables".
17
+
18
  Avoid generic financial advice. Focus strictly on the attribution data provided.
19
  """
20
 
 
27
 
28
  ## Brinson-Fachler Attribution Data (Trailing 30 Days)
29
  - Total Active Return (Alpha): {total_active_return:.2f}%
30
+ - Allocation Effect (Impact of Sector Weights): {allocation_effect:.2f}%
31
  - Selection Effect (Impact of Stock Picking): {selection_effect:.2f}%
32
 
33
+ ## Attribution Detail (The "Truth Tables")
34
+ **Top Active Contributors (JSON)**:
35
+ {top_contributors}
36
+
37
+ **Top Active Detractors (JSON)**:
38
+ {top_detractors}
39
 
40
  ## Guidelines for the Narrative:
41
+ 1. **Timeframe**: Use the EXACT date provided: "{current_date}".
42
+ 2. **Ticker Validation**: Use the Ticker symbols exactly as listed.
43
  3. **Attribution Logic**:
44
+ - If a sector is excluded (Allocation Effect), describe it as a strategic decision.
45
+ - For Detractors that are "Excluded" (e.g. Status: Excluded), say: "The portfolio faced a headwind due to the exclusion of [Sector/Stock], which rallied during the period."
46
+ - DO NOT say "We held [Excluded Stock]".
47
+ 4. **Chain of Thought (Mental Check)**:
48
+ - First, scan the JSON. Identify the "Status" of the top movers.
49
+ - Second, match the Sector to the Stock.
50
+ - Third, write the commentary based ONLY on these facts.
51
 
52
  Write a professional, concise 3-paragraph commentary.
53
  """
analytics/__pycache__/attribution.cpython-39.pyc CHANGED
Binary files a/analytics/__pycache__/attribution.cpython-39.pyc and b/analytics/__pycache__/attribution.cpython-39.pyc differ
 
analytics/attribution.py CHANGED
@@ -87,14 +87,38 @@ class AttributionEngine:
87
  total_interaction = attr_df['interaction'].sum()
88
 
89
  # Calculate Top Contributors/Detractors to active return
90
- # Active Weight * Asset Return? Or Contribution to Active Return?
91
- # Contribution to Active Return = w_p*r_a - w_b*r_a ...
92
  df['active_weight'] = df['wp'] - df['wb']
93
- df['contribution'] = df['active_weight'] * df['ret'] # Simple approx
94
 
 
95
  sorted_contrib = df.sort_values(by='contribution', ascending=False)
96
- top_contributors = sorted_contrib.head(5).index.tolist()
97
- top_detractors = sorted_contrib.tail(5).index.tolist()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
  # Narrative skeleton (to be filled by AI)
100
  narrative_raw = (
 
87
  total_interaction = attr_df['interaction'].sum()
88
 
89
  # Calculate Top Contributors/Detractors to active return
90
+ # Active Weight * Asset Return (Truth Table Logic)
 
91
  df['active_weight'] = df['wp'] - df['wb']
92
+ df['contribution'] = df['active_weight'] * df['ret']
93
 
94
+ # Sort by active contribution
95
  sorted_contrib = df.sort_values(by='contribution', ascending=False)
96
+
97
+ def get_status(row):
98
+ if row['wp'] == 0.0 and row['wb'] > 0.0:
99
+ return "Excluded"
100
+ elif row['wp'] > row['wb']:
101
+ return "Overweight"
102
+ elif row['wp'] < row['wb']:
103
+ return "Underweight"
104
+ else:
105
+ return "Neutral"
106
+
107
+ def build_truth_table(dataframe, n=5):
108
+ results = []
109
+ for ticker, row in dataframe.head(n).iterrows():
110
+ results.append({
111
+ "Ticker": ticker,
112
+ "Sector": row['sector'],
113
+ "Status": get_status(row),
114
+ "Active_Contribution": f"{row['contribution']:.4f}",
115
+ "Return": f"{row['ret']:.2%}"
116
+ })
117
+ return results
118
+
119
+ # Top 5 Winners (Contributors) & Losers (Detractors)
120
+ top_contributors = build_truth_table(sorted_contrib, 5)
121
+ top_detractors = build_truth_table(sorted_contrib.sort_values(by='contribution', ascending=True), 5)
122
 
123
  # Narrative skeleton (to be filled by AI)
124
  narrative_raw = (
api/static/index.html CHANGED
@@ -443,10 +443,29 @@
443
  // For demo, we send "None" effectively.
444
  }
445
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  const payload = {
447
  "client_id": "Web_User",
448
  "excluded_sectors": excluded,
449
  "excluded_tickers": excludedTickers,
 
450
  "initial_investment": 100000
451
  };
452
 
 
443
  // For demo, we send "None" effectively.
444
  }
445
 
446
+ // Extract Max Weight (e.g. "limit to 2%", "max weight 5%")
447
+ let maxWeight = null;
448
+ // Matches: "limit... 2%" or "weight... 0.05"
449
+ // Simple Regex: Search for number followed optionally by %
450
+ const weightMatch = lowerInput.match(/(?:limit|max|weight).*?(\d+(?:\.\d+)?)\s*%/);
451
+ if (weightMatch) {
452
+ const val = parseFloat(weightMatch[1]);
453
+ if (val > 0) {
454
+ maxWeight = val / 100.0; // Convert 2% -> 0.02
455
+ }
456
+ } else {
457
+ // Try decimal "0.05"
458
+ const decimalMatch = lowerInput.match(/(?:limit|max|weight).*?(\d+\.\d+)/);
459
+ if (decimalMatch) {
460
+ maxWeight = parseFloat(decimalMatch[1]);
461
+ }
462
+ }
463
+
464
  const payload = {
465
  "client_id": "Web_User",
466
  "excluded_sectors": excluded,
467
  "excluded_tickers": excludedTickers,
468
+ "max_weight": maxWeight,
469
  "initial_investment": 100000
470
  };
471
 
core/__pycache__/schema.cpython-39.pyc CHANGED
Binary files a/core/__pycache__/schema.cpython-39.pyc and b/core/__pycache__/schema.cpython-39.pyc differ
 
core/schema.py CHANGED
@@ -26,6 +26,7 @@ class OptimizationRequest(BaseModel):
26
  initial_investment: float = 100000.0
27
  excluded_sectors: List[str] = Field(default_factory=list, description="List of sectors to exclude (e.g., ['Energy'])")
28
  excluded_tickers: List[str] = Field(default_factory=list, description="List of specific tickers to exclude (e.g., ['AMZN'])")
 
29
  benchmark: str = "^GSPC"
30
 
31
  class Config:
@@ -92,6 +93,6 @@ class AttributionReport(BaseModel):
92
  allocation_effect: float
93
  selection_effect: float
94
  total_active_return: float
95
- top_contributors: List[str]
96
- top_detractors: List[str]
97
  narrative: str
 
26
  initial_investment: float = 100000.0
27
  excluded_sectors: List[str] = Field(default_factory=list, description="List of sectors to exclude (e.g., ['Energy'])")
28
  excluded_tickers: List[str] = Field(default_factory=list, description="List of specific tickers to exclude (e.g., ['AMZN'])")
29
+ max_weight: Optional[float] = Field(None, description="Maximum weight for any single asset (e.g., 0.05)")
30
  benchmark: str = "^GSPC"
31
 
32
  class Config:
 
93
  allocation_effect: float
94
  selection_effect: float
95
  total_active_return: float
96
+ top_contributors: List[Dict]
97
+ top_detractors: List[Dict]
98
  narrative: str
data/optimizer.py CHANGED
@@ -27,7 +27,8 @@ class PortfolioOptimizer:
27
  benchmark_weights: pd.DataFrame,
28
  sector_map: Dict[str, str],
29
  excluded_sectors: List[str],
30
- excluded_tickers: List[str] = None) -> OptimizationResult:
 
31
  """
32
  Solves the tracking error minimization problem.
33
 
@@ -100,7 +101,11 @@ class PortfolioOptimizer:
100
  min_avg_weight = 1.0 / n_active
101
  dynamic_max = max(0.20, min_avg_weight * 1.5)
102
 
103
- MAX_WEIGHT_LIMIT = dynamic_max
 
 
 
 
104
  logger.info(f"DEBUG: Active Assets={n_active}, Min Avg={min_avg_weight:.4f}, Dynamic Max Limit={MAX_WEIGHT_LIMIT:.4f}")
105
 
106
  constraints = [
 
27
  benchmark_weights: pd.DataFrame,
28
  sector_map: Dict[str, str],
29
  excluded_sectors: List[str],
30
+ excluded_tickers: List[str] = None,
31
+ max_weight: float = None) -> OptimizationResult:
32
  """
33
  Solves the tracking error minimization problem.
34
 
 
101
  min_avg_weight = 1.0 / n_active
102
  dynamic_max = max(0.20, min_avg_weight * 1.5)
103
 
104
+ if max_weight and max_weight > min_avg_weight:
105
+ logger.info(f"Applying User-Defined Max Weight: {max_weight}")
106
+ MAX_WEIGHT_LIMIT = max_weight
107
+ else:
108
+ MAX_WEIGHT_LIMIT = dynamic_max
109
  logger.info(f"DEBUG: Active Assets={n_active}, Min Avg={min_avg_weight:.4f}, Dynamic Max Limit={MAX_WEIGHT_LIMIT:.4f}")
110
 
111
  constraints = [
debug_attribution_logic.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import pandas as pd
3
+ from analytics.attribution import AttributionEngine
4
+
5
+ def test_attribution_logic():
6
+ print("Testing Attribution Logic...")
7
+
8
+ # Mock Data
9
+ # Scenario:
10
+ # - AAPL: Overweight (Held 5%, Bench 4%). Return +10%. Should be Contributor.
11
+ # - MSFT: Excluded (Held 0%, Bench 6%). Return +10%. Should be Detractor (Missed Rally).
12
+ # - GOOG: Neutral (Held 2%, Bench 2%). Return -5%. Active Contrib 0.
13
+ portfolio_weights = {"AAPL": 0.05, "MSFT": 0.0, "GOOG": 0.02}
14
+ benchmark_weights = {"AAPL": 0.04, "MSFT": 0.06, "GOOG": 0.02}
15
+
16
+ # Returns for the period
17
+ returns_data = {"AAPL": 0.10, "MSFT": 0.10, "GOOG": -0.05}
18
+ asset_returns = pd.Series(returns_data)
19
+
20
+ sector_map = {
21
+ "AAPL": "Technology",
22
+ "MSFT": "Technology",
23
+ "GOOG": "Communication Services"
24
+ }
25
+
26
+ engine = AttributionEngine()
27
+ report = engine.generate_attribution_report(
28
+ portfolio_weights,
29
+ benchmark_weights,
30
+ asset_returns,
31
+ sector_map
32
+ )
33
+
34
+ print("\n--- Attribution Report Generated ---")
35
+ print(f"Total Active Return: {report.total_active_return:.4f}")
36
+
37
+ print("\n[Top Contributors]")
38
+ for item in report.top_contributors:
39
+ print(item)
40
+
41
+ print("\n[Top Detractors]")
42
+ for item in report.top_detractors:
43
+ print(item)
44
+
45
+ # Validation Logic
46
+ # MSFT Active Weight = 0 - 0.06 = -0.06
47
+ # MSFT Active Contrib = -0.06 * 0.10 = -0.006 (Detractor)
48
+
49
+ msft = next((x for x in report.top_detractors if x['Ticker'] == 'MSFT'), None)
50
+ if msft:
51
+ if msft['Status'] == "Excluded" and float(msft['Active_Contribution']) < 0:
52
+ print("\nSUCCESS: MSFT correctly identified as Excluded Detractor.")
53
+ else:
54
+ print(f"\nFAILURE: MSFT status/logic wrong: {msft}")
55
+ else:
56
+ print("\nFAILURE: MSFT not found in detractors.")
57
+
58
+ # AAPL Active Weight = 0.05 - 0.04 = +0.01
59
+ # AAPL Active Contrib = +0.01 * 0.10 = +0.001 (Contributor)
60
+ aapl = next((x for x in report.top_contributors if x['Ticker'] == 'AAPL'), None)
61
+ current_return = float(aapl['Active_Contribution']) if aapl else 0
62
+ if aapl and current_return > 0:
63
+ print("SUCCESS: AAPL correctly identified as Overweight Contributor.")
64
+ else:
65
+ print(f"FAILURE: AAPL logic wrong. {aapl}")
66
+
67
+ if __name__ == "__main__":
68
+ test_attribution_logic()
debug_output.txt ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /opt/anaconda3/lib/python3.9/site-packages/pandas/core/computation/expressions.py:21: UserWarning: Pandas requires version '2.8.4' or newer of 'numexpr' (version '2.8.1' currently installed).
2
+ from pandas.core.computation.check import NUMEXPR_INSTALLED
3
+ /opt/anaconda3/lib/python3.9/site-packages/pandas/core/arrays/masked.py:60: UserWarning: Pandas requires version '1.3.6' or newer of 'bottleneck' (version '1.3.4' currently installed).
4
+ from pandas.core import (
5
+ Testing Attribution Logic...
6
+
7
+ --- Attribution Report Generated ---
8
+ Total Active Return: -0.0045
9
+
10
+ [Top Contributors]
11
+ {'Ticker': 'AAPL', 'Sector': 'Technology', 'Status': 'Overweight', 'Active_Contribution': '0.0010', 'Return': '10.00%'}
12
+ {'Ticker': 'GOOG', 'Sector': 'Communication Services', 'Status': 'Neutral', 'Active_Contribution': '-0.0000', 'Return': '-5.00%'}
13
+ {'Ticker': 'MSFT', 'Sector': 'Technology', 'Status': 'Excluded', 'Active_Contribution': '-0.0060', 'Return': '10.00%'}
14
+
15
+ [Top Detractors]
16
+ {'Ticker': 'MSFT', 'Sector': 'Technology', 'Status': 'Excluded', 'Active_Contribution': '-0.0060', 'Return': '10.00%'}
17
+ {'Ticker': 'GOOG', 'Sector': 'Communication Services', 'Status': 'Neutral', 'Active_Contribution': '-0.0000', 'Return': '-5.00%'}
18
+ {'Ticker': 'AAPL', 'Sector': 'Technology', 'Status': 'Overweight', 'Active_Contribution': '0.0010', 'Return': '10.00%'}
19
+
20
+ SUCCESS: MSFT correctly identified as Excluded Detractor.
21
+ SUCCESS: AAPL correctly identified as Overweight Contributor.
main.py CHANGED
@@ -101,7 +101,8 @@ class QuantScaleSystem:
101
  benchmark_weights=benchmark_weights,
102
  sector_map=sector_map,
103
  excluded_sectors=request.excluded_sectors,
104
- excluded_tickers=request.excluded_tickers
 
105
  )
106
 
107
  if opt_result.status != "optimal":
 
101
  benchmark_weights=benchmark_weights,
102
  sector_map=sector_map,
103
  excluded_sectors=request.excluded_sectors,
104
+ excluded_tickers=request.excluded_tickers,
105
+ max_weight=request.max_weight
106
  )
107
 
108
  if opt_result.status != "optimal":