Spaces:
Sleeping
Sleeping
| import requests as http_requests | |
| import json | |
| from django.http import HttpResponse, StreamingHttpResponse | |
| from rest_framework import status | |
| from rest_framework.decorators import api_view | |
| from rest_framework.decorators import parser_classes | |
| from rest_framework.parsers import FormParser, MultiPartParser | |
| from rest_framework.response import Response | |
| from .analysis_service import run_agentic_analysis | |
| from .financial_models import ALL_MODELS, run_model as run_financial_model | |
| from .risk_management_service import run_portfolio_risk_analysis | |
| from .agent_tools_service import run_agent_tool_chat | |
| from .claude_agent_service import run_agent_chat | |
| from .services import run_chat_completion, run_chat_completion_stream | |
| from .excel_export_service import generate_xlsx | |
| from .report_export_service import generate_docx | |
| from .slides_export_service import generate_pptx | |
| def chat_completion(request): | |
| user_message = str(request.data.get("message", "")).strip() | |
| if not user_message: | |
| return Response( | |
| {"detail": "Request body must include a non-empty 'message' field."}, | |
| status=status.HTTP_400_BAD_REQUEST, | |
| ) | |
| try: | |
| result = run_chat_completion(user_message) | |
| except ValueError as exc: | |
| return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST) | |
| except Exception as exc: | |
| return Response( | |
| {"detail": "Cerebras chat completion failed.", "error": str(exc)}, | |
| status=status.HTTP_502_BAD_GATEWAY, | |
| ) | |
| return Response( | |
| { | |
| "status": "ok", | |
| "model": result["model"], | |
| "content": result["content"], | |
| } | |
| ) | |
| def chat_completion_stream(request): | |
| user_message = str(request.data.get("message", "")).strip() | |
| if not user_message: | |
| return Response( | |
| {"detail": "Request body must include a non-empty 'message' field."}, | |
| status=status.HTTP_400_BAD_REQUEST, | |
| ) | |
| try: | |
| stream = run_chat_completion_stream(user_message) | |
| except ValueError as exc: | |
| return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST) | |
| except Exception as exc: | |
| return Response( | |
| {"detail": "Cerebras stream initialization failed.", "error": str(exc)}, | |
| status=status.HTTP_502_BAD_GATEWAY, | |
| ) | |
| response = StreamingHttpResponse(stream, content_type="text/plain; charset=utf-8") | |
| response["Cache-Control"] = "no-cache" | |
| response["X-Accel-Buffering"] = "no" | |
| return response | |
| def analyze_document(request): | |
| uploaded_file = request.FILES.get("file") | |
| if uploaded_file is None: | |
| return Response( | |
| {"detail": "Request must include a PDF file in the 'file' form field."}, | |
| status=status.HTTP_400_BAD_REQUEST, | |
| ) | |
| if uploaded_file.content_type and uploaded_file.content_type != "application/pdf": | |
| return Response( | |
| {"detail": "Only PDF uploads are supported for analysis."}, | |
| status=status.HTTP_400_BAD_REQUEST, | |
| ) | |
| headline = str(request.data.get("headline", "")).strip() | |
| try: | |
| payload = run_agentic_analysis(uploaded_file.read(), headline) | |
| except Exception as exc: | |
| return Response( | |
| {"detail": "Agentic analysis failed.", "error": str(exc)}, | |
| status=status.HTTP_502_BAD_GATEWAY, | |
| ) | |
| return Response( | |
| { | |
| "status": "ok", | |
| "analysis": payload, | |
| } | |
| ) | |
| def _extract_json_payload(raw_text: str): | |
| raw_text = (raw_text or "").strip() | |
| if not raw_text: | |
| return None | |
| if raw_text.startswith("```"): | |
| raw_text = raw_text.strip("`") | |
| if "\n" in raw_text: | |
| raw_text = raw_text.split("\n", 1)[1] | |
| raw_text = raw_text.rsplit("```", 1)[0].strip() | |
| try: | |
| return json.loads(raw_text) | |
| except json.JSONDecodeError: | |
| pass | |
| start = raw_text.find("{") | |
| end = raw_text.rfind("}") | |
| if start >= 0 and end > start: | |
| candidate = raw_text[start : end + 1] | |
| try: | |
| return json.loads(candidate) | |
| except json.JSONDecodeError: | |
| return None | |
| return None | |
| def fix_analysis_json(request): | |
| component_name = str(request.data.get("componentName", "")).strip() | |
| component_data = request.data.get("componentData") | |
| full_analysis = request.data.get("fullAnalysis", {}) | |
| attempt = int(request.data.get("attempt", 1)) | |
| if not component_name: | |
| return Response( | |
| {"detail": "Request body must include 'componentName'."}, | |
| status=status.HTTP_400_BAD_REQUEST, | |
| ) | |
| # Fast local path: if the malformed value is just a JSON string, parse it directly. | |
| if isinstance(component_data, str): | |
| local_candidate = _extract_json_payload(component_data) | |
| if local_candidate is not None: | |
| return Response( | |
| { | |
| "status": "ok", | |
| "componentName": component_name, | |
| "componentData": local_candidate, | |
| "resolved": True, | |
| "source": "local-parser", | |
| } | |
| ) | |
| prompt = ( | |
| "You are a strict JSON repair engine.\n" | |
| "TASK: Repair the target component so it becomes valid JSON for dashboard rendering.\n\n" | |
| "OUTPUT RULES (MANDATORY):\n" | |
| "1) Return exactly one JSON object and nothing else.\n" | |
| "2) No markdown, no code fences, no explanation.\n" | |
| "3) Use this exact envelope: {\"componentData\": <object-or-array-or-null>}.\n" | |
| "4) If you cannot repair safely, return {\"componentData\": null}.\n" | |
| "5) Preserve semantic meaning and field names whenever possible.\n" | |
| "6) Ensure result parses with strict JSON parser.\n\n" | |
| f"Target component: {component_name}\n" | |
| f"Attempt number: {attempt}\n" | |
| f"Malformed component input:\n{json.dumps(component_data, ensure_ascii=True)}\n\n" | |
| f"Full analysis context:\n{json.dumps(full_analysis, ensure_ascii=True)[:18000]}" | |
| ) | |
| try: | |
| llm_result = run_chat_completion(prompt) | |
| parsed = _extract_json_payload(llm_result.get("content", "")) | |
| except Exception as exc: | |
| return Response( | |
| {"detail": "JSON fixture failed.", "error": str(exc)}, | |
| status=status.HTTP_502_BAD_GATEWAY, | |
| ) | |
| fixed = parsed.get("componentData") if isinstance(parsed, dict) else None | |
| return Response( | |
| { | |
| "status": "ok", | |
| "componentName": component_name, | |
| "componentData": fixed, | |
| "resolved": fixed is not None, | |
| "source": "llm-fixture", | |
| } | |
| ) | |
| def agent_tool_chat(request, tool_slug: str): | |
| message = str(request.data.get("message", "")).strip() | |
| if not message: | |
| return Response( | |
| {"detail": "Request body must include a non-empty 'message' field."}, | |
| status=status.HTTP_400_BAD_REQUEST, | |
| ) | |
| history = request.data.get("history", []) | |
| artifact = request.data.get("artifact", {}) | |
| data_context = request.data.get("data_context", None) | |
| try: | |
| payload = run_agent_tool_chat( | |
| tool_slug=tool_slug, | |
| message=message, | |
| history=history, | |
| current_artifact=artifact, | |
| data_context=data_context, | |
| ) | |
| except ValueError as exc: | |
| return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST) | |
| except Exception as exc: | |
| return Response( | |
| {"detail": "Agent tool request failed.", "error": str(exc)}, | |
| status=status.HTTP_502_BAD_GATEWAY, | |
| ) | |
| return Response({"status": "ok", **payload}) | |
| def agent_chat(request): | |
| """Conversational agent with real-time tool calling (Claude Agent SDK).""" | |
| message = str(request.data.get("message", "")).strip() | |
| if not message: | |
| return Response( | |
| {"detail": "Request body must include a non-empty 'message' field."}, | |
| status=status.HTTP_400_BAD_REQUEST, | |
| ) | |
| history = request.data.get("history", []) | |
| data_context = request.data.get("data_context", None) | |
| try: | |
| payload = run_agent_chat( | |
| message=message, | |
| history=history if isinstance(history, list) else [], | |
| data_context=data_context if isinstance(data_context, dict) else None, | |
| ) | |
| except ValueError as exc: | |
| return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST) | |
| except Exception as exc: | |
| return Response( | |
| {"detail": "Agent chat failed.", "error": str(exc)}, | |
| status=status.HTTP_502_BAD_GATEWAY, | |
| ) | |
| return Response({"status": "ok", **payload}) | |
| def export_slides_pptx(request): | |
| """Accept a slides artifact JSON and return a downloadable .pptx file.""" | |
| artifact = request.data.get("artifact") | |
| if not isinstance(artifact, dict) or not artifact.get("slides"): | |
| return Response( | |
| {"detail": "Request body must include an 'artifact' object with a 'slides' array."}, | |
| status=status.HTTP_400_BAD_REQUEST, | |
| ) | |
| try: | |
| pptx_bytes = generate_pptx(artifact) | |
| except Exception as exc: | |
| return Response( | |
| {"detail": "PPTX generation failed.", "error": str(exc)}, | |
| status=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| ) | |
| title_slug = (artifact.get("title") or "deck").replace(" ", "_")[:40] | |
| filename = f"{title_slug}.pptx" | |
| response = HttpResponse( | |
| pptx_bytes, | |
| content_type="application/vnd.openxmlformats-officedocument.presentationml.presentation", | |
| ) | |
| response["Content-Disposition"] = f'attachment; filename="{filename}"' | |
| return response | |
| def export_report_docx(request): | |
| """Accept a report artifact JSON and return a downloadable .docx file.""" | |
| artifact = request.data.get("artifact") | |
| if not isinstance(artifact, dict) or not artifact.get("sections"): | |
| return Response( | |
| {"detail": "Request body must include an 'artifact' object with a 'sections' array."}, | |
| status=status.HTTP_400_BAD_REQUEST, | |
| ) | |
| try: | |
| docx_bytes = generate_docx(artifact) | |
| except Exception as exc: | |
| return Response( | |
| {"detail": "DOCX generation failed.", "error": str(exc)}, | |
| status=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| ) | |
| title_slug = (artifact.get("title") or "report").replace(" ", "_")[:40] | |
| filename = f"{title_slug}.docx" | |
| response = HttpResponse( | |
| docx_bytes, | |
| content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", | |
| ) | |
| response["Content-Disposition"] = f'attachment; filename="{filename}"' | |
| return response | |
| def export_excel_xlsx(request): | |
| """Accept an Excel artifact JSON and return a downloadable .xlsx file.""" | |
| artifact = request.data.get("artifact") | |
| if not isinstance(artifact, dict) or not artifact.get("sheets"): | |
| return Response( | |
| {"detail": "Request body must include an 'artifact' object with a 'sheets' array."}, | |
| status=status.HTTP_400_BAD_REQUEST, | |
| ) | |
| try: | |
| xlsx_bytes = generate_xlsx(artifact) | |
| except Exception as exc: | |
| return Response( | |
| {"detail": "XLSX generation failed.", "error": str(exc)}, | |
| status=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| ) | |
| title_slug = (artifact.get("title") or "model").replace(" ", "_")[:40] | |
| filename = f"{title_slug}.xlsx" | |
| response = HttpResponse( | |
| xlsx_bytes, | |
| content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | |
| ) | |
| response["Content-Disposition"] = f'attachment; filename="{filename}"' | |
| return response | |
| def multi_agent_run(request): | |
| """Run the multi-agent financial analysis pipeline with SSE streaming.""" | |
| user_prompt = str(request.data.get("prompt", "")).strip() | |
| if not user_prompt: | |
| return Response( | |
| {"detail": "Request body must include a non-empty 'prompt' field."}, | |
| status=status.HTTP_400_BAD_REQUEST, | |
| ) | |
| data_context = request.data.get("data_context", None) | |
| try: | |
| from .multi_agent_service import run_multi_agent_pipeline | |
| stream = run_multi_agent_pipeline(user_prompt, data_context) | |
| except Exception as exc: | |
| return Response( | |
| {"detail": "Multi-agent pipeline failed to start.", "error": str(exc)}, | |
| status=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| ) | |
| response = StreamingHttpResponse(stream, content_type="text/event-stream") | |
| response["Cache-Control"] = "no-cache" | |
| response["X-Accel-Buffering"] = "no" | |
| return response | |
| _TV_NEWS_URL = "https://news-mediator.tradingview.com/public/news-flow/v2/news" | |
| _TV_HEADERS = { | |
| "accept": "*/*", | |
| "origin": "https://in.tradingview.com", | |
| "referer": "https://in.tradingview.com/", | |
| "user-agent": ( | |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " | |
| "AppleWebKit/537.36 (KHTML, like Gecko) " | |
| "Chrome/145.0.0.0 Safari/537.36" | |
| ), | |
| } | |
| _COMMON_EXCHANGES = ["NYSE", "NASDAQ", "AMEX"] | |
| def stock_news(request): | |
| """Proxy TradingView news for a given symbol or ticker.""" | |
| symbol = request.query_params.get("symbol", "").strip() | |
| if not symbol: | |
| return Response( | |
| {"detail": "Query parameter 'symbol' is required (e.g. NYSE:DELL or DELL)."}, | |
| status=status.HTTP_400_BAD_REQUEST, | |
| ) | |
| if ":" in symbol: | |
| symbol_filters = [f"symbol:{symbol}"] | |
| else: | |
| symbol_filters = [f"symbol:{ex}:{symbol}" for ex in _COMMON_EXCHANGES] | |
| all_items = [] | |
| seen_ids = set() | |
| for sym_filter in symbol_filters: | |
| params = { | |
| "filter": ["lang:en", sym_filter], | |
| "client": "landing", | |
| "streaming": "false", | |
| "user_prostatus": "non_pro", | |
| } | |
| try: | |
| resp = http_requests.get( | |
| _TV_NEWS_URL, params=params, headers=_TV_HEADERS, timeout=10, | |
| ) | |
| resp.raise_for_status() | |
| items = resp.json().get("items", []) | |
| for item in items: | |
| if item["id"] not in seen_ids: | |
| seen_ids.add(item["id"]) | |
| all_items.append(item) | |
| except http_requests.RequestException: | |
| continue | |
| all_items.sort(key=lambda x: x.get("published", 0), reverse=True) | |
| return Response({"status": "ok", "items": all_items}) | |
| def breaking_news(request): | |
| """Proxy TradingView top-story / market-index breaking news.""" | |
| params = { | |
| 'filter': [ | |
| 'lang:en_IN', | |
| 'priority:important', | |
| ], | |
| 'client': 'screener', | |
| 'streaming': 'true', | |
| 'user_prostatus': 'non_pro', | |
| } | |
| try: | |
| resp = http_requests.get( | |
| _TV_NEWS_URL, params=params, headers=_TV_HEADERS, timeout=10, | |
| ) | |
| resp.raise_for_status() | |
| items = resp.json().get("items", []) | |
| except http_requests.RequestException as exc: | |
| return Response( | |
| {"detail": "Failed to fetch breaking news.", "error": str(exc)}, | |
| status=status.HTTP_502_BAD_GATEWAY, | |
| ) | |
| return Response({"status": "ok", "items": items}) | |
| _TV_SCREENER_URL = "https://screener-facade.tradingview.com/screener-facade/api/v1/screener-table/scan" | |
| _TV_SCREENER_HEADERS = { | |
| "accept": "application/json", | |
| "content-type": "text/plain;charset=UTF-8", | |
| "origin": "https://in.tradingview.com", | |
| "referer": "https://in.tradingview.com/", | |
| "user-agent": ( | |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " | |
| "AppleWebKit/537.36 (KHTML, like Gecko) " | |
| "Chrome/145.0.0.0 Safari/537.36" | |
| ), | |
| } | |
| def portfolio_risk_analysis(request): | |
| """Run portfolio risk analysis with real computations (correlation, VaR, optimal weights, stress tests).""" | |
| portfolio = request.data.get("portfolio", []) | |
| weights = request.data.get("weights", {}) | |
| portfolio_value = float(request.data.get("portfolioValue", 100_000)) | |
| if not portfolio or not isinstance(portfolio, list): | |
| return Response( | |
| {"detail": "Request body must include a non-empty 'portfolio' array of ticker symbols."}, | |
| status=status.HTTP_400_BAD_REQUEST, | |
| ) | |
| portfolio = [str(t).strip().upper() for t in portfolio if t] | |
| if not portfolio: | |
| return Response( | |
| {"detail": "Portfolio must contain at least one valid ticker."}, | |
| status=status.HTTP_400_BAD_REQUEST, | |
| ) | |
| if not isinstance(weights, dict): | |
| weights = {} | |
| try: | |
| result = run_portfolio_risk_analysis( | |
| portfolio=portfolio, | |
| weights=weights, | |
| portfolio_value=portfolio_value, | |
| ) | |
| except Exception as exc: | |
| return Response( | |
| {"detail": "Risk analysis failed.", "error": str(exc)}, | |
| status=status.HTTP_502_BAD_GATEWAY, | |
| ) | |
| if "error" in result: | |
| return Response( | |
| {"detail": result["error"]}, | |
| status=status.HTTP_400_BAD_REQUEST, | |
| ) | |
| return Response({"status": "ok", **result}) | |
| def market_benchmarks(request): | |
| """Fetch US index technicals from TradingView screener.""" | |
| params = { | |
| "table_id": "indices_quotes.us", | |
| "version": "52", | |
| "columnset_id": "technicals", | |
| } | |
| body = '{"lang":"en","range":[0,5],"scanner_product_label":"markets-screener"}' | |
| try: | |
| resp = http_requests.post( | |
| _TV_SCREENER_URL, | |
| params=params, | |
| headers=_TV_SCREENER_HEADERS, | |
| data=body, | |
| timeout=10, | |
| ) | |
| resp.raise_for_status() | |
| raw = resp.json() | |
| except http_requests.RequestException as exc: | |
| return Response( | |
| {"detail": "Failed to fetch market benchmarks.", "error": str(exc)}, | |
| status=status.HTTP_502_BAD_GATEWAY, | |
| ) | |
| symbols = raw.get("symbols", []) | |
| columns = raw.get("data", []) | |
| count = len(symbols) | |
| col_map = {} | |
| for col in columns: | |
| cid = col["id"] | |
| if cid in col_map: | |
| col_map[cid + "_2"] = col | |
| else: | |
| col_map[cid] = col | |
| benchmarks = [] | |
| for i in range(count): | |
| ticker_col = col_map.get("TickerUniversal", {}) | |
| raw_vals = ticker_col.get("rawValues", []) | |
| info = raw_vals[i] if i < len(raw_vals) else {} | |
| tech_rating = col_map.get("TechnicalRating", {}).get("rawValues", []) | |
| ma_rating = col_map.get("RatingMa", {}).get("rawValues", []) | |
| osc_rating = col_map.get("RatingOscillators", {}).get("rawValues", []) | |
| rsi_vals = col_map.get("RelativeStrengthIndex", {}).get("rawValues", []) | |
| benchmarks.append({ | |
| "symbol": symbols[i] if i < len(symbols) else "", | |
| "name": info.get("description", ""), | |
| "exchange": info.get("exchange", ""), | |
| "ticker": info.get("name", ""), | |
| "logoid": info.get("logoid", ""), | |
| "technicalRating": tech_rating[i] if i < len(tech_rating) else None, | |
| "maRating": ma_rating[i] if i < len(ma_rating) else None, | |
| "oscillatorsRating": osc_rating[i] if i < len(osc_rating) else None, | |
| "rsi": round(rsi_vals[i], 2) if i < len(rsi_vals) and isinstance(rsi_vals[i], (int, float)) else None, | |
| }) | |
| return Response({"status": "ok", "benchmarks": benchmarks}) | |
| def quant_models_list(_request): | |
| """List available quantitative/AutoML models.""" | |
| models = [ | |
| {"id": k, "name": k.replace("_", " ").title(), "description": _MODEL_DESCRIPTIONS.get(k, "")} | |
| for k in ALL_MODELS.keys() | |
| ] | |
| return Response({"models": models}) | |
| def quant_model_run(request): | |
| """Run a quantitative model on a ticker. Returns metrics, chart_data, signals, interpretation.""" | |
| ticker = str(request.data.get("ticker", "")).strip().upper() | |
| if not ticker: | |
| return Response( | |
| {"detail": "Request body must include a non-empty 'ticker' field."}, | |
| status=status.HTTP_400_BAD_REQUEST, | |
| ) | |
| model_name = str(request.data.get("model", "")).strip().lower().replace(" ", "_") | |
| if not model_name or model_name not in ALL_MODELS: | |
| return Response( | |
| {"detail": f"Unknown model. Available: {', '.join(ALL_MODELS.keys())}"}, | |
| status=status.HTTP_400_BAD_REQUEST, | |
| ) | |
| period = str(request.data.get("period", "1y")).strip() or "1y" | |
| horizon = str(request.data.get("horizon", "1-Week")).strip() | |
| scenario = str(request.data.get("scenario", "Base Case")).strip() | |
| scenario_adjust = _scenario_adjustments(scenario) | |
| kwargs = {} | |
| if model_name == "monte_carlo": | |
| kwargs["period"] = period | |
| days_map = {"1-Day": 1, "1-Week": 7, "1-Month": 30} | |
| kwargs["days"] = days_map.get(horizon, 30) | |
| kwargs["drift_adjustment"] = scenario_adjust["growth"] / 252 | |
| elif model_name == "dcf": | |
| base_growth = float(request.data.get("growth_rate", 8)) / 100 | |
| base_discount = float(request.data.get("discount_rate", 10)) / 100 | |
| base_terminal = float(request.data.get("terminal_growth", 3)) / 100 | |
| kwargs["growth_rate"] = base_growth + scenario_adjust["growth"] | |
| kwargs["discount_rate"] = base_discount + scenario_adjust["discount"] | |
| kwargs["terminal_growth"] = base_terminal + scenario_adjust["terminal_growth"] | |
| elif model_name == "correlation": | |
| extra_tickers = request.data.get("tickers") | |
| if isinstance(extra_tickers, str): | |
| extra_tickers = [t.strip().upper() for t in extra_tickers.split(",") if t.strip()] | |
| elif not isinstance(extra_tickers, list): | |
| extra_tickers = [] | |
| second_ticker = "QQQ" if ticker.upper() == "SPY" else "SPY" | |
| tickers_list = list(dict.fromkeys([ticker] + [t for t in extra_tickers if t and str(t).strip().upper() != ticker]))[:8] | |
| if len(tickers_list) < 2: | |
| tickers_list = list(dict.fromkeys([ticker, second_ticker])) | |
| kwargs["tickers"] = tickers_list | |
| kwargs["period"] = period | |
| elif model_name == "regression": | |
| kwargs["period"] = period | |
| kwargs["forecast_days"] = {"1-Day": 1, "1-Week": 7, "1-Month": 30}.get(horizon, 30) | |
| else: | |
| kwargs["period"] = period | |
| try: | |
| result = run_financial_model(model_name, ticker, **kwargs) | |
| except Exception as exc: | |
| return Response( | |
| {"detail": str(exc), "model": model_name, "ticker": ticker}, | |
| status=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| ) | |
| if "error" in result: | |
| return Response( | |
| {"detail": result["error"], "model": model_name, "ticker": ticker}, | |
| status=status.HTTP_422_UNPROCESSABLE_ENTITY, | |
| ) | |
| return Response(result) | |
| def quant_models_run_all(request): | |
| """Run all quantitative models on a ticker and generate an LLM executive summary.""" | |
| ticker = str(request.data.get("ticker", "")).strip().upper() | |
| if not ticker: | |
| return Response( | |
| {"detail": "Request body must include a non-empty 'ticker' field."}, | |
| status=status.HTTP_400_BAD_REQUEST, | |
| ) | |
| period = str(request.data.get("period", "1y")).strip() or "1y" | |
| horizon = str(request.data.get("horizon", "1-Week")).strip() | |
| scenario = str(request.data.get("scenario", "Base Case")).strip() | |
| scenario_adjust = _scenario_adjustments(scenario) | |
| growth = float(request.data.get("growth_rate", 8)) / 100 | |
| discount = float(request.data.get("discount_rate", 10)) / 100 | |
| terminal_growth = float(request.data.get("terminal_growth", 3)) / 100 | |
| days_map = {"1-Day": 1, "1-Week": 7, "1-Month": 30} | |
| days = days_map.get(horizon, 30) | |
| forecast_days = days | |
| results = {} | |
| interpretations = [] | |
| signals_list = [] | |
| for model_name in ALL_MODELS: | |
| kwargs = {"period": period} | |
| if model_name == "monte_carlo": | |
| kwargs["days"] = days | |
| kwargs["drift_adjustment"] = scenario_adjust["growth"] / 252 | |
| elif model_name == "dcf": | |
| kwargs["growth_rate"] = growth + scenario_adjust["growth"] | |
| kwargs["discount_rate"] = discount + scenario_adjust["discount"] | |
| kwargs["terminal_growth"] = terminal_growth + scenario_adjust["terminal_growth"] | |
| elif model_name == "correlation": | |
| second_ticker = "QQQ" if ticker == "SPY" else "SPY" | |
| kwargs["tickers"] = [ticker, second_ticker] | |
| elif model_name == "regression": | |
| kwargs["forecast_days"] = forecast_days | |
| try: | |
| result = run_financial_model(model_name, ticker, **kwargs) | |
| if "error" not in result: | |
| results[model_name] = result | |
| if result.get("interpretation"): | |
| interpretations.append(f"[{model_name}]: {result['interpretation']}") | |
| for sig in result.get("signals", []): | |
| signals_list.append(f"[{model_name}] {sig}") | |
| except Exception: | |
| results[model_name] = {"error": "Model failed", "model": model_name} | |
| summary_prompt = ( | |
| f"You are a concise financial analyst. Write a 3-5 sentence executive summary for {ticker}.\n\n" | |
| f"Scenario: {scenario}\n" | |
| f"Models run: {', '.join(results.keys())}\n\n" | |
| f"Key signals:\n" + "\n".join(signals_list[:15]) + "\n\n" | |
| f"Model interpretations:\n" + "\n".join(interpretations[:10]) + "\n\n" | |
| "Synthesize the findings into a clear, actionable executive summary for stakeholders." | |
| ) | |
| try: | |
| llm_result = run_chat_completion(summary_prompt) | |
| executive_summary = (llm_result.get("content") or "").strip() | |
| except Exception: | |
| executive_summary = ( | |
| f"Analysis completed for {ticker}. " | |
| f"Ran {len([r for r in results.values() if 'error' not in r])} models. " | |
| "Review individual model outputs for details." | |
| ) | |
| return Response({ | |
| "ticker": ticker, | |
| "results": results, | |
| "executive_summary": executive_summary, | |
| "models_run": list(results.keys()), | |
| }) | |
| def _scenario_adjustments(scenario: str) -> dict: | |
| """Return growth/discount/terminal_growth adjustments based on scenario.""" | |
| s = (scenario or "").lower() | |
| if "bull" in s or "soft landing" in s: | |
| return {"growth": 0.005, "discount": -0.005, "terminal_growth": 0.005} | |
| if "bear" in s or "recession" in s: | |
| return {"growth": -0.005, "discount": 0.005, "terminal_growth": -0.005} | |
| return {"growth": 0.0, "discount": 0.0, "terminal_growth": 0.0} | |
| _MODEL_DESCRIPTIONS = { | |
| "moving_averages": "SMA/EMA crossovers and trend signals", | |
| "rsi": "Relative Strength Index overbought/oversold", | |
| "macd": "MACD momentum and crossover signals", | |
| "bollinger_bands": "Volatility bands and squeeze detection", | |
| "monte_carlo": "Probabilistic price forecast", | |
| "dcf": "Discounted cash flow valuation", | |
| "beta_capm": "Beta vs benchmark and CAPM expected return", | |
| "risk_metrics": "Sharpe, Sortino, drawdown, VaR", | |
| "correlation": "Pairwise correlation matrix (multi-ticker)", | |
| "regression": "Linear trend and forecast", | |
| } | |