diff --git a/.chainlit/config.toml b/.chainlit/config.toml new file mode 100644 index 00000000..b863073d --- /dev/null +++ b/.chainlit/config.toml @@ -0,0 +1,16 @@ +[project] +enable_telemetry = false + +[UI] +name = "TradingAgents" +description = "Multi-Agent LLM Trading Analysis" +default_collapse_content = true +hide_cot = false + +[UI.theme.light] +primary = "#1a73e8" +background = "#ffffff" + +[UI.theme.dark] +primary = "#00f0ff" +background = "#09090b" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..9e3afde6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.13-slim + +WORKDIR /app + +# System deps for building Python packages +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc g++ && \ + rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# Chainlit listens on $PORT (Railway sets this automatically) +CMD chainlit run app.py --host 0.0.0.0 --port ${PORT:-8000} diff --git a/app.py b/app.py new file mode 100644 index 00000000..38c5a56f --- /dev/null +++ b/app.py @@ -0,0 +1,183 @@ +"""Chainlit web UI for TradingAgents — deployed on Railway.""" + +import os +import re +from datetime import date + +import chainlit as cl + +from tradingagents.graph.trading_graph import TradingAgentsGraph +from tradingagents.default_config import DEFAULT_CONFIG +from cli.stats_handler import StatsCallbackHandler + + +def parse_ticker_date(text: str): + """Extract ticker symbol and optional date from user message. + + Examples: + "NVDA" -> ("NVDA", today) + "Analyze AAPL 2024-12-01" -> ("AAPL", "2024-12-01") + "What about TSLA?" -> ("TSLA", today) + """ + # Try to find a date (YYYY-MM-DD) + date_match = re.search(r"(\d{4}-\d{2}-\d{2})", text) + trade_date = date_match.group(1) if date_match else str(date.today()) + + # Find uppercase 1-5 letter words as candidate tickers + candidates = re.findall(r"\b([A-Z]{1,5})\b", text) + # Filter out common English words + skip = {"I", "A", "THE", "AND", "OR", "FOR", "TO", "IN", "ON", "AT", "IS", + "IT", "OF", "BY", "AS", "AN", "BE", "IF", "SO", "DO", "MY", "UP", + "NO", "NOT", "ALL", "BUT", "HOW", "GET", "HAS", "HAD", "CAN", + "WHAT", "ABOUT", "BUY", "SELL", "HOLD"} + tickers = [c for c in candidates if c not in skip] + + ticker = tickers[0] if tickers else None + return ticker, trade_date + + +def build_config(): + """Build TradingAgents config for Anthropic/Claude.""" + config = DEFAULT_CONFIG.copy() + config["llm_provider"] = "anthropic" + config["deep_think_llm"] = os.getenv("DEEP_THINK_MODEL", "claude-sonnet-4-5-20241022") + config["quick_think_llm"] = os.getenv("QUICK_THINK_MODEL", "claude-haiku-4-5-20251001") + config["backend_url"] = None + config["max_debate_rounds"] = 1 + config["max_risk_discuss_rounds"] = 1 + config["data_vendors"] = { + "core_stock_apis": "yfinance", + "technical_indicators": "yfinance", + "fundamental_data": "yfinance", + "news_data": "yfinance", + } + return config + + +# Report field -> display name +REPORT_NAMES = { + "market_report": "Market Analyst", + "sentiment_report": "Sentiment Analyst", + "news_report": "News Analyst", + "fundamentals_report": "Fundamentals Analyst", +} + + +@cl.on_chat_start +async def on_chat_start(): + await cl.Message( + content=( + "**TradingAgents** — Multi-Agent LLM Trading Analysis\n\n" + "Send a ticker symbol to analyze. Examples:\n" + "- `NVDA`\n" + "- `Analyze AAPL 2024-12-01`\n" + "- `What's the outlook for TSLA?`\n\n" + "I'll run a team of AI analysts, researchers, traders, and risk managers " + "to produce a trading decision." + ) + ).send() + + +@cl.on_message +async def on_message(message: cl.Message): + ticker, trade_date = parse_ticker_date(message.content) + + if not ticker: + await cl.Message( + content="I couldn't find a ticker symbol. Try something like `NVDA` or `Analyze AAPL 2024-12-01`." + ).send() + return + + # Status message + status_msg = cl.Message(content=f"Analyzing **{ticker}** for **{trade_date}**...") + await status_msg.send() + + # Build graph + config = build_config() + stats = StatsCallbackHandler() + + try: + graph = TradingAgentsGraph( + selected_analysts=["market", "social", "news", "fundamentals"], + debug=False, + config=config, + callbacks=[stats], + ) + except Exception as e: + await cl.Message(content=f"Failed to initialize agents: {e}").send() + return + + # Create initial state and stream + init_state = graph.propagator.create_initial_state(ticker, trade_date) + args = graph.propagator.get_graph_args(callbacks=[stats]) + + # Track which reports/phases we've already shown + seen_reports = set() + seen_debate = False + seen_risk = False + seen_trader = False + final_state = None + + try: + async for chunk in graph.graph.astream(init_state, **args): + final_state = chunk + + # --- Analyst reports --- + for field, name in REPORT_NAMES.items(): + if field not in seen_reports and chunk.get(field): + seen_reports.add(field) + report = chunk[field] + # Show as a collapsible Step + async with cl.Step(name=f"{name} Report", type="tool") as step: + step.output = report[:3000] if len(report) > 3000 else report + + # --- Investment debate (Bull vs Bear) --- + debate = chunk.get("investment_debate_state") + if debate and not seen_debate and debate.get("judge_decision"): + seen_debate = True + async with cl.Step(name="Research Debate", type="tool") as step: + step.output = ( + f"**Judge Decision:**\n{debate['judge_decision']}" + ) + + # --- Trader plan --- + if not seen_trader and chunk.get("trader_investment_plan"): + seen_trader = True + async with cl.Step(name="Trader Plan", type="tool") as step: + step.output = chunk["trader_investment_plan"][:3000] + + # --- Risk debate --- + risk = chunk.get("risk_debate_state") + if risk and not seen_risk and risk.get("judge_decision"): + seen_risk = True + async with cl.Step(name="Risk Assessment", type="tool") as step: + step.output = f"**Risk Decision:**\n{risk['judge_decision']}" + + except Exception as e: + await cl.Message(content=f"Error during analysis: {e}").send() + return + + if not final_state: + await cl.Message(content="Analysis produced no results.").send() + return + + # Process final decision + decision_text = final_state.get("final_trade_decision", "No decision reached.") + signal = graph.process_signal(decision_text) + + # Stats summary + s = stats.get_stats() + stats_line = ( + f"*{s['llm_calls']} LLM calls · {s['tool_calls']} tool calls · " + f"{s['tokens_in']:,} tokens in · {s['tokens_out']:,} tokens out*" + ) + + await cl.Message( + content=( + f"## {ticker} — Trading Decision\n\n" + f"**Signal: {signal}**\n\n" + f"---\n\n" + f"{decision_text}\n\n" + f"---\n{stats_line}" + ) + ).send() diff --git a/railway.toml b/railway.toml new file mode 100644 index 00000000..edbee221 --- /dev/null +++ b/railway.toml @@ -0,0 +1,8 @@ +[build] +builder = "DOCKERFILE" +dockerfilePath = "Dockerfile" + +[deploy] +healthcheckPath = "/" +restartPolicyType = "ON_FAILURE" +restartPolicyMaxRetries = 3