Spaces:
Sleeping
Sleeping
| import os | |
| from datetime import datetime | |
| import re | |
| from dotenv import load_dotenv | |
| from sendgrid import SendGridAPIClient | |
| from sendgrid.helpers.mail import Mail | |
| load_dotenv() | |
| def build_html_report ( | |
| portfolio: dict, | |
| metrics: dict, | |
| forecasts: dict, | |
| sentiment: dict, | |
| explanation: str | |
| )->str : | |
| """Build a rich HTML email report.""" | |
| stock_rows = "" | |
| for stock, weight in sorted(portfolio.items(), key=lambda x:x[1] , reverse=True): | |
| forecast = forecasts.get(stock, {}) | |
| ret = forecast.get("return", 0) | |
| vol = forecast.get("volatility", 0) | |
| sig = sentiment.get(stock, {}).get("signal", "neutral") | |
| color = "#00b894" if ret > 0 else "#d63031" | |
| sig_icon = "🟢" if sig == "bullish" else \ | |
| "🔴" if sig == "bearish" else "⚪" | |
| stock_rows += f""" | |
| <tr> | |
| <td style="padding:10px;font-weight:600">{stock}</td> | |
| <td style="padding:10px;text-align:center">{weight:.1%}</td> | |
| <td style="padding:10px;text-align:center;color:{color}"> | |
| {ret :.2%} | |
| </td> | |
| <td style="padding:10px;text-align:center">±{vol:.2%}</td> | |
| <td style="padding:10px;text-align:center">{sig_icon} {sig}</td> | |
| </tr>""" | |
| return f""" | |
| <html> | |
| <body style="font-family:Arial,sans-serif;max-width:700px; | |
| margin:0 auto;padding:20px;background:#f8fafc"> | |
| <div style="background:linear-gradient(135deg,#0D9488,#14B8A6); | |
| padding:30px;border-radius:12px;text-align:center;color:white"> | |
| <h1 style="margin:0">📈 Daily Portfolio Report</h1> | |
| <p style="margin:8px 0 0;opacity:0.9"> | |
| {datetime.now().strftime("%A, %B %d, %Y")} | |
| </p> | |
| </div> | |
| <div style="display:grid;grid-template-columns:repeat(3,1fr); | |
| gap:12px;margin:20px 0"> | |
| <div style="background:white;border:1px solid #e5e7eb; | |
| border-radius:8px;padding:16px;text-align:center"> | |
| <p style="margin:0;color:#6b7280;font-size:12px">Expected Return</p> | |
| <p style="margin:4px 0 0;font-size:22px;font-weight:700;color:#0D9488"> | |
| {metrics.get('expected_return', 0):+.2%} | |
| </p> | |
| </div> | |
| <div style="background:white;border:1px solid #e5e7eb; | |
| border-radius:8px;padding:16px;text-align:center"> | |
| <p style="margin:0;color:#6b7280;font-size:12px">Sharpe Ratio</p> | |
| <p style="margin:4px 0 0;font-size:22px;font-weight:700;color:#0D9488"> | |
| {metrics.get('sharpe_ratio', 0):.2f} | |
| </p> | |
| </div> | |
| <div style="background:white;border:1px solid #e5e7eb; | |
| border-radius:8px;padding:16px;text-align:center"> | |
| <p style="margin:0;color:#6b7280;font-size:12px">Volatility</p> | |
| <p style="margin:4px 0 0;font-size:22px;font-weight:700;color:#e17055"> | |
| {metrics.get('volatility', 0):.2%} | |
| </p> | |
| </div> | |
| </div> | |
| <h2 style="color:#134e4a">📊 Portfolio Allocation</h2> | |
| <table style="width:100%;border-collapse:collapse; | |
| background:white;border-radius:8px;overflow:hidden"> | |
| <thead> | |
| <tr style="background:#0D9488;color:white"> | |
| <th style="padding:12px;text-align:left">Stock</th> | |
| <th style="padding:12px">Weight</th> | |
| <th style="padding:12px">Forecast</th> | |
| <th style="padding:12px">Volatility</th> | |
| <th style="padding:12px">Sentiment</th> | |
| </tr> | |
| </thead> | |
| <tbody>{stock_rows}</tbody> | |
| </table> | |
| <h2 style="color:#134e4a">🤖 AI Portfolio Analysis</h2> | |
| <div style="background:white;border-left:4px solid #0D9488; | |
| padding:16px;border-radius:0 8px 8px 0"> | |
| <p style="margin:0;line-height:1.8;color:#374151"> | |
| {explanation} | |
| </p> | |
| </div> | |
| <div style="text-align:center;margin-top:30px; | |
| padding-top:20px;border-top:1px solid #e5e7eb"> | |
| <p style="color:#9ca3af;font-size:12px"> | |
| ⚠️ This report is for informational purposes only. | |
| Not financial advice.<br> | |
| Generated by Portfolio Optimizer · | |
| LangChain · Groq · GitHub Actions | |
| </p> | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| def send_daily_report( results :dict)->bool : | |
| """ | |
| Send the dailyportfolio report via email | |
| Args: | |
| results:Pipeline results dict containing portfolio, mertics,etc | |
| Returns : | |
| True if sent successfully , False otherwise | |
| """ | |
| print("\n Sending daily portfolio report email ...") | |
| html_content = build_html_report( | |
| portfolio = results.get("portfolio", {}), | |
| metrics = results.get("metrics", {}), | |
| forecasts = results.get("forecasts", {}), | |
| sentiment = results.get("sentiment", {}), | |
| explanation = results.get("explanation", "No explanation available.") | |
| ) | |
| sender_email = os.getenv("SENDER_EMAIL") | |
| recipient_email = os.getenv("RECIPIENT_EMAIL") | |
| api_key = os.getenv("SENDGRID_API_KEY") | |
| if not all([sender_email, recipient_email, api_key]): | |
| print(" ⚠️ Missing email configuration in .env — skipping email") | |
| return False | |
| message = Mail( | |
| from_email = sender_email, | |
| to_emails = recipient_email, | |
| subject = f"📈 Daily Portfolio Report — " | |
| f"{datetime.now().strftime('%B %d, %Y')}", | |
| html_content = html_content | |
| ) | |
| try: | |
| sg = SendGridAPIClient(api_key) | |
| response = sg.send(message) | |
| print(f" ✅ Email sent successfully! Status: {response.status_code}") | |
| return True | |
| except Exception as e: | |
| if hasattr(e, 'body'): | |
| print(f" ❌ Email failed: {e.body}") | |
| return False | |
| if __name__ == "__main__": | |
| # Test with mock data | |
| mock_results = { | |
| "portfolio": {"AAPL": 0.285, "MSFT": 0.321, "GOOGL": 0.154}, | |
| "metrics": { | |
| "expected_return": 0.184, | |
| "volatility": 0.092, | |
| "sharpe_ratio": 1.82 | |
| }, | |
| "forecasts": { | |
| "AAPL": {"return": 0.082, "volatility": 0.021}, | |
| "MSFT": {"return": 0.051, "volatility": 0.018}, | |
| "GOOGL": {"return": -0.023, "volatility": 0.032}, | |
| }, | |
| "sentiment": { | |
| "AAPL": {"signal": "bullish"}, | |
| "MSFT": {"signal": "bullish"}, | |
| "GOOGL": {"signal": "bearish"}, | |
| }, | |
| "explanation": "This is a test explanation of the portfolio strategy." | |
| } | |
| send_daily_report(mock_results) |