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:
David Tseng 2026-03-23 13:09:53 +08:00
parent 4a11b4bf55
commit 90f1d1e120
3 changed files with 29 additions and 315 deletions

View File

@ -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"]

View File

@ -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"

View File

@ -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.