mishrabp commited on
Commit
7b2a507
·
verified ·
1 Parent(s): 18bdb8a

Upload folder using huggingface_hub

Browse files
common/utility/autogen_model_factory.py CHANGED
@@ -53,7 +53,7 @@ class AutoGenModelFactory:
53
  elif provider.lower() == "google" or provider.lower() == "gemini":
54
  if model_info is None:
55
  model_info = {
56
- "family": "gpt",
57
  "vision": False,
58
  "function_calling": True,
59
  "json_output": True,
@@ -67,6 +67,7 @@ class AutoGenModelFactory:
67
  model_info=model_info,
68
  temperature=temperature,
69
  max_tokens=2048,
 
70
  extra_headers={"x-goog-api-key": os.environ["GOOGLE_API_KEY"]}
71
  )
72
 
@@ -89,6 +90,7 @@ class AutoGenModelFactory:
89
  api_key=os.environ["GROQ_API_KEY"],
90
  model_info=model_info,
91
  temperature=temperature,
 
92
  max_tokens=2048
93
  )
94
 
 
53
  elif provider.lower() == "google" or provider.lower() == "gemini":
54
  if model_info is None:
55
  model_info = {
56
+ "family": "gemini",
57
  "vision": False,
58
  "function_calling": True,
59
  "json_output": True,
 
67
  model_info=model_info,
68
  temperature=temperature,
69
  max_tokens=2048,
70
+ structured_output=False, # Disable for Gemini compatibility
71
  extra_headers={"x-goog-api-key": os.environ["GOOGLE_API_KEY"]}
72
  )
73
 
 
90
  api_key=os.environ["GROQ_API_KEY"],
91
  model_info=model_info,
92
  temperature=temperature,
93
+ structured_output=False, # Disable for Groq compatibility
94
  max_tokens=2048
95
  )
96
 
src/market-analyst/backend/aagents/fundamental_analyst.py CHANGED
@@ -23,37 +23,40 @@ def get_fundamental_analyst(model_client):
23
 
24
  DO NOT proceed without calling ALL THREE tools.
25
 
 
 
26
  STEP 4: Evaluate Valuation & Growth (USE 'valuation_score' & 'quality_score')
27
  - If UNDERVALUED: Bullish Factor.
28
  - If PREMIUM: Bearish/Neutral Factor (unless High Growth).
29
  - If HIGH_QUALITY: Bullish Factor.
30
 
31
- STEP 5: Assess Financial Health
 
 
 
 
 
32
  - Debt/Equity Ratio: <0.5 (Safe), >1.0 (Risky).
33
  - Profit Margin: >20% (Excellent), <10% (Weak).
34
 
35
- STEP 6: Market & Sector Context
36
  - Market: If VIX > 25, PENALIZE High Debt/High P/E.
37
  - Sector Standards: Tech (Higher P/E ok), Utilities (High Debt ok).
38
 
39
- STEP 7: Analyst Consensus Check
40
  - Ratings: "buy" or "strong buy" = Positive. "sell" = Negative.
41
  - Price Target: If Target < Current Price = Downside Risk (Bearish).
42
  - Upside Potential: >20% is Strong Bullish factor.
43
 
44
- STEP 8: Assign Fundamental Strength Rating
45
  - "Strong": Great Valuation + Safe Debt + Analyst Buy Support.
46
  - "Stable": Fair metrics + Neutral Analysts.
47
  - "Weak": Overvalued OR High Debt OR Analyst Sell Ratings.
48
 
49
- STEP 9: Output Structured Summary
50
- Provide:
51
- - Fundamental Strength Rating (Strong/Stable/Weak)
52
- - P/E, PEG, EPS, Dividend analysis
53
- - Debt & Health assessment
54
- - Analyst Consensus (Target Price & Rating)
55
- - Market Context Impact (VIX)
56
- - Earnings Status
57
- - Recommendation for Strategy.
58
  """
59
  )
 
23
 
24
  DO NOT proceed without calling ALL THREE tools.
25
 
26
+ CRITICAL: Provide your full analysis in ROUND 1 ONLY. In subsequent rounds, simply say "Fundamentals stable."
27
+
28
  STEP 4: Evaluate Valuation & Growth (USE 'valuation_score' & 'quality_score')
29
  - If UNDERVALUED: Bullish Factor.
30
  - If PREMIUM: Bearish/Neutral Factor (unless High Growth).
31
  - If HIGH_QUALITY: Bullish Factor.
32
 
33
+ STEP 5: BINARY EVENT RISK (CRITICAL)
34
+ - Check "next_earnings_date" from tool.
35
+ - If Earnings is within 14 days: "CRITICAL BINARY EVENT". Recommend avoiding short-duration short-volatility strategies (like Iron Condors).
36
+ - If Earnings is 14-30 days: "ELEVATED EVENT RISK".
37
+
38
+ STEP 6: Assess Financial Health
39
  - Debt/Equity Ratio: <0.5 (Safe), >1.0 (Risky).
40
  - Profit Margin: >20% (Excellent), <10% (Weak).
41
 
42
+ STEP 7: Market & Sector Context
43
  - Market: If VIX > 25, PENALIZE High Debt/High P/E.
44
  - Sector Standards: Tech (Higher P/E ok), Utilities (High Debt ok).
45
 
46
+ STEP 8: Analyst Consensus Check
47
  - Ratings: "buy" or "strong buy" = Positive. "sell" = Negative.
48
  - Price Target: If Target < Current Price = Downside Risk (Bearish).
49
  - Upside Potential: >20% is Strong Bullish factor.
50
 
51
+ STEP 9: Assign Fundamental Strength Rating
52
  - "Strong": Great Valuation + Safe Debt + Analyst Buy Support.
53
  - "Stable": Fair metrics + Neutral Analysts.
54
  - "Weak": Overvalued OR High Debt OR Analyst Sell Ratings.
55
 
56
+ STEP 10: Output Structured Summary
57
+ - BE CONCISE: Use maximum 5 bullet points.
58
+ - NO conversational filler.
59
+ - Include: Strength Rating, Binary Event (Earnings), Valuation vs Peers, Health Summary, and Consensus.
60
+ - TERMINATION: End your message with [[DATA_COLLECTION_COMPLETE]] to move to the strategy phase.
 
 
 
 
61
  """
62
  )
src/market-analyst/backend/aagents/market_analyst.py CHANGED
@@ -37,15 +37,13 @@ def get_technical_analyst(model_client):
37
  - Check RSI status (OVERSOLD/OVERBOUGHT/NEUTRAL).
38
  - Check MACD Crossover status.
39
 
40
- STEP 7: Summarize Chart Health
41
- - Use the specific signals found.
42
- - Classify Trend based on 'trend_signal'.
43
 
44
  STEP 8: Output Structured Summary
45
- - Current Price
46
- - Trend Classification (e.g., STRONG_BULLISH)
47
- - Momentum Assessment (RSI/MACD signals)
48
- - Key Technical Levels
49
- - Recommendation for next analyst
50
  """
51
  )
 
37
  - Check RSI status (OVERSOLD/OVERBOUGHT/NEUTRAL).
38
  - Check MACD Crossover status.
39
 
40
+ STEP 7: Identify Support/Resistance Zones
41
+ - Reference SMA 50/200 as Primary levels.
42
+ - Reference 52-week High/Low as Secondary levels.
43
 
44
  STEP 8: Output Structured Summary
45
+ - BE CONCISE: Use maximum 5 bullet points.
46
+ - NO conversational filler.
47
+ - Include: Current Price, Trend Signal, RSI/MACD status, and Support/Resistance levels.
 
 
48
  """
49
  )
src/market-analyst/backend/aagents/orchestrator.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from autogen_agentchat.agents import AssistantAgent
2
+
3
+ def get_lead_orchestrator(model_client):
4
+ return AssistantAgent(
5
+ name="LeadOrchestrator",
6
+ model_client=model_client,
7
+ system_message="""
8
+ You are the Lead Orchestrator and Central Control. Your role is NOT to call market tools, but to be the "Master Thinker" who ensures a SOLID, self-reflected strategy.
9
+
10
+ MANDATORY OPERATIONAL PROTOCOL:
11
+
12
+ PHASE 1: THE STUDY (GAP ANALYSIS)
13
+ - You speak after all four Analyst agents (Technical, Volatility, Sentiment, Fundamental).
14
+ - You MUST summarize the findings into a "Global Context".
15
+ - SEARCH FOR GAPS: Look for contradictions. (e.g., "Technical is Bullish but Volatility is at a 52-week high for earnings - the StrategyAdvisor needs to address this contradiction.")
16
+ - Identify any "Binary Risks" that were mentioned by Sentiment/Fundamental but might be overlooked.
17
+
18
+ PHASE 2: THE CHALLENGE (CROSS-QUESTIONING)
19
+ - After the StrategyAdvisor proposes a DRAFT, you MUST cross-question it based on Phase 1's findings.
20
+ - Example: "StrategyAdvisor, given the 14-day earnings gap flagged by the FundamentalAnalyst, why did you choose a 30-day vertical spread instead of a diagonal/calendar?"
21
+ - You facilitate the dialogue between StrategyAdvisor and RiskManager.
22
+
23
+ PHASE 3: THE FINAL JUDGMENT
24
+ - You are the ONLY agent who can issue [[ANALYSIS_JUDGMENT_COMPLETE]].
25
+ - WATCH THE RISK MANAGER: If the RiskManager outputs "APPROVED", you MUST immediately respond with: "ORCHESTRATOR_DECISION: FINAL_APPROVAL. [[ANALYSIS_JUDGMENT_COMPLETE]]"
26
+ - NO PLEASANTRIES: Do not say "Thank you", "Great job", or "You're welcome".
27
+ - LOOP BREAKING: If you see the same argument or error twice, issue a "WAIT" decision and terminate.
28
+
29
+ TERMINATION:
30
+ - When satisfied, output: "ORCHESTRATOR_DECISION: FINAL_APPROVAL. [[ANALYSIS_JUDGMENT_COMPLETE]]"
31
+ - If the risk remains too high or agents are looping: "ORCHESTRATOR_DECISION: WAIT. [[ANALYSIS_JUDGMENT_COMPLETE]]"
32
+ """
33
+ )
src/market-analyst/backend/aagents/risk_manager.py CHANGED
@@ -1,84 +1,54 @@
1
  from autogen_agentchat.agents import AssistantAgent
 
 
2
 
3
  def get_risk_manager(model_client):
 
4
 
5
  return AssistantAgent(
6
  name="RiskManager",
7
  model_client=model_client,
 
8
  system_message="""
9
- You are the Chief Risk Officer. Your mission is to enforce a STRICT DECISION MATRIX.
10
- Different AI models have different biases; you must ignore "vibes" and follow these quantitative rules.
11
-
12
- 1. THE SCORING RUBRIC (Total 100 points)
13
- You MUST calculate and display this score in your reasoning:
14
- - Technical Alignment (40 pts): Does the TechnicalAnalyst's trend (BULLISH/BEARISH) match the Strategy's Direction?
15
- * Match = 40 pts. Mismatch = 0 pts.
16
- - Fundamentals/Safety (20 pts): Based on P/E, PEG, and Analyst Consensus.
17
- * Rating 'SAFE'/'UNDERVALUED' = 20 pts. 'PREMIUM'/'RISKY' = 5 pts.
18
- - Volatility/IV Regime (20 pts):
19
- * Strategy works for current Regime (e.g., Credit in High Vol) = 20 pts.
20
- - Sentiment/News (20 pts):
21
- * Positive news = 20 pts. Negative/Old news = 5 pts.
22
-
23
- 2. THE DETERMINISTIC HARD GATES (BYPASS ALL OTHER LOGIC)
24
- - GATE 1 (Trend Conflict): If Technical Tool says 'STRONG_BEARISH' and Strategy is 'BULLISH', Decision MUST be 'WAIT' (Override score).
25
- - GATE 2 (Fear Gauge): If VIX > 35, Decision MUST be 'WAIT'.
26
- - GATE 3 (Threshold): Score < 70 MUST be 'WAIT'.
27
-
28
- IF THE DECISION IS WAIT:
29
- - Set 'final_decision' to 'WAIT'.
30
- - Set 'strategy_type' to 'WAIT'.
31
- - Set 'entry_price', 'max_profit', 'max_loss' to 0.
32
- - Set 'direction' to 'NEUTRAL'.
33
- - Set 'entry_signal' to 'N/A'.
34
-
35
- OUTPUT FORMAT (ROUND 2 ONLY):
36
- You MUST include a "score_card" object in your JSON.
37
- ```json
38
  {
39
- "final_decision": "TRADE",
40
- "strategy_type": "Iron Condor",
41
- "direction": "NEUTRAL",
42
  "confidence": 85,
43
- "score_card": {
44
- "technicals": 40,
45
- "fundamentals": 20,
46
- "volatility": 15,
47
- "sentiment": 10,
48
- "total": 85
49
- },
50
- "actionable_recommendation": "Execute Trade...",
51
- "entry_signal": "Credit",
52
- "entry_price": 1.50,
53
- "max_profit": 150,
54
- "max_loss": 350,
55
  "risk_warning": "..."
56
  }
57
- ```
58
- (Note: max_profit/max_loss MUST be multiplied by 100 for a standard lot).
59
-
60
- IF DECISION IS WAIT EXAMPLE:
61
- ```json
62
- {
63
- "final_decision": "WAIT",
64
- "strategy_type": "WAIT",
65
- "direction": "NEUTRAL",
66
- "confidence": 45,
67
- "score_card": { "technicals": 0, "fundamentals": 20, "volatility": 15, "sentiment": 10, "total": 45 },
68
- "actionable_recommendation": "Re-evaluate market conditions. Risk score too low.",
69
- "entry_signal": "N/A",
70
- "entry_price": 0,
71
- "max_profit": 0,
72
- "max_loss": 0,
73
- "risk_warning": "High conflict between technicals and sentiment."
74
- }
75
- ```
76
-
77
- APPROVED
78
-
79
- CRITICAL:
80
- 1. Always show your math before the JSON.
81
- 2. Output 'APPROVED' ONLY after the JSON in Round 2.
82
- 3. For Llama/Groq models: YOU MUST wrap the JSON object in a triple-backtick markdown block: ```json { ... } ```
83
  """
84
  )
 
1
  from autogen_agentchat.agents import AssistantAgent
2
+ from autogen_core.tools import FunctionTool
3
+ from tools.pl_calculator import calculate_strategy_metrics
4
 
5
  def get_risk_manager(model_client):
6
+ calc_tool = FunctionTool(calculate_strategy_metrics, description="Deterministic math engine. Requires 'legs' (list of dicts with action, type, strike, price, expiry) and 'spot_price' (float).")
7
 
8
  return AssistantAgent(
9
  name="RiskManager",
10
  model_client=model_client,
11
+ tools=[calc_tool],
12
  system_message="""
13
+ You are the Chief Risk Officer and Lead Critic. You do not just validate; you find flaws and demand excellence.
14
+
15
+ MANDATORY 2-ROUND WORKFLOW:
16
+
17
+ ROUND 1: THE CRITIQUE
18
+ 1. ANALYZE the StrategyAdvisor's DRAFT_STRATEGY.
19
+ 2. CRITIQUE blocks:
20
+ - MATH: Is the P/L claim realistic? (Do not call tool yet, just use intuition).
21
+ - REGIME: Does this strategy match the VolatilityAnalyst's findings?
22
+ - EVENT: Did they ignore an earnings date from the FundamentalAnalyst?
23
+ 3. OUTPUT: "CRITIQUE: [Detailed feedback points]" or "PROVISIONALLY APPROVED: Proceed to final math."
24
+
25
+ ROUND 2: THE FINAL VERDICT
26
+ 1. MANDATORY MATH VERIFICATION: Call `calculate_strategy_metrics`.
27
+ - EXAMPLE: `calculate_strategy_metrics(legs=[{"action": "BUY", "type": "CALL", "strike": 100, "price": 5, "expiry": "2024-03-01"}], spot_price=105.5)`
28
+ - You MUST extract the `legs` and `spot_price` from the StrategyAdvisor's message.
29
+ 3. VERIFY the output matches StrategyAdvisor's final claims.
30
+ - CHECK: Ensure `actionable_recommendation` explicitly lists each leg (Strike, Type, Expiry).
31
+ 4. SCORING (STRICT):
32
+ - Technicals (40 pts), Fundamentals (20 pts), Volatility (20 pts), Event Risk (20 pts).
33
+ 4. BE FAST: Use bullet points. No conversational filler.
34
+ 5. NO PLEASANTRIES: Do not say "Thank you" or "You're welcome".
35
+ 6. IF SATISFIED: Output the word "APPROVED" followed by the final JSON immediately.
36
+ 7. IF Still flawed: Suggest "WAIT" and output JSON with decision "WAIT".
37
+
38
+ FINAL JSON SCHEMA:
 
 
 
39
  {
40
+ "ticker": "...",
41
+ "final_decision": "TRADE/WAIT",
42
+ "strategy_type": "...",
43
  "confidence": 85,
44
+ "entry_signal": "...",
45
+ "entry_price": 1.25,
46
+ "max_profit": 200,
47
+ "max_loss": 125,
48
+ "legs": [...],
49
+ "score_card": { "technicals": 40, "fundamentals": 20, "volatility": 15, "sentiment": 10, "total": 85 },
50
+ "actionable_recommendation": "...",
 
 
 
 
 
51
  "risk_warning": "..."
52
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  """
54
  )
src/market-analyst/backend/aagents/sentiment_analyst.py CHANGED
@@ -19,36 +19,25 @@ def get_sentiment_analyst(model_client):
19
 
20
  DO NOT proceed without calling the tool first.
21
 
22
- STEP 2: Aggregate FinBERT Sentiment Scores
23
- - Each article has a FinBERT score like [FinBERT: positive (0.95)] or [FinBERT: negative (0.88)]
24
- - Count positive vs negative vs neutral articles
25
- - Calculate average confidence scores
26
-
27
- STEP 3: Determine Overall Sentiment
28
- - If >70% articles are positive with avg confidence >0.80: "Strongly Bullish"
29
- - If >60% articles are positive with avg confidence >0.70: "Bullish"
30
- - If mixed signals or low confidence: "Neutral"
31
- - If >60% articles are negative with avg confidence >0.70: "Bearish"
32
- - If >70% articles are negative with avg confidence >0.80: "Strongly Bearish"
33
-
34
- STEP 4: Identify Key Events and Risk Factors
35
- - Earnings announcements (CRITICAL: VERIFY article date. IGNORE if >5 days old).
36
- - Product launches
37
- - Regulatory issues
38
- - Management changes
39
- - Sector-wide news
40
-
41
- STEP 5: Assign Sentiment Confidence
42
- - HIGH: >5 articles, >80% agreement, avg FinBERT score >0.85
43
- - MEDIUM: 3-5 articles, 60-80% agreement, avg FinBERT score 0.70-0.85
44
- - LOW: <3 articles, <60% agreement, avg FinBERT score <0.70
45
 
46
  STEP 6: Output Structured Summary
47
- Provide:
48
- - Overall Sentiment (Strongly Bullish/Bullish/Neutral/Bearish/Strongly Bearish)
49
- - Confidence Level (HIGH/MEDIUM/LOW)
50
- - Key Events (list of important news items)
51
- - Risk Factors (potential negative catalysts)
52
- - Recommendation for next analyst (e.g., "Fundamentals should verify if this positive sentiment is justified by earnings")
53
  """
54
  )
 
19
 
20
  DO NOT proceed without calling the tool first.
21
 
22
+ STEP 2: Aggregate & Categorize News Events
23
+ - Identify "Binary Events": Earnings, FDA approvals, Court rulings, Mergers.
24
+ - Identify "Macro Events": Fed news, Inflation, Sector rotation.
25
+ - Rank articles by "Impact Potential" (e.g., Earnings > General News).
26
+
27
+ STEP 3: Determine Overall Sentiment using FinBERT
28
+ - Each article has a FinBERT score like [FinBERT: positive (0.95)]
29
+ - If Binary Events are "Negative", they OVERRIDE general "Neutral" sentiment.
30
+
31
+ STEP 4: Assign Sentiment Level
32
+ - "Strongly Bullish": Coherent positive news across top sources.
33
+ - "Bearish (Event-Driven)": Negative binary news detected.
34
+
35
+ STEP 5: Evaluate Sentiment Confidence vs Time
36
+ - HIGHER weight for news within last 48 hours.
 
 
 
 
 
 
 
 
37
 
38
  STEP 6: Output Structured Summary
39
+ - BE CONCISE: Use maximum 5 bullet points.
40
+ - NO conversational filler.
41
+ - Include: Sentiment Status, Key Binary Events, Primary Risks, and strategy impact.
 
 
 
42
  """
43
  )
src/market-analyst/backend/aagents/strategy_advisor.py CHANGED
@@ -1,114 +1,64 @@
1
  from autogen_agentchat.agents import AssistantAgent
2
- from tools.market_data import get_option_chain_snapshot
 
3
 
4
  def get_strategy_advisor(model_client):
 
 
5
 
6
  return AssistantAgent(
7
  name="StrategyAdvisor",
8
  model_client=model_client,
9
- tools=[get_option_chain_snapshot],
10
  system_message="""
11
- You are an Expert Option Strategist.
12
 
13
- MANDATORY WORKFLOW (FOLLOW EXACTLY):
14
 
15
- STEP 1: Summarize analyst inputs
16
- Review what you learned from:
17
- - TechnicalAnalyst: Market Context (SPY/VIX), Trend, SMA, RSI
18
- - VolatilityAnalyst: IV vs HV, VIX level
19
- - SentimentAnalyst: Market mood, Earnings Risks
20
- - FundamentalAnalyst: P/E, health rating, Analyst Consensus, Earnings Date
 
 
 
 
 
21
 
22
- STEP 2: CALL get_option_chain_snapshot
23
- You MUST call this tool to get real option strikes and prices.
24
- DO NOT proceed without actual option chain data.
25
-
26
- STEP 3: Determine market regime
27
- - Market: Bullish (SPY > SMA50) / Bearish / High Fear (VIX > 25)
28
- - Trend: Bullish / Bearish / Neutral (from Technical)
29
- - Volatility: High (IV > HV, VIX > 20, or "Elevated"/"High" Regime) / Low
30
-
31
- STEP 4: Select strategy using RULES
32
- - HIGH Vol + Range Bound → Iron Condor (Credit)
33
- - HIGH Vol + Directional → Credit Spread (Bull Put / Bear Call)
34
- - LOW Vol + Directional → Debit Spread (Bull Call / Bear Put)
35
- - LOW Vol + Range Bound → Calendar Spread or WAIT
36
-
37
- STEP 5: Validate Risk/Reward (MANDATORY)
38
- - For Debit Spreads: Ensure Max Profit > Max Loss (Reward/Risk > 1.0).
39
- - For Credit Spreads: Ensure Probability of Profit is high (Delta checks).
40
- - METRICS SUMMARY: You MUST summarize your case using these labels before the JSON:
41
- * METRIC: Trend=[BULLISH/BEARISH]
42
- * METRIC: Volatility=[HIGH/LOW]
43
- * METRIC: Sentiment=[POSITIVE/NEGATIVE]
44
- * METRIC: Safety=[SAFE/PREMIUM]
45
-
46
- STEP 6: TEAM COLLABORATION (2 ROUNDS)
47
-
48
- ROUND 1 (DRAFT PHASE):
49
- - State "DRAFT_STRATEGY: [Your Strategy]"
50
- - Explain why you chose this (Regime, Risk/Reward).
51
- - Explicitly ask Risk Manager to review constraints.
52
- - DO NOT output the specific JSON yet, just the logic and proposed strikes.
53
-
54
- ROUND 2 (TEAMS FINALIZATION):
55
- - Review Risk Manager's critique.
56
- - If rejected, or if you switch to WAIT for any reason, you MUST:
57
- 1. Set "strategy" to "WAIT"
58
- 2. Set "estimated_entry_price", "max_profit", and "max_loss" to 0.
59
- - If accepted, Output "FINAL_STRATEGY".
60
- - Calculate Final Score (Standardized Rubric).
61
- - GENERATE THE FINAL JSON BLOCK.
62
-
63
- EXAMPLE OUTPUT (Round 2 Only):
64
- ```json
65
  {
66
- "strategy": "Bull Call Spread",
67
- "direction": "BULLISH",
68
- "confidence_score": 85,
69
- "reasoning": "Strong bullish technicals (price above SMA200, RSI 65), low IV (18% vs HV 22%), positive sentiment. Debit spread appropriate for low-vol bullish setup.",
70
- "proposed_legs": "Buy 145 Call @ $2.50, Sell 150 Call @ $1.20 (Exp: 2024-03-15)",
71
- "entry_signal": "Net Debit",
72
- "estimated_entry_price": 1.30,
73
- "max_profit": 370,
74
- "max_loss": 130,
75
- "breakeven": 146.30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  }
77
- ```
78
- (Note: max_profit/max_loss are calculated for 100 shares/1 contract).
79
 
80
- EXAMPLE OUTPUT (WAIT):
81
- ```json
82
- {
83
- "strategy": "WAIT",
84
- "direction": "NEUTRAL",
85
- "confidence_score": 45,
86
- "reasoning": "Conflicting signals: Bullish technicals but bearish sentiment and high VIX (28). Low confidence setup.",
87
- "proposed_legs": "None",
88
- "entry_signal": "N/A",
89
- "estimated_entry_price": 0,
90
- "max_profit": 0,
91
- "max_loss": 0,
92
- "breakeven": 0
93
- }
94
- ```
95
-
96
- CRITICAL REQUIREMENTS:
97
- 1. MUST call get_option_chain_snapshot before recommending
98
- 2. Use ACTUAL strikes and prices from the option chain
99
- 3. Output MUST be valid JSON in ```json code block (Only in Round 2)
100
- 4. ALL fields are REQUIRED
101
- 5. Show your confidence calculation explicitly
102
- 6. Be verbose - explain your reasoning step-by-step before JSON
103
- 7. LOT-BASED MATH: All profit/loss values (max_profit, max_loss) MUST be multiplied by 100 (standard lot size).
104
- Example: A $1.50 credit spread = $150 Max Profit.
105
-
106
- FALLBACK PROCEDURE:
107
- If get_option_chain_snapshot fails or returns "No options data found":
108
- 1. Do NOT stay silent or crash.
109
- 2. Recommend the strategy WITHOUT specific prices.
110
- 3. In "proposed_legs", write: "Hypothetical: Buy ATM Call, Sell +5% OTM Call (Data Unavailable)"
111
- 4. Set "estimated_entry_price", "max_profit", "max_loss" to 0.
112
- 5. State clearly in "reasoning" that live option data was unavailable.
113
  """
114
  )
 
