AJAY KASU commited on
Commit
e6c4c6c
·
1 Parent(s): f65f1d3

Fix: Add Directional Signage Logic to Attribution to prevent Negative Rally hallucination

Browse files
Files changed (2) hide show
  1. ai/prompts.py +6 -1
  2. analytics/attribution.py +27 -2
ai/prompts.py CHANGED
@@ -49,9 +49,14 @@ Write a "Trailing 30-Day Risk & Performance Attribution" report relative to the
49
  - DO NOT say "We held [Excluded Stock]".
50
  4. **Chain of Thought (Mental Check)**:
51
  - First, scan the JSON. Identify the "Status" of the top movers.
52
- - Second, match the Sector to the Stock.
53
  - Third, write the commentary based ONLY on these facts.
54
 
 
 
 
 
 
55
  Write a professional, concise 3-paragraph commentary.
56
  """
57
 
 
49
  - DO NOT say "We held [Excluded Stock]".
50
  4. **Chain of Thought (Mental Check)**:
51
  - First, scan the JSON. Identify the "Status" of the top movers.
52
+ - Second, READ THE "Reasoning" FIELD. This is the ground truth.
53
  - Third, write the commentary based ONLY on these facts.
54
 
55
+ 5. **Signage Rules (CRITICAL)**:
56
+ - If Return is NEGATIVE (e.g. -15%), NEVER use the word "rallied". Use "declined", "sold off", or "dropped".
57
+ - If Reasoning says "Protected (Avoided Loss)", say: "The portfolio benefited from avoiding [Stock], which declined by [Return]."
58
+ - If Reasoning says "Drag (Missed Rally)", say: "Performaance was held back by not owning [Stock], which rose by [Return]."
59
+
60
  Write a professional, concise 3-paragraph commentary.
61
  """
62
 
analytics/attribution.py CHANGED
@@ -107,12 +107,37 @@ class AttributionEngine:
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
 
 
107
  def build_truth_table(dataframe, n=5):
108
  results = []
109
  for ticker, row in dataframe.head(n).iterrows():
110
+ status = get_status(row)
111
+ ret_val = row['ret']
112
+ active_contrib = row['contribution']
113
+
114
+ # Directional Signage Logic (The "Why")
115
+ reasoning = "Neutral"
116
+
117
+ if status == "Excluded":
118
+ if ret_val < 0:
119
+ reasoning = "Protected (Avoided Loss)"
120
+ else:
121
+ reasoning = "Drag (Missed Rally)"
122
+ elif status == "Underweight":
123
+ if ret_val < 0:
124
+ reasoning = "Protected (Underweight Loser)"
125
+ else:
126
+ reasoning = "Drag (Underweight Winner)"
127
+ elif status == "Overweight":
128
+ if ret_val > 0:
129
+ reasoning = "Value Add (Stock Picking)"
130
+ else:
131
+ reasoning = "Detractor (Overweight Loser)"
132
+
133
  results.append({
134
  "Ticker": ticker,
135
  "Sector": row['sector'],
136
+ "Status": status,
137
+ "active_contribution_raw": float(row['contribution']), # Keep raw for sorting helper if needed
138
  "Active_Contribution": f"{row['contribution']:.4f}",
139
+ "Return": f"{row['ret']:.2%}",
140
+ "Reasoning": reasoning
141
  })
142
  return results
143