┌─────────────────────────────────────────────────────────────────┐
│ Browser (Next.js :3000) │
│ ├── User Portal (Terminal, Chat, Newsletter) │
│ └── Manager Portal (EventFeed, ContentCards, Reprompt) │
└──────────────┬──────────────────────────────┬───────────────────┘
│ REST (/api/*) │ WebSocket (/ws/*)
┌──────────────▼──────────────────────────────▼───────────────────┐
│ FastAPI Gateway (:8000) │
│ ├── Routers: analysis, trades, content, chat, users │
│ ├── Services: langgraph_runner, supabase_client, llm │
│ └── WebSocket: ConnectionManager broadcast │
└──────────────┬──────────────────────────────────────────────────┘
│ triggers
┌──────────────▼──────────────────────────────────────────────────┐
│ LangGraph Agent (in-process) │
│ ├── Node A: Anomaly Detection │
│ ├── Node B: News Correlation ── Gemini for reasoning │
│ ├── Node C: Behavioral Coaching ── Gemini for reasoning │
│ └── Node D: Content Generator ── Gemini for generation │
└──────────────┬──────────────────────────────────────────────────┘
│ fastmcp.Client (HTTP)
┌──────────────▼──────────────────────────────────────────────────┐
│ FastMCP Tool Server (:9000) │
│ ├── get_price_data → yfinance │
│ ├── search_market_moving_news → NewsData.io │
│ ├── fetch_article_deep_dive → NewsData.io │
│ ├── query_user_patterns → Supabase │
│ ├── verify_correlation → deterministic scoring │
│ ├── get_news_sentiment → Alpha Vantage │
│ ├── get_alpha_stock_quote → Alpha Vantage │
│ ├── get_finnhub_quote → Finnhub │
│ ├── get_crypto_candles → Finnhub │
│ └── search_mediastack_news → MediaStack │
└─────────────────────────────────────────────────────────────────┘
│
┌────▼────┐
│ Supabase │ (users, trade_history,
│ │ behavioral_metrics,
└──────────┘ content_drafts)
Key design decision: LangGraph nodes call FastMCP tools directly via fastmcp.Client (not LLM tool-calling). Gemini is used only for reasoning/generation within nodes. This gives a deterministic pipeline with AI-powered analysis at each step.
Directory: backend/
Entry point: backend/app/main.py
Runs on: port 8000
| File | Endpoint | Method | Purpose |
|---|---|---|---|
routers/analysis.py |
/api/analyze/trigger |
POST | Trigger LangGraph pipeline in background task. Accepts ticker, user_id, persona. Returns task_id. |
routers/analysis.py |
/api/analyze/result/{task_id} |
GET | Poll for completed analysis result by task ID. |
routers/trades.py |
/api/trades |
POST | Execute buy/sell trade. Writes to Supabase, updates balance. |
routers/trades.py |
/api/trades |
GET | Fetch trade history for a user (last 50). |
routers/content.py |
/api/content/drafts |
GET | List content drafts with optional ?status= filter. |
routers/content.py |
/api/content/drafts/{id} |
PATCH | Approve, decline, or reprompt a draft. Reprompt calls Gemini to regenerate. |
routers/chat.py |
/api/chat |
POST | Stateless coaching chat. Fetches user context via MCP, sends to Gemini. |
routers/users.py |
/api/users/{id}/newsletter |
GET | Personalized event-driven newsletter. Aggregates content drafts into events. |
| File | Function | Purpose |
|---|---|---|
services/llm.py |
get_llm_client() |
Returns AsyncOpenAI pointed at Gemini's OpenAI-compatible endpoint. |
services/llm.py |
MODEL |
"gemini-2.0-flash" — used by all LLM calls in backend. |
services/langgraph_runner.py |
run_analysis() |
Invokes the compiled LangGraph pipeline, broadcasts WebSocket events at each step, saves drafts to Supabase. |
services/langgraph_runner.py |
create_event_callback() |
Creates an async callback that tags events with task_id and broadcasts via ConnectionManager. |
services/supabase_client.py |
get_supabase() |
Singleton Supabase client factory. |
| File | Endpoint/Class | Purpose |
|---|---|---|
ws/manager.py |
ConnectionManager |
Manages active WebSocket connections. Methods: connect(), disconnect(), broadcast(). |
ws/manager.py |
WS /ws/events |
Accepts client connections, keeps alive, broadcasts pipeline events. |
WebSocket event shape:
{
"task_id": "uuid",
"event_type": "node_complete | coaching_warning | content_ready | analysis_started | analysis_complete | error",
"node": "anomaly_detection | news_correlation | behavioral_coaching | content_generator",
"data": { ... }
}| File | Function | Purpose |
|---|---|---|
scripts/seed_data.py |
seed() |
Creates demo user, behavioral metrics, 17 trade history records (mix of revenge_trade, panic_sell, fomo_buy, disciplined), and 3 content drafts (Buffett/Trump/Burry for NVDA). |
| File | Class/Function | Purpose |
|---|---|---|
app/config.py |
Settings |
Pydantic settings loaded from env: gemini_api_key, supabase_url, supabase_key, newsdata_api_key, mcp_server_url, backend_port, mem0_api_key. |
app/config.py |
get_settings() |
LRU-cached settings singleton. |
| File | Function | Purpose |
|---|---|---|
services/memory.py |
get_memory_client() |
Singleton Mem0 Platform client. |
services/memory.py |
add_memory() |
Store conversation exchanges as persistent memory for a user. |
services/memory.py |
search_memory() |
Semantic search across user memories. |
services/memory.py |
get_all_memories() |
Retrieve all stored memories for a user. |
services/memory.py |
delete_memory() |
Delete a specific memory by ID. |
services/memory.py |
format_memories_for_prompt() |
Format memories into LLM-ready prompt text. |
The Mem0 persistent memory layer adds cross-session context to two key surfaces:
-
Chat endpoint (
/api/chat): Before each response, searches Mem0 for memories relevant to the user's message. After responding, stores the exchange as a new memory. This allows the coaching bot to reference past conversations ("Last time you asked about FOMO, I suggested..."). -
Behavioral coaching node (LangGraph Node C): Before generating coaching advice, searches for past coaching history. After generating, stores the advice. This means the pipeline remembers "We warned this user about revenge trading 3 times this week" and can escalate accordingly.
Memory API endpoints:
| Endpoint | Method | Purpose |
|---|---|---|
/api/memory/{user_id} |
GET | List all memories for a user |
/api/memory/search |
POST | Semantic search across memories |
/api/memory/{memory_id} |
DELETE | Delete a specific memory |
/api/memory/user/{user_id} |
DELETE | Delete all memories for a user |
Directory: fastmcp_server/
Entry point: fastmcp_server/server.py
Runs on: port 9000 (streamable-HTTP transport)
| File | Tool Function | Input | Output | Data Source |
|---|---|---|---|---|
tools/price_data.py |
get_price_data(ticker, timeframe) |
ticker (str), timeframe (str, default "5d") | {ticker, timeframe, prices[], anomaly{zscore, mean, std, is_anomaly}, latest_price, change_pct} |
yfinance + Z-score computation |
tools/news_search.py |
search_market_moving_news(ticker, timeframe) |
ticker (str), timeframe (str, default "7d") | {ticker, timeframe, article_count, articles[{article_id, title, source, published_at, sentiment, ai_summary, category}]} |
NewsData.io API |
tools/news_deep_dive.py |
fetch_article_deep_dive(article_id) |
article_id (str) | {article_id, title, source, published_at, full_content, key_entities[], key_figures{}} |
NewsData.io API |
tools/user_patterns.py |
query_user_patterns(user_id) |
user_id (str) | {user_id, user_name, balance, behavioral_metrics{}, recent_trades[], pattern_summary{win_rate, avg_loss, avg_win, most_common_emotional_tag}} |
Supabase |
tools/correlation.py |
verify_correlation(price_event, news_event) |
price_event (dict), news_event (dict) | {correlation_score, strength, temporal_score, relevance_score, magnitude_score, justification} |
Deterministic 3-layer scoring |
tools/alpha_vantage.py |
get_news_sentiment(tickers, topics?, time_from?, sort?, limit?) |
tickers (str), topics (str, optional), time_from (str, optional), sort (str, default "LATEST"), limit (int, default 10) | {ticker, sentiment_score_avg, sentiment_label, article_count, articles[{title, source, time_published, sentiment_score, sentiment_label, summary, url}]} |
Alpha Vantage NEWS_SENTIMENT API |
tools/alpha_vantage.py |
get_alpha_stock_quote(ticker) |
ticker (str) | {ticker, price, change, change_pct, volume, high, low, previous_close, latest_trading_day} |
Alpha Vantage GLOBAL_QUOTE API |
tools/finnhub_data.py |
get_finnhub_quote(ticker) |
ticker (str) | {ticker, current_price, change, change_pct, high, low, open, previous_close, timestamp} |
Finnhub Quote API |
tools/finnhub_data.py |
get_crypto_candles(symbol, resolution?, days_back?) |
symbol (str, e.g. "BTC"), resolution (str, default "D"), days_back (int, default 5) | {symbol, resolution, candle_count, candles[{timestamp, open, high, low, close, volume}], latest_close} |
Finnhub Crypto Candle API |
tools/mediastack_news.py |
search_mediastack_news(keywords, categories?, countries?, languages?, sort?, limit?) |
keywords (str), categories (str, optional), countries (str, default "us"), languages (str, default "en"), sort (str, default "published_desc"), limit (int, default 10) | {keywords, article_count, articles[{title, source, author, description, url, category, published_at, country, language}]} |
MediaStack News API |
Every tool has a mock fallback that activates when the live API is unavailable (rate-limited, missing key, network error). Mock data is realistic and demo-ready.
| File | Function | Purpose |
|---|---|---|
utils/zscore.py |
compute_zscore(prices, window=20) |
Rolling Z-score over a price window. Returns {zscore, mean, std, is_anomaly}. Anomaly threshold: abs(zscore) > 2.0. |
utils/supabase_client.py |
get_supabase() |
Singleton Supabase client factory. |
verify_correlation uses a weighted 3-layer check:
| Layer | Weight | What it measures | Score range |
|---|---|---|---|
| Temporal proximity | 30% | Time between price event and news publish. ≤2h = 1.0, ≤24h = 0.3–1.0, ≤72h = 0.2, else 0. | 0–1 |
| Relevance | 40% | Keyword overlap between ticker/company names and article text. | 0–1 |
| Magnitude vs Sentiment | 30% | Alignment of sentiment direction (positive/negative) with price direction (up/down), scaled by magnitude. | 0–1 |
Final score ≥ 0.6 → Strong, ≥ 0.4 → Moderate, < 0.4 → Weak.
Directory: langgraph_agent/
Invoked by: backend/app/services/langgraph_runner.py
Not a standalone server — runs in-process within FastAPI.
START
│
▼
┌─────────────────────┐
│ Node A: Anomaly │ Calls: get_price_data
│ Detection │ Sets: price_data, is_anomaly, should_continue
└─────────┬───────────┘
│
┌─────▼──────┐
│ anomaly? │
└──┬─────┬───┘
yes │ │ no
▼ ▼
┌──────────┐ END
│ Node B: │
│ News │ Calls: search_market_moving_news, verify_correlation
│ Corr. │ Uses Gemini for justification text
└────┬─────┘
▼
┌──────────┐
│ Node C: │ Calls: query_user_patterns
│ Coaching │ Uses Gemini for personalized advice
└────┬─────┘
▼
┌──────────┐
│ Node D: │ Calls: fetch_article_deep_dive
│ Content │ Uses Gemini with persona prompts
│ Gen. │ Generates X + LinkedIn posts per persona
└────┬─────┘
▼
END
AnalysisState is a TypedDict carrying data across all nodes:
| Field | Type | Set by |
|---|---|---|
ticker |
str | Input |
user_id |
str | Input |
persona |
str | Input ("buffett", "trump", "burry", or "all") |
price_data |
dict | Node A |
is_anomaly |
bool | Node A |
news_articles |
list[dict] | Node B |
correlation_result |
dict | Node B |
user_patterns |
dict | Node C |
coaching_advice |
str | Node C |
content_drafts |
list[dict] | Node D |
should_continue |
bool | Node A (controls routing) |
error |
str | None | Any node on failure |
task_id |
str | Input |
event_callback |
callable | Input (WebSocket broadcaster) |
| File | Function | MCP Tools Called | Gemini Usage |
|---|---|---|---|
nodes/anomaly_detection.py |
anomaly_detection(state) |
get_price_data |
None |
nodes/news_correlation.py |
news_correlation(state) |
search_market_moving_news, verify_correlation |
Generates 2-3 sentence justification |
nodes/behavioral_coaching.py |
behavioral_coaching(state) |
query_user_patterns |
Generates risk assessment, coaching advice, behavioral warning |
nodes/content_generator.py |
content_generator(state) |
fetch_article_deep_dive |
Generates X post (≤280 chars) + LinkedIn post (2-3 paragraphs) per persona |
| Persona | Style | Tone |
|---|---|---|
| Buffett | Folksy, value-driven wisdom. Metaphors about farming, fishing, baseball. | Calm, patient. References intrinsic value, margin of safety, compound returns. |
| Trump | Hyperbolic, bullish, ALL CAPS keywords, exclamation marks. | Extremely confident, short punchy sentences, superlatives. |
| Burry | Doom-laden, cryptic, hyper-technical. | Bearish, contrarian. References historical crashes, P/E ratios, debt levels. |
| File | Constant | Purpose |
|---|---|---|
prompts/correlation.py |
CORRELATION_JUSTIFICATION_PROMPT |
Template for explaining why a news event caused a price move. |
prompts/coaching.py |
COACHING_PROMPT |
Template for generating behavioral coaching advice. Includes risk assessment, specific advice, and behavioral warning. |
prompts/personas.py |
CONTENT_GENERATION_PROMPT |
Template for generating X + LinkedIn posts. Outputs JSON {x_post, linkedin_post}. |
| Function | Purpose |
|---|---|
call_mcp_tool(tool_name, arguments) |
Opens fastmcp.Client connection to http://localhost:9000/mcp, calls the named tool, parses JSON from the text content block. |
| Export | Value | Purpose |
|---|---|---|
get_llm_client() |
AsyncOpenAI(base_url=Gemini endpoint) |
Returns client for Gemini 2.0 Flash via OpenAI compatibility layer. |
MODEL |
"gemini-2.0-flash" |
Model identifier passed to all chat.completions.create() calls. |
Directory: frontend/
Entry point: frontend/src/app/layout.tsx
Runs on: port 3000
Proxies: /api/* → :8000, /ws/* → :8000 (via next.config.ts rewrites)
| Route | File | Description |
|---|---|---|
/ |
app/page.tsx |
Landing page with links to User and Manager portals. |
/portal/user |
app/portal/user/page.tsx |
Trading terminal: ticker selector, price chart, trade form, trade history. |
/portal/user/chat |
app/portal/user/chat/page.tsx |
AI coaching chat with behavioral context. |
/portal/user/newsletter |
app/portal/user/newsletter/page.tsx |
Event-driven newsletter with correlation badges. Real-time refresh via WebSocket. |
/portal/manager |
app/portal/manager/page.tsx |
Content management: run analysis, event feed, content cards with approve/decline/reprompt. |
| File | Component | Purpose |
|---|---|---|
app/layout.tsx |
RootLayout |
HTML shell, dark mode, global CSS. |
app/portal/layout.tsx |
PortalLayout |
Top nav bar with portal toggle (User/Manager), sub-navigation, user avatar. |
| File | Component | Props | Purpose |
|---|---|---|---|
components/trading/TerminalPanel.tsx |
TerminalPanel |
onTradeExecuted? callback |
Buy/sell form with ticker selector, action toggle, amount input, result display. |
components/trading/PriceChart.tsx |
PriceChart |
data: PricePoint[], ticker: string |
Recharts AreaChart with gradient fill. Color-coded green/red. Displays price and change %. |
components/trading/TradeHistory.tsx |
TradeHistory |
trades: Trade[] |
Table with ticker, action badge, amount, PnL, emotional tag badge. Color-coded by tag. |
| File | Component | Props | Purpose |
|---|---|---|---|
components/chat/ChatWindow.tsx |
ChatWindow |
none | Full chat interface: message list, input, loading dots. Calls sendChatMessage API. Auto-scrolls. |
components/chat/CoachMessage.tsx |
CoachMessage |
message: ChatMessage |
Single message bubble. User = right/blue, Coach = left/dark with "AI Coach" label. |
| File | Component | Props | Purpose |
|---|---|---|---|
components/newsletter/NewsletterCard.tsx |
NewsletterCard |
event object |
Card with ticker badge, change %, correlation strength badge (Strong/Moderate/Weak with color), headline, summary, Z-score. |
| File | Component | Props | Purpose |
|---|---|---|---|
components/manager/EventFeed.tsx |
EventFeed |
events: WSEvent[] |
Scrollable event list. Each event has type badge (color-coded), node name, and contextual data (ticker, anomaly, correlation, coaching snippet, draft count). |
components/manager/ContentCard.tsx |
ContentCard |
draft, onUpdated? |
Draft card with platform icon (X/LinkedIn), persona badge, content, correlation score. Actions: Approve (green), Decline (red), Edit (blue). Expandable RepromptBox. |
components/manager/PersonaSelector.tsx |
PersonaSelector |
value, onChange |
Button group: Buffett (emerald), Trump (orange), Burry (red), All (blue). |
components/manager/RepromptBox.tsx |
RepromptBox |
onSubmit, loading |
Textarea + Regenerate button for custom content instructions. |
| File | Hook | Returns | Purpose |
|---|---|---|---|
hooks/useWebSocket.ts |
useWebSocket(url?) |
{events, connected, clearEvents} |
Connects to ws://localhost:8000/ws/events. Accumulates parsed WSEvent objects. Auto-reconnects after 3s on disconnect. |
hooks/useAnalysis.ts |
useAnalysis() |
{runAnalysis, loading, taskId, error} |
Wraps triggerAnalysis API call with loading/error state management. |
| Function | HTTP Call | Purpose |
|---|---|---|
triggerAnalysis(ticker, userId, persona) |
POST /api/analyze/trigger |
Start analysis pipeline |
executeTrade(data) |
POST /api/trades |
Execute buy/sell trade |
getTradeHistory(userId) |
GET /api/trades |
Fetch trade history |
getContentDrafts(status?) |
GET /api/content/drafts |
List content drafts |
updateContentDraft(id, data) |
PATCH /api/content/drafts/{id} |
Approve, decline, or reprompt |
sendChatMessage(userId, message) |
POST /api/chat |
Send message to AI coach |
getUserNewsletter(userId) |
GET /api/users/{id}/newsletter |
Fetch newsletter events |
| Interface | Key fields |
|---|---|
User |
id, name, email, balance |
Trade |
id, ticker, action, amount, entry_price, pnl, emotional_tag, news_context |
PricePoint |
date, open, high, low, close, volume |
AnomalyResult |
zscore, mean, std, is_anomaly |
PriceData |
ticker, prices[], anomaly, latest_price, change_pct |
NewsArticle |
article_id, title, source, sentiment, ai_summary |
CorrelationResult |
correlation_score, strength, temporal/relevance/magnitude scores, justification |
ContentDraft |
id, ticker, platform, persona, content, status, analysis_context |
BehavioralMetrics |
revenge_trade_probability, panic_sell_threshold, avg_loss_after_news_event, drawdown_threshold |
WSEvent |
task_id, event_type, node, data |
ChatMessage |
role, content, timestamp |
Schema defined in docs/schema.sql.
| Table | Purpose | Key Columns |
|---|---|---|
users |
Trader profiles | id (UUID), name, email, balance |
trade_history |
All executed trades | user_id (FK), ticker, action, amount, entry_price, exit_price, pnl, emotional_tag, news_context |
behavioral_metrics |
Per-user behavioral profile | user_id (FK, unique), revenge_trade_probability, panic_sell_threshold, avg_loss_after_news_event, drawdown_threshold |
content_drafts |
Generated social media posts | ticker, platform (x/linkedin), persona (buffett/trump/burry), content, status (pending/approved/declined), correlation_score, analysis_context (JSONB) |
| Service | Used by | Purpose | Env var |
|---|---|---|---|
| Google Gemini | LangGraph nodes, Backend chat/content routers | LLM reasoning and generation (via OpenAI-compatible endpoint) | GEMINI_API_KEY |
| Supabase | FastMCP user_patterns tool, Backend routers | Database for users, trades, metrics, drafts | SUPABASE_URL, SUPABASE_KEY |
| yfinance | FastMCP price_data tool | Historical stock price data + OHLC | None (free, no key) |
| NewsData.io | FastMCP news_search + news_deep_dive tools | Market news articles with sentiment | NEWSDATA_API_KEY |
| Alpha Vantage | FastMCP get_news_sentiment + get_alpha_stock_quote tools | Market news sentiment analysis and real-time stock quotes | ALPHAADVANTAGE_API_KEY |
| Finnhub | FastMCP get_finnhub_quote + get_crypto_candles tools | Real-time stock quotes and crypto OHLCV candlestick data | FINHNHUB_API_KEY |
| MediaStack | FastMCP search_mediastack_news tool | Global news search with keyword, category, and geographic filtering | MEDIASTACK_API_KEY |