1
  from autogen_agentchat.agents import AssistantAgent
2
+ from autogen_core.tools import FunctionTool
3
+ from tools.market_data import get_option_chain_snapshot, get_available_expirations
4
 
5
  def get_strategy_advisor(model_client):
6
+ chain_tool = FunctionTool(get_option_chain_snapshot, description="Get option chain for a specific expiry.")
7
+ exp_tool = FunctionTool(get_available_expirations, description="Get all available option expiration dates.")
8
 
9
  return AssistantAgent(
10
  name="StrategyAdvisor",
11
  model_client=model_client,
12
+ tools=[chain_tool, exp_tool],
13
  system_message="""
14
+ You are an Expert Multi-Leg Option Strategist with high self-awareness. You design complex spreads and refine them through self-reflection.
15
 
16
+ MANDATORY HIERARCHICAL WORKFLOW (Team 2):
17
 
18
+ 1. STUDY ANALYST CONTEXT: You will receive a summary from Phase 1.
19
+ 2. CALL DATA TOOLS: Use `get_available_expirations` and `get_option_chain_snapshot`.
20
+ - CONSTRAINT: Use expiries in the 30-60 day range ONLY (1-2 months). Ignore further dates.
21
+ 3. DESIGN STRATEGY: Propose a multi-leg strategy.
22
+ - CRITICAL: You MUST include a `DRAFT_STRATEGY_LEGS` block:
23
+ DRAFT_STRATEGY_LEGS:
24
+ [{"action": "BUY", "type": "CALL", "strike": 150.0, "price": 2.5, "expiry": "2024-03-01"}, ...]
25
+ 4. BE FAST: Skip lengthy reasoning in the draft. Go straight to the legs.
26
+ 5. NO PLEASANTRIES: Do not say "Thank you", "I understand", or "You're welcome".
27
+ 6. FINALIZE: When RiskManager approves, output your final strategy inside a `FINAL_STRATEGY` block.
28
+ - CRITICAL: The `actionable_recommendation` field MUST explicitly list each leg with its type, strike, and EXACT expiry date.
29
 
30
+ JSON SCHEMA (ROUND 2 ONLY):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  {
32
+ "ticker": "...",
33
+ "final_decision": "TRADE/WAIT",
34
+ "actionable_recommendation": "EXPLAIN EACH LEG: 'Buy $150 Call (Exp 2024-03-01), Sell $155 Call (Exp 2024-03-01)...'",
35
+ "strategy_type": "...",
36
+ "direction": "BULLISH/BEARISH/NEUTRAL",
37
+ "confidence": 85,
38
+ "reasoning": "...",
39
+ "entry_signal": "Net Debit/Credit",
40
+ "entry_price": 1.25,
41
+ "max_profit": 200,
42
+ "max_loss": 125,
43
+ "legs": [
44
+ {
45
+ "action": "SELL",
46
+ "type": "CALL",
47
+ "strike": 150,
48
+ "expiry": "2024-03-01",
49
+ "price": 2.50
50
+ },
51
+ {
52
+ "action": "BUY",
53
+ "type": "CALL",
54
+ "strike": 150,
55
+ "expiry": "2024-03-15",
56
+ "price": 3.75
57
+ }
58
+ ],
59
+ "risk_warning": "..."
60
  }
 
 
61
 
62
+ CRITICAL: All profit/loss values MUST be multiplied by 100 per contract.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  """
64
  )
src/market-analyst/backend/aagents/volatility_analyst.py CHANGED
@@ -1,52 +1,41 @@
1
  from autogen_agentchat.agents import AssistantAgent
2
  from autogen_core.tools import FunctionTool
3
- from tools.market_data import get_historical_volatility, get_option_chain_snapshot
4
 
5
  def get_volatility_analyst(model_client):
6
  vol_tool = FunctionTool(get_historical_volatility, description="Get historical volatility and VIX context.")
7
  chain_tool = FunctionTool(get_option_chain_snapshot, description="Get option chain snapshot for near-term expiry.")
 
8
 
9
  return AssistantAgent(
10
  name="VolatilityAnalyst",
11
  model_client=model_client,
12
- tools=[vol_tool, chain_tool],
13
  system_message="""
14
- You are an Expert Volatility & Derivatives Analyst.
15
 
16
  MANDATORY WORKFLOW:
17
 
18
  STEP 1: CALL get_historical_volatility to get HV and VIX data
19
- STEP 2: CALL get_option_chain_snapshot to get IV and option liquidity data
 
20
 
21
- DO NOT proceed without calling BOTH tools first.
22
 
23
- STEP 3: Analyze IV vs HV Relationship
24
- - IV > HV: Options are expensive, good for selling (credit spreads, iron condors)
25
- - IV < HV: Options are cheap, good for buying (debit spreads, long options)
26
- - IV ≈ HV: Fair value, strategy depends on other factors
27
 
28
- STEP 4: Evaluate VIX Context
29
- - VIX < 15: Low market fear, stable environment
30
- - VIX 15-20: Normal volatility
31
- - VIX 20-30: Elevated fear, caution advised
32
- - VIX > 30: High fear, extreme volatility
33
 
34
- STEP 5: Determine Volatility Regime (USE 'volatility_regime' from tool)
35
- - If LOW_VOL: Buy debit spreads or long options.
36
- - If ELEVATED_VOL: Sell credit spreads.
37
- - If HIGH_RISK_VOL: Sell Iron Condors or WAIT.
38
 
39
- STEP 6: Assess Option Liquidity
40
- - Check bid-ask spreads from option chain
41
- - Wide spreads (>$0.50) = Poor liquidity, avoid
42
- - Tight spreads (<$0.20) = Good liquidity, tradeable
43
-
44
- STEP 7: Summarize Findings
45
- Output:
46
- - Volatility Regime classification
47
- - IV vs HV comparison
48
- - VIX level and interpretation
49
- - Liquidity assessment
50
- - Recommendation: "Options are EXPENSIVE - favor selling" or "Options are CHEAP - favor buying"
51
  """
52
  )
 
1
  from autogen_agentchat.agents import AssistantAgent
2
  from autogen_core.tools import FunctionTool
3
+ from tools.market_data import get_historical_volatility, get_option_chain_snapshot, get_volatility_term_structure
4
 
5
  def get_volatility_analyst(model_client):
6
  vol_tool = FunctionTool(get_historical_volatility, description="Get historical volatility and VIX context.")
7
  chain_tool = FunctionTool(get_option_chain_snapshot, description="Get option chain snapshot for near-term expiry.")
8
+ term_tool = FunctionTool(get_volatility_term_structure, description="Get IV across multiple expiries to identify Term Structure skew.")
9
 
10
  return AssistantAgent(
11
  name="VolatilityAnalyst",
12
  model_client=model_client,
13
+ tools=[vol_tool, chain_tool, term_tool],
14
  system_message="""
15
+ You are an Expert Volatility & Derivatives Analyst specializing in Volatility Surface and Term Structure.
16
 
17
  MANDATORY WORKFLOW:
18
 
19
  STEP 1: CALL get_historical_volatility to get HV and VIX data
20
+ STEP 2: CALL get_volatility_term_structure to analyze IV across 4 months of expiries
21
+ STEP 3: CALL get_option_chain_snapshot to get near-term IV and liquidity
22
 
23
+ DO NOT proceed without calling ALL THREE tools first.
24
 
25
+ STEP 4: Analyze IV vs HV (Vertical Skew)
26
+ - IV > HV: Options are rich. Look for Credit Spreads, Iron Condors.
27
+ - IV < HV: Options are cheap. Look for Debit Spreads, Long Options.
 
28
 
29
+ STEP 5: Analyze Term Structure (Horizontal/Time Skew)
30
+ - FRONT IV > BACK IV (Inverted): Potential "Calendar Spread" (Sell Front, Buy Back) if you expect a mean reversion.
31
+ - BACK IV > FRONT IV (Contango): Standard. Long-dated options are more expensive.
 
 
32
 
33
+ STEP 6: Assess "Volatility Squeeze"
34
+ - If IV is at 52-week lows and HV is dropping: Potential for a volatility breakout. Recommend DEBIT strategies.
 
 
35
 
36
+ STEP 7: Output Structured Summary
37
+ - BE CONCISE: Use maximum 5 bullet points.
38
+ - NO conversational filler.
39
+ - Include: Volatility Regime, IV vs HV status, Term Structure summary, and strategy bias.
 
 
 
 
 
 
 
 
40
  """
41
  )
src/market-analyst/backend/consistency_test.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import asyncio
4
+ import json
5
+ import time
6
+
7
+ try:
8
+ from dotenv import load_dotenv
9
+ load_dotenv()
10
+ except ImportError:
11
+ pass
12
+
13
+ # Add path for common and local modules
14
+ current_dir = os.path.dirname(os.path.abspath(__file__))
15
+ repo_root = os.path.abspath(os.path.join(current_dir, "../../../"))
16
+ if repo_root not in sys.path:
17
+ sys.path.append(repo_root)
18
+ if current_dir not in sys.path:
19
+ sys.path.append(current_dir)
20
+
21
+ from common.utility.autogen_model_factory import AutoGenModelFactory
22
+ from teams.team import get_analyst_team, get_decision_team
23
+
24
+ async def run_analysis(ticker, provider, model_name, run_id):
25
+ print(f"\n--- STARTING RUN {run_id} [{provider.upper()} - {model_name}] ---", flush=True)
26
+ start_time = time.time()
27
+
28
+ try:
29
+ model_client = AutoGenModelFactory.get_model(
30
+ provider=provider,
31
+ model_name=model_name,
32
+ temperature=0
33
+ )
34
+ except Exception as e:
35
+ return {"error": f"Model error: {e}"}
36
+
37
+ # PHASE 1
38
+ print(f"[{run_id}] Phase 1: Data Collection...", end="", flush=True)
39
+ analyst_team = get_analyst_team(model_client)
40
+ phase1_task = f"Perform complete analyst data collection for {ticker}."
41
+ analyst_context = []
42
+
43
+ try:
44
+ async for message in analyst_team.run_stream(task=phase1_task):
45
+ source = getattr(message, 'source', 'System')
46
+ content = getattr(message, 'content', '')
47
+ if not content or source == 'User': continue
48
+ if len(str(content)) > 200:
49
+ analyst_context.append(f"[{source}]: {content}")
50
+ except Exception as e:
51
+ return {"error": f"Phase 1 Error: {e}"}
52
+ print("Done.", flush=True)
53
+
54
+ # PHASE 2
55
+ print(f"[{run_id}] Phase 2: Strategy & Risk...", end="", flush=True)
56
+ market_context_str = "\n\n".join(analyst_context)
57
+ decision_team = get_decision_team(model_client)
58
+ phase2_task = f"ANALYST CONTEXT:\n{market_context_str}\n\nGOAL: Design, critique, and finalize trade for {ticker}. Only the LeadOrchestrator can end the cycle."
59
+
60
+ final_json = None
61
+ last_message = ""
62
+ try:
63
+ async for message in decision_team.run_stream(task=phase2_task):
64
+ content = getattr(message, 'content', '')
65
+ source = getattr(message, 'source', 'System')
66
+ if content:
67
+ last_message = f"[{source}]: {content[:500]}"
68
+ if "FINAL_STRATEGY:" in str(content) or "ORCHESTRATOR_DECISION: FINAL_APPROVAL" in str(content):
69
+ try:
70
+ # Search for JSON anywhere in the text
71
+ json_pattern = r'\{.*\}'
72
+ match = re.search(json_pattern, str(content), re.DOTALL)
73
+ if match:
74
+ final_json = json.loads(match.group(0))
75
+ except:
76
+ pass
77
+ except Exception as e:
78
+ return {"error": f"Phase 2 Error: {e}"}
79
+
80
+ if not final_json:
81
+ print(f"DEBUG: No strategy JSON found. Last message preview: {last_message}", flush=True)
82
+
83
+ print("Done.", flush=True)
84
+
85
+ elapsed = time.time() - start_time
86
+ return {
87
+ "provider": provider,
88
+ "model": model_name,
89
+ "time": round(elapsed, 2),
90
+ "strategy": final_json.get("strategy_type", "N/A") if final_json else "N/A",
91
+ "direction": final_json.get("direction", "N/A") if final_json else "N/A",
92
+ "confidence": final_json.get("confidence", 0) if final_json else 0
93
+ }
94
+
95
+ async def main():
96
+ ticker = "META"
97
+ print(f"=== MULTI-MODEL CONSISTENCY BENCHMARK FOR {ticker} ===")
98
+
99
+ tests = [
100
+ {"provider": "openai", "model": "gpt-4o", "label": "GPT-4o Run 1"},
101
+ {"provider": "openai", "model": "gpt-4o", "label": "GPT-4o Run 2"},
102
+ {"provider": "google", "model": "gemini-2.0-flash", "label": "Gemini 2.0 Flash"}
103
+ ]
104
+
105
+ results = []
106
+ for i, test in enumerate(tests):
107
+ res = await run_analysis(ticker, test["provider"], test["model"], i+1)
108
+ results.append(res)
109
+ if "error" in res:
110
+ print(f"Error in {test['label']}: {res['error']}")
111
+
112
+ print("\n" + "="*60)
113
+ print(f"{'Run':<5} {'Model':<20} {'Time':<10} {'Strategy':<20} {'Dir':<10}")
114
+ print("-" * 60)
115
+ for i, res in enumerate(results):
116
+ if "error" in res: continue
117
+ print(f"{i+1:<5} {res['model']:<20} {res['time']:<10} {res['strategy']:<20} {res['direction']:<10}")
118
+ print("="*60)
119
+
120
+ if __name__ == "__main__":
121
+ asyncio.run(main())
src/market-analyst/backend/main.py CHANGED
@@ -17,7 +17,7 @@ if current_dir not in sys.path:
17
  sys.path.append(current_dir)
