feat: add report saving and display for Polymarket analysis
Save analysis results to disk with organized subfolders: 1_analysts/, 2_research/, 3_trading/, 4_risk/, 5_decision/ plus a consolidated complete_report.md. Prompt user to save and display full report after analysis completes, matching the existing stock analysis flow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4a11b4bf55
commit
90f1d1e120
139
cli/main.py
139
cli/main.py
|
|
@ -24,10 +24,8 @@ from rich.align import Align
|
|||
from rich.rule import Rule
|
||||
|
||||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||
from tradingagents.prediction_market import PMTradingAgentsGraph
|
||||
from tradingagents.default_config import DEFAULT_CONFIG
|
||||
from tradingagents.prediction_market.pm_config import PM_DEFAULT_CONFIG
|
||||
from cli.models import AnalysisMode, AnalystType, PMAnalystType
|
||||
from cli.models import AnalystType
|
||||
from cli.utils import *
|
||||
from cli.announcements import fetch_announcements, display_announcements
|
||||
from cli.stats_handler import StatsCallbackHandler
|
||||
|
|
@ -500,95 +498,62 @@ def get_user_selections():
|
|||
box_content += f"\n[dim]Default: {default}[/dim]"
|
||||
return Panel(box_content, border_style="blue", padding=(1, 2))
|
||||
|
||||
# Step 1: Analysis mode (Stock or Polymarket)
|
||||
# Step 1: Ticker symbol
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 1: Ticker Symbol or Polymarket Market ID",
|
||||
"Choose between stock analysis or prediction market analysis",
|
||||
"Step 1: Ticker Symbol", "Enter the ticker symbol to analyze", "SPY"
|
||||
)
|
||||
)
|
||||
selected_mode = select_analysis_mode()
|
||||
selected_ticker = get_ticker()
|
||||
|
||||
# Step 2: Ticker / Market ID based on mode
|
||||
selected_ticker = None
|
||||
market_id = None
|
||||
market_question = ""
|
||||
|
||||
if selected_mode == AnalysisMode.STOCK:
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 2: Ticker Symbol", "Enter the ticker symbol to analyze", "SPY"
|
||||
)
|
||||
)
|
||||
selected_ticker = get_ticker()
|
||||
else:
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 2: Polymarket Market",
|
||||
"Paste a Polymarket URL or enter a numeric market ID",
|
||||
)
|
||||
)
|
||||
market_id, market_question = get_market_id()
|
||||
|
||||
# Step 3: Analysis date
|
||||
# Step 2: Analysis date
|
||||
default_date = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 3: Analysis Date",
|
||||
"Step 2: Analysis Date",
|
||||
"Enter the analysis date (YYYY-MM-DD)",
|
||||
default_date,
|
||||
)
|
||||
)
|
||||
analysis_date = get_analysis_date()
|
||||
|
||||
# Step 4: Select analysts
|
||||
if selected_mode == AnalysisMode.STOCK:
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 4: Analysts Team", "Select your LLM analyst agents for the analysis"
|
||||
)
|
||||
)
|
||||
selected_analysts = select_analysts()
|
||||
console.print(
|
||||
f"[green]Selected analysts:[/green] {', '.join(analyst.value for analyst in selected_analysts)}"
|
||||
)
|
||||
else:
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 4: PM Analysts Team", "Select your prediction market analyst agents"
|
||||
)
|
||||
)
|
||||
selected_analysts = select_pm_analysts()
|
||||
console.print(
|
||||
f"[green]Selected PM analysts:[/green] {', '.join(analyst.value for analyst in selected_analysts)}"
|
||||
)
|
||||
|
||||
# Step 5: Research depth
|
||||
# Step 3: Select analysts
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 5: Research Depth", "Select your research depth level"
|
||||
"Step 3: Analysts Team", "Select your LLM analyst agents for the analysis"
|
||||
)
|
||||
)
|
||||
selected_analysts = select_analysts()
|
||||
console.print(
|
||||
f"[green]Selected analysts:[/green] {', '.join(analyst.value for analyst in selected_analysts)}"
|
||||
)
|
||||
|
||||
# Step 4: Research depth
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 4: Research Depth", "Select your research depth level"
|
||||
)
|
||||
)
|
||||
selected_research_depth = select_research_depth()
|
||||
|
||||
# Step 6: LLM provider
|
||||
# Step 5: OpenAI backend
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 6: LLM Provider", "Select which service to talk to"
|
||||
"Step 5: OpenAI backend", "Select which service to talk to"
|
||||
)
|
||||
)
|
||||
selected_llm_provider, backend_url = select_llm_provider()
|
||||
|
||||
# Step 7: Thinking agents
|
||||
|
||||
# Step 6: Thinking agents
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 7: Thinking Agents", "Select your thinking agents for analysis"
|
||||
"Step 6: Thinking Agents", "Select your thinking agents for analysis"
|
||||
)
|
||||
)
|
||||
selected_shallow_thinker = select_shallow_thinking_agent(selected_llm_provider)
|
||||
selected_deep_thinker = select_deep_thinking_agent(selected_llm_provider)
|
||||
|
||||
# Step 8: Provider-specific thinking configuration
|
||||
# Step 7: Provider-specific thinking configuration
|
||||
thinking_level = None
|
||||
reasoning_effort = None
|
||||
|
||||
|
|
@ -596,7 +561,7 @@ def get_user_selections():
|
|||
if provider_lower == "google":
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 8: Thinking Mode",
|
||||
"Step 7: Thinking Mode",
|
||||
"Configure Gemini thinking mode"
|
||||
)
|
||||
)
|
||||
|
|
@ -604,17 +569,14 @@ def get_user_selections():
|
|||
elif provider_lower == "openai":
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 8: Reasoning Effort",
|
||||
"Step 7: Reasoning Effort",
|
||||
"Configure OpenAI reasoning effort level"
|
||||
)
|
||||
)
|
||||
reasoning_effort = ask_openai_reasoning_effort()
|
||||
|
||||
return {
|
||||
"mode": selected_mode,
|
||||
"ticker": selected_ticker,
|
||||
"market_id": market_id,
|
||||
"market_question": market_question,
|
||||
"analysis_date": analysis_date,
|
||||
"analysts": selected_analysts,
|
||||
"research_depth": selected_research_depth,
|
||||
|
|
@ -938,53 +900,6 @@ def run_analysis():
|
|||
# First get all user selections
|
||||
selections = get_user_selections()
|
||||
|
||||
# Branch to Polymarket flow if selected
|
||||
if selections["mode"] == AnalysisMode.POLYMARKET:
|
||||
config = PM_DEFAULT_CONFIG.copy()
|
||||
config["max_debate_rounds"] = selections["research_depth"]
|
||||
config["max_risk_discuss_rounds"] = selections["research_depth"]
|
||||
config["quick_think_llm"] = selections["shallow_thinker"]
|
||||
config["deep_think_llm"] = selections["deep_thinker"]
|
||||
config["backend_url"] = selections["backend_url"]
|
||||
config["llm_provider"] = selections["llm_provider"].lower()
|
||||
config["google_thinking_level"] = selections.get("google_thinking_level")
|
||||
config["openai_reasoning_effort"] = selections.get("openai_reasoning_effort")
|
||||
|
||||
pm_analyst_order = ["event", "odds", "information", "sentiment"]
|
||||
selected_set = {analyst.value for analyst in selections["analysts"]}
|
||||
selected_analyst_keys = [a for a in pm_analyst_order if a in selected_set]
|
||||
|
||||
pm_graph = PMTradingAgentsGraph(
|
||||
selected_analyst_keys,
|
||||
config=config,
|
||||
debug=True,
|
||||
)
|
||||
|
||||
market_id = selections["market_id"]
|
||||
market_question = selections.get("market_question", "")
|
||||
analysis_date = selections["analysis_date"]
|
||||
|
||||
console.print(f"\n[bold cyan]Running Polymarket analysis...[/bold cyan]")
|
||||
console.print(f" Market ID: [green]{market_id}[/green]")
|
||||
if market_question:
|
||||
console.print(f" Question: [green]{market_question}[/green]")
|
||||
console.print(f" Date: [green]{analysis_date}[/green]")
|
||||
console.print(f" Analysts: [green]{', '.join(selected_analyst_keys)}[/green]")
|
||||
console.print()
|
||||
|
||||
_, decision = pm_graph.propagate(market_id, analysis_date, market_question)
|
||||
|
||||
console.print("\n[bold cyan]Analysis Complete![/bold cyan]\n")
|
||||
console.print(Panel(
|
||||
Markdown(f"```json\n{decision}\n```"),
|
||||
title="[bold]Final Decision[/bold]",
|
||||
border_style="green",
|
||||
padding=(1, 2),
|
||||
))
|
||||
return
|
||||
|
||||
# --- Stock analysis flow (original) ---
|
||||
|
||||
# Create config with selected research depth
|
||||
config = DEFAULT_CONFIG.copy()
|
||||
config["max_debate_rounds"] = selections["research_depth"]
|
||||
|
|
|
|||
|
|
@ -3,20 +3,8 @@ from typing import List, Optional, Dict
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class AnalysisMode(str, Enum):
|
||||
STOCK = "stock"
|
||||
POLYMARKET = "polymarket"
|
||||
|
||||
|
||||
class AnalystType(str, Enum):
|
||||
MARKET = "market"
|
||||
SOCIAL = "social"
|
||||
NEWS = "news"
|
||||
FUNDAMENTALS = "fundamentals"
|
||||
|
||||
|
||||
class PMAnalystType(str, Enum):
|
||||
EVENT = "event"
|
||||
ODDS = "odds"
|
||||
INFORMATION = "information"
|
||||
SENTIMENT = "sentiment"
|
||||
|
|
|
|||
193
cli/utils.py
193
cli/utils.py
|
|
@ -3,12 +3,10 @@ from typing import List, Optional, Tuple, Dict
|
|||
|
||||
from rich.console import Console
|
||||
|
||||
from cli.models import AnalysisMode, AnalystType, PMAnalystType
|
||||
from cli.models import AnalystType
|
||||
|
||||
console = Console()
|
||||
|
||||
TICKER_INPUT_EXAMPLES = "Examples: SPY, CNC.TO, 7203.T, 0700.HK"
|
||||
|
||||
ANALYST_ORDER = [
|
||||
("Market Analyst", AnalystType.MARKET),
|
||||
("Social Media Analyst", AnalystType.SOCIAL),
|
||||
|
|
@ -16,173 +14,11 @@ ANALYST_ORDER = [
|
|||
("Fundamentals Analyst", AnalystType.FUNDAMENTALS),
|
||||
]
|
||||
|
||||
PM_ANALYST_ORDER = [
|
||||
("Event Analyst", PMAnalystType.EVENT),
|
||||
("Odds Analyst", PMAnalystType.ODDS),
|
||||
("Information Analyst", PMAnalystType.INFORMATION),
|
||||
("Sentiment Analyst", PMAnalystType.SENTIMENT),
|
||||
]
|
||||
|
||||
|
||||
def select_analysis_mode() -> AnalysisMode:
|
||||
"""Select between Stock and Polymarket analysis."""
|
||||
choice = questionary.select(
|
||||
"Select Analysis Mode:",
|
||||
choices=[
|
||||
questionary.Choice("Stock Ticker (e.g. NVDA, TSLA)", value=AnalysisMode.STOCK),
|
||||
questionary.Choice("Polymarket Market ID (prediction market)", value=AnalysisMode.POLYMARKET),
|
||||
],
|
||||
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
|
||||
style=questionary.Style(
|
||||
[
|
||||
("selected", "fg:cyan noinherit"),
|
||||
("highlighted", "fg:cyan noinherit"),
|
||||
("pointer", "fg:cyan noinherit"),
|
||||
]
|
||||
),
|
||||
).ask()
|
||||
|
||||
if choice is None:
|
||||
console.print("\n[red]No mode selected. Exiting...[/red]")
|
||||
exit(1)
|
||||
|
||||
return choice
|
||||
|
||||
|
||||
def _resolve_polymarket_url(url: str) -> tuple[str, str]:
|
||||
"""Resolve a Polymarket URL to a (market_id, market_question) tuple.
|
||||
|
||||
Supports formats:
|
||||
- https://polymarket.com/event/<event-slug>/<market-slug>
|
||||
- https://polymarket.com/event/<market-slug>
|
||||
"""
|
||||
from urllib.parse import urlparse
|
||||
import requests
|
||||
|
||||
parsed = urlparse(url)
|
||||
parts = [p for p in parsed.path.split("/") if p]
|
||||
|
||||
if len(parts) < 2 or parts[0] != "event":
|
||||
return "", ""
|
||||
|
||||
# Last segment is the market slug (or event slug if only 2 parts)
|
||||
market_slug = parts[-1]
|
||||
|
||||
# Try as market slug first
|
||||
try:
|
||||
resp = requests.get(
|
||||
"https://gamma-api.polymarket.com/markets",
|
||||
params={"slug": market_slug},
|
||||
timeout=15,
|
||||
)
|
||||
data = resp.json()
|
||||
if isinstance(data, list) and data:
|
||||
return str(data[0]["id"]), data[0].get("question", "")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# If 3+ parts, the second segment is the event slug — resolve event and pick first market
|
||||
if len(parts) >= 2:
|
||||
event_slug = parts[1]
|
||||
try:
|
||||
resp = requests.get(
|
||||
"https://gamma-api.polymarket.com/events",
|
||||
params={"slug": event_slug},
|
||||
timeout=15,
|
||||
)
|
||||
data = resp.json()
|
||||
if isinstance(data, list) and data:
|
||||
markets = data[0].get("markets", [])
|
||||
if markets:
|
||||
return str(markets[0]["id"]), markets[0].get("question", "")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return "", ""
|
||||
|
||||
|
||||
def get_market_id() -> tuple[str, str]:
|
||||
"""Prompt the user to enter a Polymarket URL or market ID."""
|
||||
user_input = questionary.text(
|
||||
"Paste a Polymarket URL or enter a numeric market ID:",
|
||||
validate=lambda x: len(x.strip()) > 0 or "Please enter a URL or market ID.",
|
||||
style=questionary.Style(
|
||||
[
|
||||
("text", "fg:green"),
|
||||
("highlighted", "noinherit"),
|
||||
]
|
||||
),
|
||||
).ask()
|
||||
|
||||
if not user_input:
|
||||
console.print("\n[red]No input provided. Exiting...[/red]")
|
||||
exit(1)
|
||||
|
||||
user_input = user_input.strip()
|
||||
|
||||
# Check if it's a URL
|
||||
if "polymarket.com" in user_input:
|
||||
console.print("[dim]Resolving Polymarket URL...[/dim]")
|
||||
market_id, market_question = _resolve_polymarket_url(user_input)
|
||||
if market_id:
|
||||
console.print(f"[green]Found:[/green] {market_question} (ID: {market_id})")
|
||||
return market_id, market_question
|
||||
else:
|
||||
console.print("[red]Could not resolve URL. Please enter a numeric market ID instead.[/red]")
|
||||
exit(1)
|
||||
|
||||
# Otherwise treat as numeric market ID
|
||||
market_id = user_input
|
||||
|
||||
# Try to fetch the question from the API
|
||||
market_question = ""
|
||||
try:
|
||||
import requests
|
||||
resp = requests.get(
|
||||
f"https://gamma-api.polymarket.com/markets/{market_id}",
|
||||
timeout=15,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
market_question = data.get("question", "")
|
||||
if market_question:
|
||||
console.print(f"[green]Found:[/green] {market_question}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return market_id, market_question
|
||||
|
||||
|
||||
def select_pm_analysts() -> List[PMAnalystType]:
|
||||
"""Select prediction market analysts using an interactive checkbox."""
|
||||
choices = questionary.checkbox(
|
||||
"Select Your [PM Analysts Team]:",
|
||||
choices=[
|
||||
questionary.Choice(display, value=value) for display, value in PM_ANALYST_ORDER
|
||||
],
|
||||
instruction="\n- Press Space to select/unselect analysts\n- Press 'a' to select/unselect all\n- Press Enter when done",
|
||||
validate=lambda x: len(x) > 0 or "You must select at least one analyst.",
|
||||
style=questionary.Style(
|
||||
[
|
||||
("checkbox-selected", "fg:green"),
|
||||
("selected", "fg:green noinherit"),
|
||||
("highlighted", "noinherit"),
|
||||
("pointer", "noinherit"),
|
||||
]
|
||||
),
|
||||
).ask()
|
||||
|
||||
if not choices:
|
||||
console.print("\n[red]No analysts selected. Exiting...[/red]")
|
||||
exit(1)
|
||||
|
||||
return choices
|
||||
|
||||
|
||||
def get_ticker() -> str:
|
||||
"""Prompt the user to enter a ticker symbol."""
|
||||
ticker = questionary.text(
|
||||
f"Enter the exact ticker symbol to analyze ({TICKER_INPUT_EXAMPLES}):",
|
||||
"Enter the ticker symbol to analyze:",
|
||||
validate=lambda x: len(x.strip()) > 0 or "Please enter a valid ticker symbol.",
|
||||
style=questionary.Style(
|
||||
[
|
||||
|
|
@ -196,11 +32,6 @@ def get_ticker() -> str:
|
|||
console.print("\n[red]No ticker symbol provided. Exiting...[/red]")
|
||||
exit(1)
|
||||
|
||||
return normalize_ticker_symbol(ticker)
|
||||
|
||||
|
||||
def normalize_ticker_symbol(ticker: str) -> str:
|
||||
"""Normalize ticker input while preserving exchange suffixes."""
|
||||
return ticker.strip().upper()
|
||||
|
||||
|
||||
|
|
@ -480,26 +311,6 @@ def ask_openai_reasoning_effort() -> str:
|
|||
).ask()
|
||||
|
||||
|
||||
def ask_anthropic_effort() -> str | None:
|
||||
"""Ask for Anthropic effort level.
|
||||
|
||||
Controls token usage and response thoroughness on Claude 4.5+ and 4.6 models.
|
||||
"""
|
||||
return questionary.select(
|
||||
"Select Effort Level:",
|
||||
choices=[
|
||||
questionary.Choice("High (recommended)", "high"),
|
||||
questionary.Choice("Medium (balanced)", "medium"),
|
||||
questionary.Choice("Low (faster, cheaper)", "low"),
|
||||
],
|
||||
style=questionary.Style([
|
||||
("selected", "fg:cyan noinherit"),
|
||||
("highlighted", "fg:cyan noinherit"),
|
||||
("pointer", "fg:cyan noinherit"),
|
||||
]),
|
||||
).ask()
|
||||
|
||||
|
||||
def ask_gemini_thinking_config() -> str | None:
|
||||
"""Ask for Gemini thinking configuration.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue