Upload folder using huggingface_hub
Browse files- common/utility/autogen_model_factory.py +3 -1
- src/market-analyst/backend/aagents/fundamental_analyst.py +16 -13
- src/market-analyst/backend/aagents/market_analyst.py +6 -8
- src/market-analyst/backend/aagents/orchestrator.py +33 -0
- src/market-analyst/backend/aagents/risk_manager.py +40 -70
- src/market-analyst/backend/aagents/sentiment_analyst.py +18 -29
- src/market-analyst/backend/aagents/strategy_advisor.py +48 -98
- src/market-analyst/backend/aagents/volatility_analyst.py +19 -30
- src/market-analyst/backend/consistency_test.py +121 -0
- src/market-analyst/backend/main.py +46 -123
- src/market-analyst/backend/model_test.py +27 -0
- src/market-analyst/backend/teams/team.py +59 -10
- src/market-analyst/backend/test_gemini.py +35 -0
- src/market-analyst/backend/tools/market_data.py +79 -21
- src/market-analyst/backend/tools/pl_calculator.py +98 -0
- src/market-analyst/backend/verify_e2e.py +91 -0
- src/market-analyst/frontend/src/App.vue +228 -18
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": "
|
| 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:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
- Debt/Equity Ratio: <0.5 (Safe), >1.0 (Risky).
|
| 33 |
- Profit Margin: >20% (Excellent), <10% (Weak).
|
| 34 |
|
| 35 |
-
STEP
|
| 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
|
| 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
|
| 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
|
| 50 |
-
|
| 51 |
-
-
|
| 52 |
-
-
|
| 53 |
-
-
|
| 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:
|
| 41 |
-
-
|
| 42 |
-
-
|
| 43 |
|
| 44 |
STEP 8: Output Structured Summary
|
| 45 |
-
-
|
| 46 |
-
-
|
| 47 |
-
-
|
| 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.
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
OUTPUT FORMAT (ROUND 2 ONLY):
|
| 36 |
-
You MUST include a "score_card" object in your JSON.
|
| 37 |
-
```json
|
| 38 |
{
|
| 39 |
-
"
|
| 40 |
-
"
|
| 41 |
-
"
|
| 42 |
"confidence": 85,
|
| 43 |
-
"
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 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
|
| 23 |
-
-
|
| 24 |
-
-
|
| 25 |
-
-
|
| 26 |
-
|
| 27 |
-
STEP 3: Determine Overall Sentiment
|
| 28 |
-
-
|
| 29 |
-
- If
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
-
|
| 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 |
-
|
| 48 |
-
-
|
| 49 |
-
-
|
| 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
|
|
|
|
| 3 |
|
| 4 |
def get_strategy_advisor(model_client):
|
|
|
|
|
|
|
| 5 |
|
| 6 |
return AssistantAgent(
|
| 7 |
name="StrategyAdvisor",
|
| 8 |
model_client=model_client,
|
| 9 |
-
tools=[
|
| 10 |
system_message="""
|
| 11 |
-
You are an Expert Option Strategist.
|
| 12 |
|
| 13 |
-
MANDATORY WORKFLOW (
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
|
| 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 |
-
"
|
| 67 |
-
"
|
| 68 |
-
"
|
| 69 |
-
"
|
| 70 |
-
"
|
| 71 |
-
"
|
| 72 |
-
"
|
| 73 |
-
"
|
| 74 |
-
"
|
| 75 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
}
|
| 77 |
-
```
|
| 78 |
-
(Note: max_profit/max_loss are calculated for 100 shares/1 contract).
|
| 79 |
|
| 80 |
-
|
| 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
|
|
|
|
| 20 |
|
| 21 |
-
DO NOT proceed without calling
|
| 22 |
|
| 23 |
-
STEP
|
| 24 |
-
- IV > HV: Options are
|
| 25 |
-
- IV < HV: Options are cheap
|
| 26 |
-
- IV ≈ HV: Fair value, strategy depends on other factors
|
| 27 |
|
| 28 |
-
STEP
|
| 29 |
-
-
|
| 30 |
-
-
|
| 31 |
-
- VIX 20-30: Elevated fear, caution advised
|
| 32 |
-
- VIX > 30: High fear, extreme volatility
|
| 33 |
|
| 34 |
-
STEP
|
| 35 |
-
- If
|
| 36 |
-
- If ELEVATED_VOL: Sell credit spreads.
|
| 37 |
-
- If HIGH_RISK_VOL: Sell Iron Condors or WAIT.
|
| 38 |
|
| 39 |
-
STEP
|
| 40 |
-
-
|
| 41 |
-
-
|
| 42 |
-
-
|
| 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
|
| 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
|
| 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
|
| 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=
|
| 121 |
-
model_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 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 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
|
| 143 |
-
|
| 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 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
if
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 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 |
-
|
| 187 |
-
|
| 188 |
-
|
| 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 |
-
#
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
| 43 |
"""
|
| 44 |
-
|
|
|
|
| 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 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 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
|
| 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
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 416 |
-
<div
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
>
|
| 422 |
-
<div
|
| 423 |
-
|
| 424 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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;
|