18
 
19
  from common.utility.autogen_model_factory import AutoGenModelFactory
20
- from teams.team import get_trading_team, extract_json
21
  from tools.news_data import get_sentiment_pipeline
22
 
23
  app = FastAPI(title="Market Analyst API")
@@ -35,8 +35,6 @@ active_analyses = {}
35
 
36
  @app.on_event("startup")
37
  async def startup_event():
38
- # Warm up the model in a background thread if possible,
39
- # but for now let's just trigger the lazy load.
40
  print("Warming up FinBERT...")
41
  get_sentiment_pipeline()
42
 
@@ -44,12 +42,10 @@ async def startup_event():
44
  async def health():
45
  return {"status": "healthy"}
46
 
47
-
48
  @app.post("/cancel/{analysis_id}")
49
  async def cancel_analysis(analysis_id: str):
50
- """Cancel a running analysis."""
51
  if analysis_id in active_analyses:
52
- active_analyses[analysis_id] = True # Mark as cancelled
53
  return {"status": "cancelled", "analysis_id": analysis_id}
54
  return {"status": "not_found", "analysis_id": analysis_id}
55
 
@@ -57,40 +53,9 @@ async def cancel_analysis(analysis_id: str):
57
  async def analyze(ticker: str, provider: str = "openai"):
58
  import uuid
59
  analysis_id = str(uuid.uuid4())
60
- active_analyses[analysis_id] = False # False = not cancelled
61
 
62
  async def event_generator() -> AsyncGenerator[str, None]:
63
- # Guardrail: Check Trading Hours (9:30 AM - 4:00 PM ET, Mon-Fri)
64
- guardrail_enabled = os.getenv("MARKET_GUARDRAIL_ON", "true").lower() == "true"
65
-
66
- if guardrail_enabled:
67
- try:
68
- from datetime import datetime, time
69
- import pytz
70
-
71
- et_tz = pytz.timezone('US/Eastern')
72
- now_et = datetime.now(et_tz)
73
-
74
- # Check if weekend (Saturday=5, Sunday=6)
75
- is_weekend = now_et.weekday() >= 5
76
-
77
- # Check market hours (09:30 - 16:00)
78
- market_open = time(9, 30)
79
- market_close = time(16, 0)
80
- is_market_hours = market_open <= now_et.time() <= market_close
81
-
82
- if is_weekend or not is_market_hours:
83
- msg = f"MARKET CLOSED ({now_et.strftime('%I:%M %p')} ET). Analysis requires live data. Please return Mon-Fri, 9:30 AM - 4:00 PM ET.\nSet MARKET_GUARDRAIL_ON=false to bypass."
84
- yield f"data: {json.dumps({'source': 'System', 'content': msg, 'error': msg})}\n\n"
85
- yield "data: [DONE]\n\n"
86
- return
87
- except ImportError:
88
- print("Warning: pytz not found, skipping market hours check.")
89
- pass
90
- except Exception as e:
91
- print(f"Time check error: {e}")
92
-
93
- # Setup Model
94
  if provider == "openai":
95
  model_name = "gpt-4o"
96
  family = "gpt"
@@ -98,8 +63,6 @@ async def analyze(ticker: str, provider: str = "openai"):
98
  model_name = "llama-3.3-70b-versatile"
99
  family = "groq"
100
  elif provider == "google":
101
- # Using Gemini Pro for more robust reasoning and
102
- # higher quality decision making across multiple agents.
103
  model_name = "gemini-pro-latest"
104
  family = "gemini"
105
  else:
@@ -107,119 +70,79 @@ async def analyze(ticker: str, provider: str = "openai"):
107
  family = "gpt"
108
 
109
  try:
110
- temp = 0
111
- # For Non-OpenAI providers, let the factory handle default model_info metadata
112
- if provider in ["google", "groq"]:
113
- info = None
114
- else:
115
- info = {"family": family, "vision": False, "function_calling": True, "json_output": True, "structured_output": True}
116
-
117
  model_client = AutoGenModelFactory.get_model(
118
  provider=provider,
119
  model_name=model_name,
120
- temperature=temp,
121
- model_info=info
 
 
 
 
 
 
122
  )
123
  except Exception as e:
124
  yield f"data: {json.dumps({'error': f'Model initialization failed: {str(e)}'})}\n\n"
125
  return
126
 
127
- team = get_trading_team(model_client)
128
- task = f"""
129
- Perform a professional multi-agent trade analysis for {ticker.upper()}.
130
- 1. TechnicalAnalyst: Deep chart study, SMA trends, and RSI momentum.
131
- 2. VolatilityAnalyst: Study IV vs HV, VIX context, and option chain liquidity.
132
- 3. SentimentAnalyst: News sentiment (Top 5 stories) and market mood.
133
- 4. FundamentalAnalyst: Check P/E, PEG, and balance sheet health.
134
- 5. StrategyAdvisor: MUST call get_option_chain_snapshot to get real strikes. Use findings from all analysts to recommend an optimal option spread with SPECIFIC STRIKES AND PRICES.
135
- 6. RiskManager: Final validation. Output JSON with "final_decision" (TRADE/WAIT), "confidence", and "actionable_recommendation".
136
- """
137
-
138
- # Yield initial status
139
- yield f"data: {json.dumps({'source': 'System', 'content': 'Starting sequential analysis for ' + ticker.upper() + '...', 'analysis_id': analysis_id})}\n\n"
140
 
141
  try:
142
- async for message in team.run_stream(task=task):
143
- # Check if cancelled
144
- if active_analyses.get(analysis_id, False):
145
- yield f"data: {json.dumps({'source': 'System', 'content': 'Analysis cancelled by user.'})}\n\n"
146
- yield "data: [DONE]\n\n"
147
- break
148
-
149
  raw_source = getattr(message, 'source', 'System')
150
  content = getattr(message, 'content', '')
151
-
152
- # Check for tool_calls if content is empty (Explains 0-len messages)
153
- if not content:
154
- tool_calls = getattr(message, 'tool_calls', None)
155
- if tool_calls:
156
- content = f"[Tool Call] Executing {len(tool_calls)} function(s)."
157
-
158
- # Handle non-string content (e.g., ToolCalls/FunctionCalls)
159
- if not isinstance(content, str):
160
- try:
161
- content = str(content)
162
- except:
163
- content = "[Complex Content]"
164
-
165
- if not content and not hasattr(message, 'models_usage'):
166
- continue
167
-
168
- # Skip echoing the huge prompt task
169
- if raw_source.lower() == 'user' and "Perform a professional multi-agent" in content:
170
- continue
171
-
172
- payload = {
173
- "source": raw_source,
174
- "content": content
175
- }
176
-
177
- # If RiskManager, try to extract structured JSON for the frontend
178
- if raw_source == 'RiskManager':
179
- structured = extract_json(content)
180
- if structured:
181
- payload["structured_result"] = structured
182
-
183
- print(f"[DEBUG] Sent: {raw_source} (len: {len(content)})")
184
  yield f"data: {json.dumps(payload)}\n\n"
185
  except Exception as e:
186
- print(f"[STREAM ERROR] {str(e)}")
187
- error_msg = f"Analysis execution failed: {str(e)}"
188
- yield f"data: {json.dumps({'source': 'Error', 'content': error_msg})}\n\n"
189
-
190
- print("[DEBUG] Done.")
191
- # Cleanup
192
- if analysis_id in active_analyses:
193
- del active_analyses[analysis_id]
194
  yield "data: [DONE]\n\n"
195
 
196
  return StreamingResponse(event_generator(), media_type="text/event-stream")
197
 
198
- # Serve Frontend Static Files
199
  frontend_dist = os.path.abspath(os.path.join(current_dir, "../frontend/dist"))
200
- print(f"Checking for frontend at: {frontend_dist}")
201
-
202
  if os.path.exists(frontend_dist):
203
- # Mount assets folder explicitly
204
  assets_dir = os.path.join(frontend_dist, "assets")
205
  if os.path.exists(assets_dir):
206
  app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
207
-
208
- # Serve index.html for the root and any other non-API routes
209
  from fastapi.responses import FileResponse
210
  @app.get("/{rest_of_path:path}")
211
  async def serve_frontend(rest_of_path: str):
212
- # If it's a file that exists in dist, serve it
213
  file_path = os.path.join(frontend_dist, rest_of_path)
214
- if os.path.isfile(file_path):
215
- return FileResponse(file_path)
216
- # Otherwise serve index.html (SPA routing)
217
  return FileResponse(os.path.join(frontend_dist, "index.html"))
218
  else:
219
- print("WARNING: Frontend dist folder not found!")
220
  @app.get("/")
221
- async def root():
222
- return {"message": "Market Analyst API is running. Frontend not built.", "path": frontend_dist}
223
 
224
  if __name__ == "__main__":
225
  import uvicorn
 
17
  sys.path.append(current_dir)
18
 
19
  from common.utility.autogen_model_factory import AutoGenModelFactory
20
+ from teams.team import get_analyst_team, get_decision_team, extract_json
21
  from tools.news_data import get_sentiment_pipeline
22
 
23
  app = FastAPI(title="Market Analyst API")
 
35
 
36
  @app.on_event("startup")
37
  async def startup_event():
 
 
38
  print("Warming up FinBERT...")
39
  get_sentiment_pipeline()
40
 
 
42
  async def health():
43
  return {"status": "healthy"}
44
 
 
45
  @app.post("/cancel/{analysis_id}")
46
  async def cancel_analysis(analysis_id: str):
 
47
  if analysis_id in active_analyses:
48
+ active_analyses[analysis_id] = True
49
  return {"status": "cancelled", "analysis_id": analysis_id}
50
  return {"status": "not_found", "analysis_id": analysis_id}
51
 
 
53
  async def analyze(ticker: str, provider: str = "openai"):
54
  import uuid
55
  analysis_id = str(uuid.uuid4())
56
+ active_analyses[analysis_id] = False
57
 
58
  async def event_generator() -> AsyncGenerator[str, None]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  if provider == "openai":
60
  model_name = "gpt-4o"
61
  family = "gpt"
 
63
  model_name = "llama-3.3-70b-versatile"
64
  family = "groq"
65
  elif provider == "google":
 
 
66
  model_name = "gemini-pro-latest"
67
  family = "gemini"
68
  else:
 
70
  family = "gpt"
71
 
72
  try:
 
 
 
 
 
 
 
73
  model_client = AutoGenModelFactory.get_model(
74
  provider=provider,
75
  model_name=model_name,
76
+ temperature=0,
77
+ model_info={
78
+ "family": family,
79
+ "vision": False,
80
+ "function_calling": True,
81
+ "json_output": True,
82
+ "structured_output": True if provider == "openai" else False
83
+ }
84
  )
85
  except Exception as e:
86
  yield f"data: {json.dumps({'error': f'Model initialization failed: {str(e)}'})}\n\n"
87
  return
88
 
89
+ # PHASE 1: Analysts
90
+ yield f"data: {json.dumps({'source': 'System', 'content': 'PHASE 1: Starting Data Collection for ' + ticker.upper(), 'analysis_id': analysis_id})}\n\n"
91
+ analyst_team = get_analyst_team(model_client)
92
+ phase1_task = f"Perform complete analyst data collection for {ticker.upper()}."
93
+ analyst_context = []
 
 
 
 
 
 
 
 
94
 
95
  try:
96
+ async for message in analyst_team.run_stream(task=phase1_task):
97
+ if active_analyses.get(analysis_id, False): break
 
 
 
 
 
98
  raw_source = getattr(message, 'source', 'System')
99
  content = getattr(message, 'content', '')
100
+ if not content or raw_source == 'User': continue
101
+ analyst_context.append(f"[{raw_source}]: {content}")
102
+ yield f"data: {json.dumps({'source': raw_source, 'content': str(content)})}\n\n"
103
+ except Exception as e:
104
+ yield f"data: {json.dumps({'source': 'Error', 'content': f'Phase 1 bug: {str(e)}'})}\n\n"
105
+
106
+ # PHASE 2: Decision
107
+ yield f"data: {json.dumps({'source': 'System', 'content': 'PHASE 2: Designing Strategy...'})}\n\n"
108
+ market_context_str = "\n\n".join(analyst_context)
109
+ decision_team = get_decision_team(model_client)
110
+ phase2_task = f"ANALYST CONTEXT:\n{market_context_str}\n\nGOAL: Design, critique, and finalize trade for {ticker}. Only the LeadOrchestrator can end the cycle."
111
+
112
+ try:
113
+ async for message in decision_team.run_stream(task=phase2_task):
114
+ if active_analyses.get(analysis_id, False): break
115
+ raw_source = getattr(message, 'source', 'System')
116
+ content = getattr(message, 'content', '')
117
+ if not content or (raw_source == 'User' and "ANALYST CONTEXT" in content): continue
118
+ payload = {"source": raw_source, "content": str(content)}
119
+ if raw_source in ['RiskManager', 'LeadOrchestrator']:
120
+ structured = extract_json(str(content))
121
+ if structured: payload["structured_result"] = structured
 
 
 
 
 
 
 
 
 
 
 
122
  yield f"data: {json.dumps(payload)}\n\n"
123
  except Exception as e:
124
+ yield f"data: {json.dumps({'source': 'Error', 'content': f'Phase 2 bug: {str(e)}'})}\n\n"
125
+
126
+ if analysis_id in active_analyses: del active_analyses[analysis_id]
 
 
 
 
 
127
  yield "data: [DONE]\n\n"
128
 
129
  return StreamingResponse(event_generator(), media_type="text/event-stream")
130
 
131
+ # Static mounting logic...
132
  frontend_dist = os.path.abspath(os.path.join(current_dir, "../frontend/dist"))
 
 
133
  if os.path.exists(frontend_dist):
 
134
  assets_dir = os.path.join(frontend_dist, "assets")
135
  if os.path.exists(assets_dir):
136
  app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
 
 
137
  from fastapi.responses import FileResponse
138
  @app.get("/{rest_of_path:path}")
139
  async def serve_frontend(rest_of_path: str):
 
140
  file_path = os.path.join(frontend_dist, rest_of_path)
141
+ if os.path.isfile(file_path): return FileResponse(file_path)
 
 
142
  return FileResponse(os.path.join(frontend_dist, "index.html"))
143
  else:
 
144
  @app.get("/")
145
+ async def root(): return {"message": "API running. Frontend missing."}
 
146
 
147
  if __name__ == "__main__":
148
  import uvicorn
src/market-analyst/backend/model_test.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ from dotenv import load_dotenv
4
+ load_dotenv()
5
+ from autogen_ext.models.openai import OpenAIChatCompletionClient
6
+
7
+ import sys
8
+ current_dir = os.path.dirname(os.path.abspath(__file__))
9
+ repo_root = os.path.abspath(os.path.join(current_dir, "../../../"))
10
+ if repo_root not in sys.path:
11
+ sys.path.append(repo_root)
12
+
13
+ async def main():
14
+ try:
15
+ from common.utility.autogen_model_factory import AutoGenModelFactory
16
+ client = AutoGenModelFactory.get_model(provider="openai", model_name="gpt-4o")
17
+ from autogen_core.models import UserMessage
18
+ resp = await client.create([UserMessage(content="Say hello", source="user")])
19
+ print(f"Type: {type(resp)}")
20
+ print(f"Response: {resp.content}")
21
+ except Exception as e:
22
+ import traceback
23
+ print(f"DEBUG_ERROR: {e}")
24
+ traceback.print_exc()
25
+
26
+ if __name__ == "__main__":
27
+ asyncio.run(main())
src/market-analyst/backend/teams/team.py CHANGED
@@ -10,8 +10,8 @@ parent_dir = os.path.abspath(os.path.join(current_dir, ".."))
10
  if parent_dir not in sys.path:
11
  sys.path.append(parent_dir)
12
 
13
- from autogen_agentchat.teams import RoundRobinGroupChat
14
- from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination
15
 
16
  # Import agents
17
  # Adjust imports to work whether called from here or app.py
@@ -22,6 +22,7 @@ try:
22
  from ..aagents.strategy_advisor import get_strategy_advisor
23
  from ..aagents.risk_manager import get_risk_manager
24
  from ..aagents.fundamental_analyst import get_fundamental_analyst
 
25
  except ImportError:
26
  try:
27
  from aagents.market_analyst import get_technical_analyst
@@ -30,6 +31,7 @@ except ImportError:
30
  from aagents.strategy_advisor import get_strategy_advisor
31
  from aagents.risk_manager import get_risk_manager
32
  from aagents.fundamental_analyst import get_fundamental_analyst
 
33
  except ImportError:
34
  # Try absolute (if market-analyst is in path but not as package)
35
  from src.market_analyst.backend.aagents.market_analyst import get_technical_analyst
@@ -39,23 +41,49 @@ except ImportError:
39
  from src.market_analyst.backend.aagents.risk_manager import get_risk_manager
40
  from src.market_analyst.backend.aagents.fundamental_analyst import get_fundamental_analyst
41
 
42
- def get_trading_team(model_client):
 
 
43
  """
44
- Creates and returns the RoundRobinGroupChat team for predictable sequential execution.
 
45
  """
46
  technical = get_technical_analyst(model_client)
47
  volatility = get_volatility_analyst(model_client)
48
  sentiment = get_sentiment_analyst(model_client)
49
  fundamental = get_fundamental_analyst(model_client)
 
 
 
 
 
 
 
 
 
 
 
50
  strategy = get_strategy_advisor(model_client)
51
  risk = get_risk_manager(model_client)
 
52
 
53
- team = RoundRobinGroupChat(
54
- participants=[technical, volatility, sentiment, fundamental, strategy, risk],
55
- # Increased limit for 2-round detailed discussion
56
- termination_condition=TextMentionTermination("APPROVED") | MaxMessageTermination(60)
 
 
 
 
 
 
 
 
 
 
 
 
57
  )
58
- return team
59
 
60
  def extract_json(text: str) -> Dict[str, Any]:
61
  """
@@ -98,7 +126,9 @@ def validate_and_complete_json(data: Dict[str, Any]) -> Dict[str, Any]:
98
  "entry_price": 0,
99
  "max_profit": 0,
100
  "max_loss": 0,
101
- "risk_warning": "Analysis incomplete"
 
 
102
  }
103
 
104
  # Add missing required fields with defaults
@@ -106,4 +136,23 @@ def validate_and_complete_json(data: Dict[str, Any]) -> Dict[str, Any]:
106
  if field not in data:
107
  data[field] = default_value
108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  return data
 
10
  if parent_dir not in sys.path:
11
  sys.path.append(parent_dir)
12
 
13
+ from autogen_agentchat.teams import SelectorGroupChat
14
+ from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination, HandoffTermination
15
 
16
  # Import agents
17
  # Adjust imports to work whether called from here or app.py
 
22
  from ..aagents.strategy_advisor import get_strategy_advisor
23
  from ..aagents.risk_manager import get_risk_manager
24
  from ..aagents.fundamental_analyst import get_fundamental_analyst
25
+ from ..aagents.orchestrator import get_lead_orchestrator
26
  except ImportError:
27
  try:
28
  from aagents.market_analyst import get_technical_analyst
 
31
  from aagents.strategy_advisor import get_strategy_advisor
32
  from aagents.risk_manager import get_risk_manager
33
  from aagents.fundamental_analyst import get_fundamental_analyst
34
+ from aagents.orchestrator import get_lead_orchestrator
35
  except ImportError:
36
  # Try absolute (if market-analyst is in path but not as package)
37
  from src.market_analyst.backend.aagents.market_analyst import get_technical_analyst
 
41
  from src.market_analyst.backend.aagents.risk_manager import get_risk_manager
42
  from src.market_analyst.backend.aagents.fundamental_analyst import get_fundamental_analyst
43
 
44
+ from autogen_agentchat.teams import SelectorGroupChat, RoundRobinGroupChat
45
+
46
+ def get_analyst_team(model_client):
47
  """
48
+ Team 1: DATA COLLECTORS.
49
+ Independent analysts gather data and provide a comprehensive market snapshot.
50
  """
51
  technical = get_technical_analyst(model_client)
52
  volatility = get_volatility_analyst(model_client)
53
  sentiment = get_sentiment_analyst(model_client)
54
  fundamental = get_fundamental_analyst(model_client)
55
+
56
+ return RoundRobinGroupChat(
57
+ participants=[technical, volatility, sentiment, fundamental],
58
+ termination_condition=TextMentionTermination("[[DATA_COLLECTION_COMPLETE]]") | MaxMessageTermination(15)
59
+ )
60
+
61
+ def get_decision_team(model_client):
62
+ """
63
+ Team 2: STRATEGY & RISK.
64
+ Uses the Analyst Context (Team 1 output) to design, critique, and finalize the trade.
65
+ """
66
  strategy = get_strategy_advisor(model_client)
67
  risk = get_risk_manager(model_client)
68
+ orchestrator = get_lead_orchestrator(model_client)
69
 
70
+ participants = [strategy, risk, orchestrator]
71
+
72
+ selector_prompt = """
73
+ Select the next agent based on the conversation history:
74
+ - Choose StrategyAdvisor to propose or update the trade.
75
+ - Choose RiskManager to verify the proposal or critique it.
76
+ - Choose LeadOrchestrator ONLY if the RiskManager has said "APPROVED" or if the discussion is stuck.
77
+
78
+ Output only the name of the next agent.
79
+ """
80
+
81
+ return SelectorGroupChat(
82
+ participants=participants,
83
+ model_client=model_client,
84
+ termination_condition=TextMentionTermination("[[ANALYSIS_JUDGMENT_COMPLETE]]") | MaxMessageTermination(12),
85
+ selector_prompt=selector_prompt
86
  )
 
87
 
88
  def extract_json(text: str) -> Dict[str, Any]:
89
  """
 
126
  "entry_price": 0,
127
  "max_profit": 0,
128
  "max_loss": 0,
129
+ "risk_warning": "Analysis incomplete",
130
+ "expiry_date": "N/A",
131
+ "legs": []
132
  }
133
 
134
  # Add missing required fields with defaults
 
136
  if field not in data:
137
  data[field] = default_value
138
 
139
+ # Fallback: If expiry_date is "N/A" but we have legs, take it from there
140
+ if data.get("expiry_date") == "N/A" and data.get("legs"):
141
+ # Take expiry of first leg
142
+ data["expiry_date"] = data["legs"][0].get("expiry", "N/A")
143
+
144
+ # Existing Fallback: If expiry_date is still "N/A", try to extract it from context
145
+ if data.get("expiry_date") == "N/A":
146
+ # Look for YYYY-MM-DD pattern
147
+ date_pattern = r'\d{4}-\d{2}-\d{2}'
148
+
149
+ # Check 'actionable_recommendation' or 'reasoning' (if present)
150
+ for search_field in ["actionable_recommendation", "reasoning", "risk_warning", "proposed_legs"]:
151
+ field_val = data.get(search_field, "")
152
+ if isinstance(field_val, str):
153
+ match = re.search(date_pattern, field_val)
154
+ if match:
155
+ data["expiry_date"] = match.group(0)
156
+ break
157
+
158
  return data
src/market-analyst/backend/test_gemini.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import os
3
+ try:
4
+ from dotenv import load_dotenv
5
+ load_dotenv()
6
+ except ImportError:
7
+ pass
8
+ from common.utility.autogen_model_factory import AutoGenModelFactory
9
+ from autogen_agentchat.agents import AssistantAgent
10
+ from autogen_agentchat.teams import SelectorGroupChat
11
+ from autogen_agentchat.conditions import MaxMessageTermination
12
+
13
+ async def test_gemini_selection():
14
+ model_client = AutoGenModelFactory.get_model(
15
+ provider="google",
16
+ model_name="gemini-2.0-flash",
17
+ temperature=0
18
+ )
19
+
20
+ a = AssistantAgent("AgentA", model_client, system_message="User wants to say hi.")
21
+ b = AssistantAgent("AgentB", model_client, system_message="You say hello back.")
22
+
23
+ team = SelectorGroupChat(
24
+ [a, b],
25
+ model_client=model_client,
26
+ termination_condition=MaxMessageTermination(2),
27
+ selector_prompt="Select AgentA first, then AgentB."
28
+ )
29
+
30
+ print("Starting team run...")
31
+ async for message in team.run_stream(task="Say hello"):
32
+ print(f"[{getattr(message, 'source', 'System')}] {getattr(message, 'content', '')}")
33
+
34
+ if __name__ == "__main__":
35
+ asyncio.run(test_gemini_selection())
src/market-analyst/backend/tools/market_data.py CHANGED
@@ -3,6 +3,15 @@ import pandas as pd
3
  import math
4
  from datetime import datetime, timedelta
5
 
 
 
 
 
 
 
 
 
 
6
  def check_and_fix_ticker(symbol: str) -> str:
7
  """
8
  Checks if the ticker has data. If not, tries appending '.NS' (for NSE India).
@@ -59,6 +68,7 @@ def get_historical_volatility(symbol: str, period: str = "1mo") -> dict:
59
  print(f"[DEBUG] get_historical_volatility called for: {symbol}")
60
  try:
61
  symbol = check_and_fix_ticker(symbol)
 
62
  ticker = yf.Ticker(symbol)
63
  hist = ticker.history(period=period)
64
  if hist.empty:
@@ -84,11 +94,21 @@ def get_historical_volatility(symbol: str, period: str = "1mo") -> dict:
84
  except Exception as e:
85
  return {"error": str(e)}
86
 
87
- def get_option_chain_snapshot(symbol: str) -> str:
 
 
 
 
 
 
 
 
 
88
  """
89
- Fetches a snapshot of the option chain.
 
90
  """
91
- print(f"[DEBUG] get_option_chain_snapshot called for: {symbol}")
92
  try:
93
  symbol = check_and_fix_ticker(symbol)
94
  ticker = yf.Ticker(symbol)
@@ -97,19 +117,16 @@ def get_option_chain_snapshot(symbol: str) -> str:
97
  if not expirations:
98
  return f"No options data found for {symbol}."
99
 
100
- target_date = None
101
- today = datetime.now()
102
-
103
- for exp in expirations:
104
- exp_date = datetime.strptime(exp, "%Y-%m-%d")
105
- days_to_exp = (exp_date - today).days
106
- # Adjusted window: 7 to 45 days to capture monthly expiries for better liquidity
107
- if 7 <= days_to_exp <= 45:
108
- target_date = exp
109
- break
110
-
111
- if not target_date:
112
- target_date = expirations[0]
113
 
114
  opt = ticker.option_chain(target_date)
115
  calls = opt.calls
@@ -119,7 +136,7 @@ def get_option_chain_snapshot(symbol: str) -> str:
119
  if isinstance(price_info, str): return price_info
120
  current_price = float(price_info)
121
 
122
- # Filter around ATM for most relevant strikes (closest 6 calls/puts)
123
  ntm_calls = calls.iloc[(calls['strike'] - current_price).abs().argsort()[:6]].sort_values('strike')
124
  ntm_puts = puts.iloc[(puts['strike'] - current_price).abs().argsort()[:6]].sort_values('strike')
125
 
@@ -131,8 +148,7 @@ def get_option_chain_snapshot(symbol: str) -> str:
131
  last = row.get('lastPrice', 0.0)
132
  ask = row.get('ask', 0.0)
133
  vol = row.get('volume', 0)
134
- iv = round(row['impliedVolatility']*100, 1)
135
- # Fallback logic for display clarity
136
  price_display = f"{ask}" if ask > 0 else f"{last} (Last)"
137
  summary += f"Strike: {row['strike']} | Price: {price_display} | IV: {iv}% | Vol: {vol}\n"
138
 
@@ -141,8 +157,7 @@ def get_option_chain_snapshot(symbol: str) -> str:
141
  last = row.get('lastPrice', 0.0)
142
  ask = row.get('ask', 0.0)
143
  vol = row.get('volume', 0)
144
- iv = round(row['impliedVolatility']*100, 1)
145
- # Fallback logic for display clarity
146
  price_display = f"{ask}" if ask > 0 else f"{last} (Last)"
147
  summary += f"Strike: {row['strike']} | Price: {price_display} | IV: {iv}% | Vol: {vol}\n"
148
 
@@ -151,6 +166,49 @@ def get_option_chain_snapshot(symbol: str) -> str:
151
  except Exception as e:
152
  return f"Error fetching option chain: {str(e)}"
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  def get_market_indices() -> str:
155
  """
156
  Fetches current market context using SPY (S&P 500) and ^VIX.
 
3
  import math
4
  from datetime import datetime, timedelta
5
 
6
+ def normalize_period(period: str) -> str:
7
+ """Standardizes period strings for yfinance."""
8
+ p = period.lower().strip()
9
+ if p in ["1yr", "1year"]: return "1y"
10
+ if p in ["3mo", "3month"]: return "3mo"
11
+ if p in ["1mo", "1month"]: return "1mo"
12
+ if p in ["1wk", "1week"]: return "1wk"
13
+ return p
14
+
15
  def check_and_fix_ticker(symbol: str) -> str:
16
  """
17
  Checks if the ticker has data. If not, tries appending '.NS' (for NSE India).
 
68
  print(f"[DEBUG] get_historical_volatility called for: {symbol}")
69
  try:
70
  symbol = check_and_fix_ticker(symbol)
71
+ period = normalize_period(period)
72
  ticker = yf.Ticker(symbol)
73
  hist = ticker.history(period=period)
74
  if hist.empty:
 
94
  except Exception as e:
95
  return {"error": str(e)}
96
 
97
+ def get_available_expirations(symbol: str) -> list:
98
+ """Returns a list of available option expiration dates."""
99
+ try:
100
+ symbol = check_and_fix_ticker(symbol)
101
+ ticker = yf.Ticker(symbol)
102
+ return list(ticker.options)
103
+ except Exception as e:
104
+ return []
105
+
106
+ def get_option_chain_snapshot(symbol: str, target_date: str = None) -> str:
107
  """
108
+ Fetches a snapshot of the option chain for a specific expiry.
109
+ If target_date is None, picks the nearest liquid monthly expiry.
110
  """
111
+ print(f"[DEBUG] get_option_chain_snapshot called for: {symbol} (Target: {target_date})")
112
  try:
113
  symbol = check_and_fix_ticker(symbol)
114
  ticker = yf.Ticker(symbol)
 
117
  if not expirations:
118
  return f"No options data found for {symbol}."
119
 
120
+ if not target_date or target_date not in expirations:
121
+ today = datetime.now()
122
+ for exp in expirations:
123
+ exp_date = datetime.strptime(exp, "%Y-%m-%d")
124
+ days_to_exp = (exp_date - today).days
125
+ if 7 <= days_to_exp <= 45:
126
+ target_date = exp
127
+ break
128
+ if not target_date:
129
+ target_date = expirations[0]
 
 
 
130
 
131
  opt = ticker.option_chain(target_date)
132
  calls = opt.calls
 
136
  if isinstance(price_info, str): return price_info
137
  current_price = float(price_info)
138
 
139
+ # Filter around ATM for most relevant strikes
140
  ntm_calls = calls.iloc[(calls['strike'] - current_price).abs().argsort()[:6]].sort_values('strike')
141
  ntm_puts = puts.iloc[(puts['strike'] - current_price).abs().argsort()[:6]].sort_values('strike')
142
 
 
148
  last = row.get('lastPrice', 0.0)
149
  ask = row.get('ask', 0.0)
150
  vol = row.get('volume', 0)
151
+ iv = round(row['impliedVolatility']*100, 1) if not pd.isna(row.get('impliedVolatility')) else 0
 
152
  price_display = f"{ask}" if ask > 0 else f"{last} (Last)"
153
  summary += f"Strike: {row['strike']} | Price: {price_display} | IV: {iv}% | Vol: {vol}\n"
154
 
 
157
  last = row.get('lastPrice', 0.0)
158
  ask = row.get('ask', 0.0)
159
  vol = row.get('volume', 0)
160
+ iv = round(row['impliedVolatility']*100, 1) if not pd.isna(row.get('impliedVolatility')) else 0
 
161
  price_display = f"{ask}" if ask > 0 else f"{last} (Last)"
162
  summary += f"Strike: {row['strike']} | Price: {price_display} | IV: {iv}% | Vol: {vol}\n"
163
 
 
166
  except Exception as e:
167
  return f"Error fetching option chain: {str(e)}"
168
 
169
+ def get_volatility_term_structure(symbol: str) -> str:
170
+ """
171
+ Analyzes IV across multiple expiries to identify Term Structure skew.
172
+ """
173
+ print(f"[DEBUG] get_volatility_term_structure called for: {symbol}")
174
+ try:
175
+ symbol = check_and_fix_ticker(symbol)
176
+ ticker = yf.Ticker(symbol)
177
+ expirations = ticker.options[:4] # Check first 4 expiries
178
+
179
+ if not expirations:
180
+ return "No options data for volatility analysis."
181
+
182
+ results = []
183
+ for exp in expirations:
184
+ opt = ticker.option_chain(exp)
185
+ # Use mean IV of ATM calls
186
+ calls = opt.calls
187
+ price_info = get_current_price(symbol)
188
+ if isinstance(price_info, str): continue
189
+ current_price = float(price_info)
190
+ atm_iv = calls.iloc[(calls['strike'] - current_price).abs().argsort()[:2]]['impliedVolatility'].mean()
191
+ results.append(f"- {exp}: {round(atm_iv * 100, 1)}% IV")
192
+
193
+ summary = f"VOLATILITY TERM STRUCTURE for {symbol}:\n" + "\n".join(results)
194
+
195
+ # Analyze skew
196
+ if len(expirations) >= 2:
197
+ try:
198
+ iv1 = float(results[0].split(": ")[1].replace("% IV", ""))
199
+ iv2 = float(results[1].split(": ")[1].replace("% IV", ""))
200
+ if iv1 > iv2 + 5:
201
+ summary += f"\n\nSKEW ALERT: Front-month IV is significantly HIGHER ({iv1}% vs {iv2}%). Potential for Calendar Spreads (Sell Front, Buy Back)."
202
+ elif iv1 < iv2 - 5:
203
+ summary += f"\n\nSKEW ALERT: Front-month IV is significantly LOWER ({iv1}% vs {iv2}%). Diagonal opportunities."
204
+ else:
205
+ summary += f"\n\nTerm Structure is relatively flat."
206
+ except: pass
207
+
208
+ return summary
209
+ except Exception as e:
210
+ return f"Error analyzing term structure: {str(e)}"
211
+
212
  def get_market_indices() -> str:
213
  """
214
  Fetches current market context using SPY (S&P 500) and ^VIX.
src/market-analyst/backend/tools/pl_calculator.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from typing import List, Dict, Any
3
+
4
+ def calculate_strategy_metrics(legs: List[Dict[str, Any]], spot_price: float) -> Dict[str, Any]:
5
+ """
6
+ Calculates Max Profit, Max Loss, and Breakeven for a given set of option legs.
7
+ Leg format: {'action': 'BUY'/'SELL', 'type': 'CALL'/'PUT', 'strike': float, 'price': float, 'expiry': str}
8
+ """
9
+ if not legs:
10
+ return {"max_profit": 0, "max_loss": 0, "breakeven": 0, "net_cost": 0}
11
+
12
+ # 1. Calculate Net Debit/Credit
13
+ net_premium = 0
14
+ for leg in legs:
15
+ multiplier = 1 if leg['action'].upper() == 'BUY' else -1
16
+ net_premium += leg['price'] * multiplier
17
+
18
+ # Positive net_premium = Debit (Paying)
19
+ # Negative net_premium = Credit (Receiving)
20
+ is_debit = net_premium > 0
21
+ net_cost = abs(net_premium) * 100 # Multiplied by contract size
22
+
23
+ # 2. Identify Strategy Type and Calculate Risk
24
+ leg_count = len(legs)
25
+ expiries = set(leg['expiry'] for leg in legs)
26
+ is_multi_expiry = len(expiries) > 1
27
+
28
+ # Sort legs by strike for easier analysis
29
+ sorted_legs = sorted(legs, key=lambda x: x['strike'])
30
+
31
+ max_profit = 0
32
+ max_loss = 0
33
+
34
+ if is_multi_expiry:
35
+ # Complex calculation for Calendars/Diagonals
36
+ # For simplicity in this version, we provide an ESTIMATE based on premium paid
37
+ # Usually Max Loss = Net Debit Paid
38
+ if is_debit:
39
+ max_loss = net_cost
40
+ # Max profit is capped by the back-month value at front-month expiration
41
+ # This is hard to calculate without a model, so we flag it as an estimate
42
+ max_profit = "Estimated (Limited)"
43
+ else:
44
+ # Net Credit Calendar (Rare/Risky)
45
+ max_loss = "Unlimited"
46
+ max_profit = net_cost
47
+
48
+ elif leg_count == 1:
49
+ # Long/Short Call/Put
50
+ if legs[0]['action'].upper() == 'BUY':
51
+ max_loss = net_cost
52
+ max_profit = "Unlimited"
53
+ else:
54
+ max_profit = net_cost
55
+ max_loss = "Unlimited"
56
+
57
+ elif leg_count == 2:
58
+ # Spreads (Vertical)
59
+ s1, s2 = sorted_legs[0]['strike'], sorted_legs[1]['strike']
60
+ spread_width = (s2 - s1) * 100
61
+
62
+ if is_debit:
63
+ max_loss = net_cost
64
+ max_profit = spread_width - net_cost
65
+ else:
66
+ max_profit = net_cost
67
+ max_loss = spread_width - net_cost
68
+
69
+ elif leg_count == 4:
70
+ # Iron Condor / Iron Butterfly
71
+ # Max Profit = Net Credit
72
+ # Max Loss = Width of widest wing - Net Credit
73
+ if not is_debit:
74
+ put_spread_width = (sorted_legs[1]['strike'] - sorted_legs[0]['strike']) * 100
75
+ call_spread_width = (sorted_legs[3]['strike'] - sorted_legs[2]['strike']) * 100
76
+ widest_wing = max(put_spread_width, call_spread_width)
77
+ max_profit = net_cost
78
+ max_loss = widest_wing - net_cost
79
+ else:
80
+ # Reverse Iron Condor (Debit)
81
+ max_loss = net_cost
82
+ max_profit = max(sorted_legs[1]['strike'] - sorted_legs[0]['strike'], sorted_legs[3]['strike'] - sorted_legs[2]['strike']) * 100 - net_cost
83
+
84
+ elif leg_count == 3:
85
+ # Butterfly / Christmas Tree
86
+ # S1 (Buy 1), S2 (Sell 2), S3 (Buy 1)
87
+ if is_debit:
88
+ wing_width = (sorted_legs[1]['strike'] - sorted_legs[0]['strike']) * 100
89
+ max_loss = net_cost
90
+ max_profit = wing_width - net_cost
91
+
92
+ return {
93
+ "max_profit": max_profit,
94
+ "max_loss": max_loss,
95
+ "net_premium": round(net_premium, 2),
96
+ "is_debit": is_debit,
97
+ "leg_details": [f"{l['action']} {l['type']} {l['strike']} @ {l['price']} (Exp: {l['expiry']})" for l in legs]
98
+ }
src/market-analyst/backend/verify_e2e.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import asyncio
4
+ import json
5
+
6
+ try:
7
+ from dotenv import load_dotenv
8
+ load_dotenv()
9
+ except ImportError:
10
+ pass
11
+
12
+ # Add path for common and local modules
13
+ current_dir = os.path.dirname(os.path.abspath(__file__))
14
+ repo_root = os.path.abspath(os.path.join(current_dir, "../../../"))
15
+ if repo_root not in sys.path:
16
+ sys.path.append(repo_root)
17
+ if current_dir not in sys.path:
18
+ sys.path.append(current_dir)
19
+
20
+ from common.utility.autogen_model_factory import AutoGenModelFactory
21
+ from teams.team import get_analyst_team, get_decision_team
22
+
23
+ async def main():
24
+ ticker = "MSFT"
25
+ provider = "openai"
26
+ import time
27
+ start_time = time.time()
28
+
29
+ print(f"--- OPTIMIZED PERFORMANCE RUN FOR {ticker} ---", flush=True)
30
+
31
+ try:
32
+ model_client = AutoGenModelFactory.get_model(
33
+ provider=provider,
34
+ model_name="gpt-4o",
35
+ temperature=0,
36
+ model_info={"family": "gpt", "vision": False, "function_calling": True, "json_output": True, "structured_output": True}
37
+ )
38
+ except Exception as e:
39
+ print(f"Model error: {e}")
40
+ return
41
+
42
+ # PHASE 1
43
+ print("\n[PHASE 1: DATA COLLECTION]", flush=True)
44
+ analyst_team = get_analyst_team(model_client)
45
+ phase1_task = f"Perform complete analyst data collection for {ticker}."
46
+ analyst_context = []
47
+
48
+ try:
49
+ async for message in analyst_team.run_stream(task=phase1_task):
50
+ source = getattr(message, 'source', 'System')
51
+ content = getattr(message, 'content', '')
52
+ if not content or source == 'User': continue
53
+
54
+ # Print first 200 chars of each significant report
55
+ if len(str(content)) > 200:
56
+ print(f"[{source}] generated a report ({len(str(content))} chars).", flush=True)
57
+ analyst_context.append(f"[{source}]: {content}")
58
+ else:
59
+ # Likely a tool call result or short comment
60
+ pass
61
+ except Exception as e:
62
+ print(f"Phase 1 Error: {e}")
63
+
64
+ # PHASE 2
65
+ print("\n[PHASE 2: STRATEGY & RISK]", flush=True)
66
+ market_context_str = "\n\n".join(analyst_context)
67
+ decision_team = get_decision_team(model_client)
68
+ phase2_task = f"ANALYST CONTEXT:\n{market_context_str}\n\nGOAL: Design, critique, and finalize trade for {ticker}. Only the LeadOrchestrator can end the cycle."
69
+
70
+ try:
71
+ msg_count = 0
72
+ async for message in decision_team.run_stream(task=phase2_task):
73
+ source = getattr(message, 'source', 'System')
74
+ content = getattr(message, 'content', '')
75
+ if not content or (source == 'User' and "ANALYST CONTEXT" in content): continue
76
+
77
+ msg_count += 1
78
+ print(f"[{msg_count}] {source}: {str(content)[:150]}...", flush=True)
79
+
80
+ if "[[ANALYSIS_JUDGMENT_COMPLETE]]" in str(content):
81
+ print("\n--- STABLE TERMINATION DETECTED ---", flush=True)
82
+ break
83
+ except Exception as e:
84
+ print(f"Phase 2 Error: {e}")
85
+
86
+ print(f"\n--- PERFORMANCE SUMMARY ---", flush=True)
87
+ print(f"Total Time: {time.time() - start_time:.2f}s", flush=True)
88
+ print("--- VERIFICATION COMPLETE ---", flush=True)
89
+
90
+ if __name__ == "__main__":
91
+ asyncio.run(main())
src/market-analyst/frontend/src/App.vue CHANGED
@@ -16,6 +16,7 @@ import {
16
  TrendingUp,
17
  XCircle,
18
  Clock,
 
19
  Heart,
20
  Zap,
21
  LineChart,
@@ -38,7 +39,8 @@ const workflowAgents = [
38
  { id: 'SentimentAnalyst', name: 'Sentiment', role: 'Evaluates market sentiment from news and monitors earnings risks.' },
39
  { id: 'FundamentalAnalyst', name: 'Fundamental', role: 'Evaluates Valuation (P/E, PEG), EPS Growth, and Financial Health vs Market Risk.' },
40
  { id: 'StrategyAdvisor', name: 'Strategy', role: 'Formulates multi-leg option strategies with risk/reward calculation.' },
41
- { id: 'RiskManager', name: 'Risk', role: 'Critiques strategy in 2-round debate and issues final governance approval.' }
 
42
  ]
43
 
44
  const providers = [
@@ -55,6 +57,7 @@ const agentIcons = {
55
  'FundamentalAnalyst': Landmark,
56
  'StrategyAdvisor': BrainCircuit,
57
  'RiskManager': ShieldCheck,
 
58
  'System': LayoutDashboard,
59
  'User': Search,
60
  // Backward compatibility
@@ -69,6 +72,7 @@ const agentColors = {
69
  'FundamentalAnalyst': '#f59e0b', // Amber
70
  'StrategyAdvisor': '#8b5cf6', // Purple
71
  'RiskManager': '#ef4444', // Red
 
72
  'System': '#94a3b8', // Slate
73
  'User': '#60a5fa', // Light Blue
74
  'MarketAnalyst': '#3b82f6'
@@ -172,7 +176,11 @@ const analyzeTicker = (symbol) => {
172
 
173
  eventSource.onmessage = (event) => {
174
  if (event.data === '[DONE]') {
175
- activeAgent.value = null
 
 
 
 
176
  if (pendingResult.value) {
177
  results.value.push(pendingResult.value)
178
  saveToHistory(pendingResult.value) // Save to history
@@ -412,19 +420,26 @@ onMounted(() => {
412
  </header>
413
 
414
  <main class="dashboard-content">
415
- <div class="workflow-breadcrumb glass">
416
- <div
417
- v-for="agent in workflowAgents"
418
- :key="agent.id"
419
- :class="['breadcrumb-item', { active: activeAgent === agent.id }]"
420
- :data-tooltip="agent.role"
421
- >
422
- <div class="item-icon-wrapper" :style="{ color: getAgentColor(agent.id) }">
423
- <component :is="getAgentIcon(agent.id)" :size="14" />
424
- <div v-if="activeAgent === agent.id" class="bulb" :style="{ backgroundColor: getAgentColor(agent.id) }"></div>
 
 
 
 
 
 
 
 
 
425
  </div>
426
- <span class="item-label">{{ agent.name }}</span>
427
- <ChevronRight v-if="agent.id !== 'RiskManager'" :size="14" class="separator" />
428
  </div>
429
  </div>
430
 
@@ -505,11 +520,52 @@ onMounted(() => {
505
  </div>
506
 
507
  <div class="card-body">
508
- <h4>{{ res.actionable_recommendation }}</h4>
509
- <div v-if="res.entry_price && res.entry_price !== 'N/A'" class="entry-info">
510
- <Clock :size="14" />
511
- <span>Entry Signal: <strong>{{ res.entry_signal }}</strong> at <strong>${{ res.entry_price }}</strong></span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  </div>
 
513
  <p class="risk-info">
514
  <AlertTriangle v-if="res.risk_warning" :size="14" />
515
  {{ res.risk_warning || 'No specific risk warnings identified.' }}
@@ -550,6 +606,7 @@ onMounted(() => {
550
  <th>Decision</th>
551
  <th class="mobile-hide">Confidence</th>
552
  <th class="mobile-hide">Strategy</th>
 
553
  <th class="mobile-hide">Max Profit</th>
554
  <th class="mobile-hide" style="width: 50px"></th>
555
  </tr>
@@ -571,6 +628,7 @@ onMounted(() => {
571
  </div>
572
  </td>
573
  <td class="mobile-hide">{{ item.strategy_type }}</td>
 
574
  <td class="col-profit mobile-hide" :class="{ 'has-profit': item.max_profit > 0 }">
575
  {{ item.max_profit ? '$' + item.max_profit : '-' }}
576
  </td>
@@ -617,6 +675,10 @@ onMounted(() => {
617
  <label>Strategy</label>
618
  <span>{{ selectedReport.strategy_type }}</span>
619
  </div>
 
 
 
 
620
  <div class="metric-box">
621
  <label>Entry</label>
622
  <span>{{ selectedReport.entry_signal }} @ ${{ selectedReport.entry_price || 'N/A' }}</span>
@@ -1210,6 +1272,19 @@ onMounted(() => {
1210
  letter-spacing: 0.1em;
1211
  }
1212
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1213
  .pulse-border {
1214
  animation: pulse-border 2s infinite;
1215
  }
@@ -1268,6 +1343,115 @@ onMounted(() => {
1268
  white-space: nowrap;
1269
  }
1270
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1271
  @media (max-width: 1024px) {
1272
  .footer-content {
1273
  flex-direction: column;
@@ -1287,6 +1471,32 @@ onMounted(() => {
1287
  z-index: 50; /* Ensure tooltips appear above content below */
1288
  }
1289
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1290
  .breadcrumb-item {
1291
  display: flex;
1292
  align-items: center;
 
16
  TrendingUp,
17
  XCircle,
18
  Clock,
19
+ Calendar,
20
  Heart,
21
  Zap,
22
  LineChart,
 
39
  { id: 'SentimentAnalyst', name: 'Sentiment', role: 'Evaluates market sentiment from news and monitors earnings risks.' },
40
  { id: 'FundamentalAnalyst', name: 'Fundamental', role: 'Evaluates Valuation (P/E, PEG), EPS Growth, and Financial Health vs Market Risk.' },
41
  { id: 'StrategyAdvisor', name: 'Strategy', role: 'Formulates multi-leg option strategies with risk/reward calculation.' },
42
+ { id: 'RiskManager', name: 'Risk', role: 'Critiques strategy in 2-round debate and performs math verification.' },
43
+ { id: 'LeadOrchestrator', name: 'Orchestrator', role: 'Central Control. Performs gap analysis, cross-questions agents, and issues final approval.' }
44
  ]
45
 
46
  const providers = [
 
57
  'FundamentalAnalyst': Landmark,
58
  'StrategyAdvisor': BrainCircuit,
59
  'RiskManager': ShieldCheck,
60
+ 'LeadOrchestrator': Crown,
61
  'System': LayoutDashboard,
62
  'User': Search,
63
  // Backward compatibility
 
72
  'FundamentalAnalyst': '#f59e0b', // Amber
73
  'StrategyAdvisor': '#8b5cf6', // Purple
74
  'RiskManager': '#ef4444', // Red
75
+ 'LeadOrchestrator': '#facc15', // Gold
76
  'System': '#94a3b8', // Slate
77
  'User': '#60a5fa', // Light Blue
78
  'MarketAnalyst': '#3b82f6'
 
176
 
177
  eventSource.onmessage = (event) => {
178
  if (event.data === '[DONE]') {
179
+ // Delay clearing active agent so user can see the final Orchestrator state
180
+ setTimeout(() => {
181
+ activeAgent.value = null
182
+ }, 3000)
183
+
184
  if (pendingResult.value) {
185
  results.value.push(pendingResult.value)
186
  saveToHistory(pendingResult.value) // Save to history
 
420
  </header>
421
 
422
  <main class="dashboard-content">
423
+ <div class="breadcrumb-container glass">
424
+ <div class="breadcrumb-label">
425
+ <Sparkles :size="12" />
426
+ Active Intelligence Pipeline
427
+ <span class="legend-text">(Glowing icons indicate agents in action)</span>
428
+ </div>
429
+ <div class="workflow-breadcrumb">
430
+ <div
431
+ v-for="(agent, index) in workflowAgents"
432
+ :key="agent.id"
433
+ :class="['breadcrumb-item', { active: activeAgent === agent.id }]"
434
+ :data-tooltip="agent.role"
435
+ >
436
+ <div class="item-icon-wrapper" :style="{ color: getAgentColor(agent.id) }">
437
+ <component :is="getAgentIcon(agent.id)" :size="14" />
438
+ <div v-if="activeAgent === agent.id" class="bulb" :style="{ backgroundColor: getAgentColor(agent.id) }"></div>
439
+ </div>
440
+ <span class="item-label">{{ agent.name }}</span>
441
+ <ChevronRight v-if="index < workflowAgents.length - 1" :size="14" class="separator" />
442
  </div>
 
 
443
  </div>
444
  </div>
445
 
 
520
  </div>
521
 
522
  <div class="card-body">
523
+ <h4 class="strat-title">{{ res.strategy_type }}</h4>
524
+
525
+ <!-- Strategy Legs Table -->
526
+ <div v-if="res.legs && res.legs.length" class="legs-container glass-inset">
527
+ <div class="legs-header">Strategy Components</div>
528
+ <table class="legs-table">
529
+ <thead>
530
+ <tr>
531
+ <th>Leg</th>
532
+ <th>Type</th>
533
+ <th>Strike</th>
534
+ <th>Expiry</th>
535
+ <th>Price</th>
536
+ </tr>
537
+ </thead>
538
+ <tbody>
539
+ <tr v-for="(leg, idx) in res.legs" :key="idx">
540
+ <td>
541
+ <span class="action-badge" :class="leg.action.toLowerCase()">
542
+ {{ leg.action }}
543
+ </span>
544
+ </td>
545
+ <td>{{ leg.type }}</td>
546
+ <td><span class="strike-pill">${{ leg.strike }}</span></td>
547
+ <td class="col-expiry">{{ leg.expiry }}</td>
548
+ <td class="col-price">${{ leg.price }}</td>
549
+ </tr>
550
+ </tbody>
551
+ </table>
552
+ </div>
553
+
554
+ <div class="risk-reward-grid">
555
+ <div class="rr-item">
556
+ <span class="rr-label">Max Profit</span>
557
+ <span class="rr-val profit">${{ res.max_profit }}</span>
558
+ </div>
559
+ <div class="rr-item">
560
+ <span class="rr-label">Max Loss</span>
561
+ <span class="rr-val loss">${{ res.max_loss }}</span>
562
+ </div>
563
+ <div class="rr-item">
564
+ <span class="rr-label">Entry</span>
565
+ <span class="rr-val">${{ res.entry_price || res.estimated_entry_price }}</span>
566
+ </div>
567
  </div>
568
+
569
  <p class="risk-info">
570
  <AlertTriangle v-if="res.risk_warning" :size="14" />
571
  {{ res.risk_warning || 'No specific risk warnings identified.' }}
 
606
  <th>Decision</th>
607
  <th class="mobile-hide">Confidence</th>
608
  <th class="mobile-hide">Strategy</th>
609
+ <th class="mobile-hide">Expiry</th>
610
  <th class="mobile-hide">Max Profit</th>
611
  <th class="mobile-hide" style="width: 50px"></th>
612
  </tr>
 
628
  </div>
629
  </td>
630
  <td class="mobile-hide">{{ item.strategy_type }}</td>
631
+ <td class="mobile-hide">{{ item.expiry_date || 'N/A' }}</td>
632
  <td class="col-profit mobile-hide" :class="{ 'has-profit': item.max_profit > 0 }">
633
  {{ item.max_profit ? '$' + item.max_profit : '-' }}
634
  </td>
 
675
  <label>Strategy</label>
676
  <span>{{ selectedReport.strategy_type }}</span>
677
  </div>
678
+ <div class="metric-box">
679
+ <label>Expiry</label>
680
+ <span>{{ selectedReport.expiry_date || 'N/A' }}</span>
681
+ </div>
682
  <div class="metric-box">
683
  <label>Entry</label>
684
  <span>{{ selectedReport.entry_signal }} @ ${{ selectedReport.entry_price || 'N/A' }}</span>
 
1272
  letter-spacing: 0.1em;
1273
  }
1274
 
1275
+ .expiry-badge {
1276
+ padding: 0.25rem 0.5rem;
1277
+ background: rgba(255, 255, 255, 0.1);
1278
+ border-radius: 0.5rem;
1279
+ font-weight: 700;
1280
+ font-size: 0.75rem;
1281
+ color: var(--text-primary);
1282
+ display: flex;
1283
+ align-items: center;
1284
+ gap: 0.4rem;
1285
+ border: 1px solid rgba(255, 255, 255, 0.2);
1286
+ }
1287
+
1288
  .pulse-border {
1289
  animation: pulse-border 2s infinite;
1290
  }
 
1343
  white-space: nowrap;
1344
  }
1345
 
1346
+ /* Strategy Legs Table */
1347
+ .legs-container {
1348
+ margin: 1.25rem 0;
1349
+ padding: 1rem;
1350
+ border-radius: 0.75rem;
1351
+ background: rgba(255, 255, 255, 0.02);
1352
+ border: 1px solid rgba(255, 255, 255, 0.05);
1353
+ }
1354
+
1355
+ .legs-header {
1356
+ font-size: 0.7rem;
1357
+ font-weight: 800;
1358
+ text-transform: uppercase;
1359
+ color: var(--text-muted);
1360
+ margin-bottom: 0.75rem;
1361
+ letter-spacing: 0.05em;
1362
+ display: flex;
1363
+ justify-content: space-between;
1364
+ }
1365
+
1366
+ .legs-table {
1367
+ width: 100%;
1368
+ border-collapse: collapse;
1369
+ font-size: 0.85rem;
1370
+ }
1371
+
1372
+ .legs-table th {
1373
+ text-align: left;
1374
+ padding: 0.5rem;
1375
+ color: var(--text-secondary);
1376
+ font-weight: 600;
1377
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
1378
+ }
1379
+
1380
+ .legs-table td {
1381
+ padding: 0.6rem 0.5rem;
1382
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
1383
+ }
1384
+
1385
+ .action-badge {
1386
+ padding: 0.15rem 0.4rem;
1387
+ border-radius: 0.25rem;
1388
+ font-size: 0.7rem;
1389
+ font-weight: 800;
1390
+ }
1391
+
1392
+ .action-badge.buy { background: rgba(16, 185, 129, 0.15); color: var(--success); }
1393
+ .action-badge.sell { background: rgba(239, 68, 68, 0.15); color: var(--danger); }
1394
+
1395
+ .strike-pill {
1396
+ font-weight: 700;
1397
+ color: var(--text-primary);
1398
+ }
1399
+
1400
+ .col-expiry {
1401
+ font-size: 0.75rem;
1402
+ color: var(--text-secondary);
1403
+ }
1404
+
1405
+ .col-price {
1406
+ font-weight: 600;
1407
+ color: var(--accent-primary);
1408
+ }
1409
+
1410
+ .risk-reward-grid {
1411
+ display: grid;
1412
+ grid-template-columns: repeat(3, 1fr);
1413
+ gap: 0.75rem;
1414
+ margin: 1rem 0;
1415
+ }
1416
+
1417
+ .rr-item {
1418
+ padding: 0.75rem;
1419
+ background: rgba(255, 255, 255, 0.03);
1420
+ border-radius: 0.5rem;
1421
+ display: flex;
1422
+ flex-direction: column;
1423
+ gap: 0.25rem;
1424
+ border: 1px solid rgba(255, 255, 255, 0.05);
1425
+ }
1426
+
1427
+ .rr-label {
1428
+ font-size: 0.65rem;
1429
+ font-weight: 700;
1430
+ color: var(--text-muted);
1431
+ text-transform: uppercase;
1432
+ }
1433
+
1434
+ .rr-val {
1435
+ font-size: 1.1rem;
1436
+ font-weight: 800;
1437
+ }
1438
+
1439
+ .rr-val.profit { color: var(--success); }
1440
+ .rr-val.loss { color: var(--danger); }
1441
+
1442
+ .strat-title {
1443
+ margin-bottom: 0.5rem;
1444
+ color: var(--accent-primary);
1445
+ font-weight: 800;
1446
+ }
1447
+
1448
+ .strat-reasoning {
1449
+ font-size: 0.95rem;
1450
+ line-height: 1.5;
1451
+ color: var(--text-secondary);
1452
+ margin-bottom: 1rem;
1453
+ }
1454
+
1455
  @media (max-width: 1024px) {
1456
  .footer-content {
1457
  flex-direction: column;
 
1471
  z-index: 50; /* Ensure tooltips appear above content below */
1472
  }
1473
 
1474
+ .breadcrumb-label {
1475
+ text-align: center;
1476
+ font-size: 0.65rem;
1477
+ font-weight: 800;
1478
+ text-transform: uppercase;
1479
+ color: var(--text-muted);
1480
+ letter-spacing: 0.1em;
1481
+ margin-top: 0.5rem;
1482
+ display: flex;
1483
+ align-items: center;
1484
+ justify-content: center;
1485
+ gap: 0.4rem;
1486
+ opacity: 0.7;
1487
+ }
1488
+
1489
+ .legend-text {
1490
+ font-size: 0.6rem;
1491
+ color: var(--accent-primary);
1492
+ text-transform: none;
1493
+ letter-spacing: normal;
1494
+ margin-left: 0.5rem;
1495
+ font-weight: 500;
1496
+ font-style: italic;
1497
+ opacity: 0.9;
1498
+ }
1499
+
1500
  .breadcrumb-item {
1501
  display: flex;
1502
  align-items: center;