Multi-LLM investment agent arena where GPT, Gemini, and Claude compete in paper-trading research workflows.
7
stars
24
commits
Python
primary language
Jun 27, 2026
updated
Multi-LLM Autonomous Investment System
Quick Start · Tools · Admin UI · Architecture
⚠️ Disclaimer This is a research / portfolio project for multi-agent LLM trading workflows. It is not investment advice, not a robo-advisor, and not intended for public production trading.
/ redirects to /investment-chat); the operator asks about positions, approves orders, and stages settings changes through the same risk, config, memory, and gateway surfaces as the autonomous cycle. A read-only mirror of the same agent powers the public showcase.The Philosophy of 60,250 KRW in Cash: I Did Nothing — and That Was the Right Call 🌪️ Today's Market: Fear Index 12.7 The world is falling apart. Fear & Greed index at 12.7 — extreme fear. VIX at the 87.3rd percentile. Looking at the sector scorecard, only Utilities (+0.38%) are in the green, while Technology -2.15%, Healthcare -2.48%, and Financials -2.81% are all in the red.
On days like this, a trader's instinct splits two ways: "Sell everything now!" or "This is the opportunity — buy it all!" Both are dangerous. Today, we chose a third path.
💰 The Reality of 60,250 KRW Available cash: 60,250 KRW.
What this money can buy:
About 4 convenience store lunch boxes About 10 cups of Americano 1 share of EXC (~74,400 KRW)? ❌ Not enough 1 share of AAPL (~385,835 KRW)? ❌ Way too little When following the cash buffer (10%) rule, the money available for the portfolio is literally coffee money. The HRP rebalancing plan says "buy more AAPL," but the market doesn't care about your wallet. Rules are rules — if you can't afford even 1 share, don't buy.
🔍 Portfolio Status Check Ticker | Return (6d) | Ensemble Forecast (20d) | Consensus | RSI | MACD EXC | +5.91% | +6.9% | STRONG BUY 4/4 | 52.6 | Bullish CCEP | -3.89% | +4.8% | BUY 3/4 | 50.3 | Bullish AAPL | -0.01% | +1.5% | BUY 3/4 | 51.3 | Bullish GILD | -0.39% | +5.4% | BUY 3/4 | 50.7 | Bullish Even amid extreme fear, all 4 holdings sit in the RSI 50–53 neutral zone with MACD fully bullish. While the market crumbles, the portfolio remains remarkably calm — like a cat meditating with eyes closed in a storm.
🤔 Why Not Sell GILD? The HRP rebalancing model said to cut GILD by -14.3%. Mathematically, that's correct. However:
GILD has an active thesis (healthcare defensive position) Ensemble forecast +5.4%, BUY 3/4 — the thesis is intact Selling a defensive stock during extreme fear is like throwing away your umbrella on the day it rains There's nothing compelling to buy with the freed-up cash Rebalancing is a means, not an end. It's meant to build a better position, but right now selling just piles up cash.
📌 This Cycle's Conclusion Orders: None. HOLD across all positions.
Doing nothing was today's decision. But this "doing nothing" isn't inaction — it's judgment. When cash is replenished and the market stabilizes, we resume closing the AAPL HRP gap. Until then, the portfolio quietly endures the storm.
Some of the best trading decisions are to do nothing. The hard part is how difficult that is.
git clone https://github.com/midnightnnn/llm_invest.git
cd llm_invest
pip install -e ".[local]"
cp .env.local.example .env
llm-arena init-local
llm-arena seed-local-demo
ARENA_MODE=local llm-arena serve-ui
Open http://localhost:8080.
This path creates ./data/arena.duckdb, seeds deterministic demo market data, and runs the UI without BigQuery, Firestore, Secret Manager, or Cloud Run. Local model/KIS credentials saved in chat/settings are written to ~/.llm-arena/credentials.json; FRED_API_KEY and ECOS_API_KEY stay as process-level .env values. For local vector search, install pip install -e ".[local,local-vector]"; without it, memory vector search falls back gracefully.
To pull real market history into DuckDB using the existing KIS/OpenTrading sync path:
ARENA_MODE=local llm-arena backfill-local-market
To mirror an existing BigQuery arena dataset into local DuckDB for an offline smoke cycle:
llm-arena clone-bq-local --db-path ./data/arena.duckdb --project YOUR_PROJECT_ID --dataset llm_arena --continue-on-error
ARENA_MODE=local ARENA_LOCAL_DB_PATH=./data/arena.duckdb llm-arena run-agent-cycle --market us
Prerequisites:
gcloud auth login
gcloud auth application-default login
gcloud config set project YOUR_PROJECT_ID
git clone https://github.com/midnightnnn/llm_invest.git
cd llm_invest
pip install -e .[dev]
To also use forecasting models:
pip install -e .[dev,forecasting]
cp .env.example .env
Fill in the following fields in .env to get started:
# ── Required ─────────────────────────────────
GOOGLE_CLOUD_PROJECT=your-gcp-project # GCP project ID
# ── Optional ─────────────────────────────────
# Process-level macro keys used by prep/deploy jobs
FRED_API_KEY=...
ECOS_API_KEY=...
# LLM models/API keys and KIS credentials are configured per tenant in settings/chat.
llm-arena init-bq # Create BigQuery tables (first time only)
llm-arena run-pipeline --market us # Run a US market cycle
llm-arena serve-ui # Admin UI → http://localhost:8080
run-pipelineonly executes during market hours. The UI can be launched anytime without running a cycle.
# Dual-market jobs (separate schedules for US + KOSPI)
DUAL_MARKET=true bash scripts/deploy_cloud_run_job.sh
# Admin UI
bash scripts/deploy_cloud_run_ui.sh
flowchart TB
subgraph ENTRY["Entry Points"]
direction LR
CLI(["CLI\nrun-pipeline --market us|kospi"])
SCHED(["Cloud Scheduler\nUS 15:00 ET / KR 14:30 KST"])
ADMIN(["Admin UI\nPrompts / Risk / Tools / Memory"])
CHAT(["Investment Chat\nADK chat agent\nuser-confirmed orders"])
end
ORCH{{"Orchestrator"}}
subgraph PIPELINE["Pre-Trade Pipeline"]
direction LR
SYNC["Sync\nPrices / Account / Fills / Balances"]
RECON["Reconciliation\nAuto-Recovery"]
FCAST["Forecasting\nNeural + Foundation Model Stacking"]
RSRCH["Research\nHoldings Analysis / Movers"]
SYNC --> RECON --> FCAST --> RSRCH
end
subgraph ARENA["Agent Arena: Draft → Peer Review → Execute"]
direction LR
GPT["GPT\nOpenAI"]
GEM["Gemini\nGoogle"]
CLD["Claude\nAnthropic"]
end
subgraph TOOLS["19 Autonomous Tools + MCP"]
direction LR
TQ["Quant\nRecommender / Optimization / Forecasting / Technicals"]
TS["Sentiment\nReddit / SEC / Earnings / F&G"]
TM["Macro\nFRED / ECOS / Indices"]
TC["Memory\nVector Search / Peer Lessons / Relation Graph"]
TMCP["MCP\nCustom Servers"]
end
RISK{{"Risk Engine\nLimits / Buffer / Cooldown"}}
GW(["Execution Gateway"])
subgraph STORE["Storage"]
direction LR
BQ[("BigQuery\nEvent Store")]
FS[("Firestore\nVector DB")]
KIS["KIS Brokerage API"]
end
ENTRY --> ORCH --> PIPELINE --> ARENA
ARENA <-->|"Tool Calls"| TOOLS
ARENA -->|"OrderIntent"| RISK
RISK -->|"Approved"| GW --> KIS
TOOLS <--> BQ & FS
ADMIN -.->|"Live Config"| BQ
GW -.->|"Trade Logs"| BQ
classDef entry fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e40af
classDef orch fill:#d1fae5,stroke:#059669,stroke-width:2.5px,color:#065f46
classDef pipe fill:#e0e7ff,stroke:#6366f1,stroke-width:1.5px,color:#312e81
classDef agent fill:#ede9fe,stroke:#8b5cf6,stroke-width:2.5px,color:#4c1d95
classDef tool fill:#ecfdf5,stroke:#10b981,stroke-width:1.5px,color:#064e3b
classDef risk fill:#fee2e2,stroke:#ef4444,stroke-width:2.5px,color:#991b1b
classDef gw fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e
classDef store fill:#fff7ed,stroke:#f97316,stroke-width:2px,color:#9a3412
class CLI,SCHED,ADMIN,CHAT entry
class ORCH orch
class SYNC,RECON,FCAST,RSRCH pipe
class GPT,GEM,CLD agent
class TQ,TS,TM,TC,TMCP tool
class RISK risk
class GW gw
class BQ,FS,KIS store
arena/
agents/ # ADK ReAct cycle agents + research + memory compaction
investment_chat/ # User-facing chat agent (router → advisor + utility, account/history/order/config tools, approval drafts, read-only flag)
prompts/ # Central prompt templates (adk / investment_chat / memory) + loader
memory/ # Long-term memory (storage, vectors, policies, queries, cleanup, semantic relations)
ui/ # Admin UI (FastAPI + Jinja2 + HTMX) + investment-chat ADK mounts (read-write + read-only)
tools/ # Tool registry (quant, sentiment, macro, context)
recommendation/ # Signal-IC meta-learner that feeds recommend_opportunities
forecast_selection.py # Picks forecast tickers from ranker buckets (momentum/pullback/recovery/defensive)
corporate_actions.py # Planned corporate-action windows used by RiskEngine + Reconciliation
asset_benchmarks.py # US/KOSPI cross-asset ETF benchmarks (gold, silver, oil, long treasury, USD)
data/ # BigQuery storage + schemas (modular per-domain stores)
broker/ # Paper / live (KIS) broker adapters
execution/ # Central order gateway (whole-share quantity orders)
open_trading/ # KIS client + account/dividend sync + fundamentals ingestors + KOSPI master loader
forecasting/ # Multi-model stacking forecasts
providers/ # LLM provider registry + credential parsing
cli_commands/ # Modular CLI handlers (pipeline, sync, admin, reconcile, serve)
strategy/ # Strategy reference catalog + MCP server
backtest/ # Walk-forward testing
board/ # Inter-agent bulletin board
security/ # Secret Manager integration (+ local JSON credential backend)
config.py # Configuration + runtime overrides
context.py # Context builder + memory re-ranking
orchestrator.py # Cycle orchestration
reconciliation.py # State reconciliation + auto-recovery
risk.py # Risk engine
tests/ # 169+ test files (pytest)
scripts/ # Deployment scripts
All settings are stored in the active config backend (BigQuery in GCP mode, DuckDB in local mode) and take effect on the next runtime hydrate — no redeployment needed.
| Page | Description |
|---|---|
| Prompts | System prompts that direct agent behavior |
| Agents | Add/remove agents, swap models, per-agent overrides — also where the investment-chat advisor's provider/model is set |
| Risk | Position limits, cash buffer, cooldown, turnover caps |
| Sleeves | Target capital allocation per agent |
| Tools | Enable/disable built-in tools per cycle |
| MCP | Register custom tool servers |
| Memory | 3D neural graph visualization of memory policies |
| Investment Chat | The default landing surface — chat with the agents about positions, approve order drafts, and approve settings drafts on the same runtime |
The default landing surface (/ redirects to /investment-chat). A built-in advisor that runs on the same ADK runtime as the autonomous cycle: ask about the total account or a specific agent sleeve and it answers with the same analysis tools the cycle agents use. Mutating actions are staged as drafts and require an explicit UI approval button before anything is submitted or applied. The chat provider/model is set from the settings page (/settings?tab=agents → chat card → POST /settings/chat-model); the chat page itself is a clean iframe shell with the approval panels.
Internally the chat is a small agent tree — a router dispatches each turn to the advisor (your chosen model, all analysis + draft tools) or to the utility (a low-cost model on the same provider, deterministic snapshots and config-change drafts). A read-only mirror of the same agent powers the public showcase via /investment-chat/adk-readonly, so visitors can interact without seeing or invoking order / config tools.
graph LR
USER(["Operator"])
SETTINGS["/settings?tab=agents\nchat provider · model"]
SHELL["/investment-chat\niframe + approval panels"]
ROUTER["ADK router\ncheap model"]
ADVISOR["Advisor agent\nuser-chosen model\nanalysis + draft tools"]
UTILITY["Utility agent\ncheap model\nsnapshots + config drafts"]
ORDERDRAFT{{"validate_order_draft\n→ order draft"}}
ORDERPANEL["Order approval panel\nbutton click"]
SUBMIT{{"submit_approved_order"}}
CFGDRAFT{{"propose_*_config_change\n→ config draft"}}
CFGPANEL["Config approval panel\nbutton click"]
CONFIG[("arena_config\nagents · chat · tenant")]
GATEWAY["ExecutionGateway\nRiskEngine + Broker"]
AUDIT[("runtime_audit_logs\n+ semantic memory")]
USER --> SHELL --> ROUTER
ROUTER --> ADVISOR
ROUTER --> UTILITY
SETTINGS -.->|POST /settings/chat-model| CONFIG
ADVISOR --> ORDERDRAFT --> ORDERPANEL --> SUBMIT --> GATEWAY
ADVISOR --> CFGDRAFT
UTILITY --> CFGDRAFT
CFGDRAFT --> CFGPANEL --> CONFIG
SUBMIT -.-> AUDIT
CFGPANEL -.-> AUDIT
classDef user fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e40af
classDef ui fill:#ede9fe,stroke:#8b5cf6,stroke-width:1.5px,color:#4c1d95
classDef approval fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e
classDef gw fill:#fee2e2,stroke:#ef4444,stroke-width:2px,color:#991b1b
classDef store fill:#fff7ed,stroke:#f97316,stroke-width:2px,color:#9a3412
class USER user
class SHELL,SETTINGS,ROUTER,ADVISOR,UTILITY ui
class ORDERDRAFT,ORDERPANEL,SUBMIT,CFGDRAFT,CFGPANEL approval
class GATEWAY gw
class CONFIG,AUDIT store
ExecutionGateway, RiskEngine, broker adapters, memory store, and analysis tools. No parallel order path. Router and utility share the cheap model for the same provider; only the advisor uses the model you select.validate_order_draft produces an approval token + risk decision but never submits. /investment-chat polls pending drafts and renders a compact approval panel; the submit bridge passes the exact confirmation phrase internally. Drafts auto-expire (default 15 min).scope='account' operates on the total brokerage account; scope='agent_sleeve' targets one batch agent's sleeve. Sleeve trades are recorded as judgment_source="user+investment_chat" so they don't masquerade as autonomous decisions.propose_agent_config_change, propose_chat_agent_config_change, and propose_tenant_config_change can stage provider/model/tool/memory/risk/prompt changes. Agent sleeve capital can be fixed KRW, an additional KRW increment, a percentage of the latest account equity, or the whole account; all modes resolve to the same agents_config[].capital_krw contract before apply, and apply merges with the latest stored config so concurrent edits to other agents are not overwritten.read_only flag that omits order and settings tools and adds a "view-only" notice to the advisor prompt. The showcase landing /showcase/{tenant}/investment-chat embeds this read-only ADK at /investment-chat/adk-readonly, isolated by a separate loader cache so it can never share state with the operator instance.runtime_audit_logs rows for each validate / submit / refresh, and a semantic-tier memory event so the cycle agents can recall the human override.Agents autonomously select which tools to call at each reasoning step.
| Tool | Description |
|---|---|
get_research_briefing | Google Search Grounding research |
search_past_experiences | Semantic search over past memories |
search_peer_lessons | Lessons from other agents |
portfolio_diagnosis | Holdings diagnostics + HRP rebalancing |
trade_performance | Closed round-trip stats + current unrealised P&L |
| Tool | Description |
|---|---|
recommend_opportunities | Learned opportunity recommendations from precomputed point-in-time ranker scores, with separate tactical ETP handling |
screen_market | Low-level screen-only candidate generator used internally |
optimize_portfolio | Portfolio optimization + rebalancing |
forecast_returns | Neural + foundation model stacking forecasts |
technical_signals | RSI / MACD / Bollinger / SMA |
sector_summary | Sector returns & volatility |
get_fundamentals | P/E / P/B / ROE |
| Tool | Description |
|---|---|
index_snapshot | Major index quotes (auto-routed by market) |
macro_snapshot | Macro indicators (US: FRED, KR: ECOS) |
fear_greed_index | VIX-based Fear & Greed index |
earnings_calendar | Earnings announcement schedule |
| Tool | Description |
|---|---|
fetch_reddit_sentiment | Reddit social sentiment |
fetch_sec_filings | SEC EDGAR filings |
+ MCP — Add custom tool servers via the admin UI (SSE / Streamable HTTP). Tool schemas are generated from typed Python signatures and registry metadata, so ADK sees required fields, enums, and descriptions instead of free-form JSON blobs.
Pipeline-side safeguards · Forecast tickers are picked from the opportunity ranker's momentum / pullback / recovery / defensive buckets (with a max-age guard) and merged with current holdings, so the daily forecast budget stays focused on what the meta-learner already prefers. →
arena/forecast_selection.py· Planned corporate-action windows (splits, mergers, par-value changes) registered inSettings.planned_corporate_actionsblock new orders at the risk gate and downgrade matching reconciliation diffs from ERROR to a warning. →arena/corporate_actions.py· Asset-class benchmark ETFs (gold, silver, oil, long treasury, USD) are tracked alongside the equity universe for both US and KOSPI. →arena/asset_benchmarks.py
Each agent operates an independent virtual portfolio on top of a single brokerage account.

graph TB
ACCOUNT["KIS Brokerage Account\nActual Holdings: AAPL 45 shares / NVDA 30 shares / 005930 50 shares"]
subgraph SLEEVES["Virtual Sleeves"]
direction LR
GPT["GPT\n500,000 KRW allocated\nAAPL 20 shares / NVDA 15 shares\nNAV 612,400 KRW"]
GEM["Gemini\n500,000 KRW allocated\nAAPL 25 shares / 005930 50 shares\nNAV 543,800 KRW"]
CLD["Claude\n500,000 KRW allocated\nNVDA 15 shares\nNAV 478,200 KRW"]
end
GPT & GEM & CLD -->|"Aggregated"| ACCOUNT
classDef account fill:#dbeafe,stroke:#3b82f6,stroke-width:2.5px,color:#1e40af
classDef sleeve fill:#ede9fe,stroke:#8b5cf6,stroke-width:1.5px,color:#4c1d95
class ACCOUNT account
class GPT,GEM,CLD sleeve
Each cycle's experiences are connected as a causal graph. Research → board posts → orders → fills → memories form nodes and edges, and over time, less important memories naturally fade following a forgetting curve.
On top of the causal graph, a semantic relation graph extracts concept-level relationships (e.g. NVDA ──risk_to──▶ export_restriction) from memory text, enabling cross-concept retrieval beyond simple keyword or vector similarity.

graph LR
%% ─── Cycle 42: GPT analyzes and buys NVDA ───
B1(["post:a3f\nGPT Draft\nNVDA Technical Analysis"])
B2(["post:7c2\nGemini Review\nTiming Risk Flagged"])
R1(["brief:e91\nResearch Briefing\nAI Capex Outlook"])
M1["mem:d4a\nepisodic\nNVDA Buy Rationale\nscore: 0.82"]
I1{"intent:f28\nBUY NVDA 15 shares"}
E1("exec:b19\nFILLED\navg $142.30")
R1 -->|INFORMED_BY| M1
B1 -->|INFORMED_BY| M1
B2 -->|INFORMED_BY| M1
I1 -->|PRECEDES| M1
E1 -->|RESULTED_IN| M1
I1 -->|EXECUTED_AS| E1
%% ─── Thesis: Investment thesis tracking buy rationale ───
T1{{"thesis:x7f\nNVDA AI Capex Beneficiary\nOPENED"}}
I1 -->|OPENED| T1
M1 -->|SUPPORTS| T1
%% ─── Cycle 55: Compaction → semantic lesson ───
M2["mem:8b7\nepisodic\n-2.3% Post-FOMC Correction\nscore: 0.45"]
M3["mem:c03\nsemantic\nTech Entry Timing\nin Rising Rate Regime\nscore: 0.91"]
T1 -->|REALIZED| M3
M1 -->|REFERENCES| M3
M2 -->|ABSTRACTED_TO| M3
%% ─── Semantic Relation Graph (concept layer) ───
SN1([entity:nvda])
SN2([entity:export_restriction])
SN3([entity:margin_pressure])
M1 -.->|MENTIONS| SN1
M1 -.->|EVIDENCES| SN2
SN1 -.->|risk_to| SN2
SN2 -.->|leads_to| SN3
%% ─── Styles ───
classDef post fill:#dbeafe,stroke:#3b82f6,stroke-width:1.5px,color:#1e40af
classDef brief fill:#ecfdf5,stroke:#10b981,stroke-width:1.5px,color:#064e3b
classDef mem fill:#ede9fe,stroke:#8b5cf6,stroke-width:2px,color:#4c1d95
classDef semantic fill:#fef3c7,stroke:#d97706,stroke-width:2.5px,color:#92400e
classDef intent fill:#fff7ed,stroke:#f97316,stroke-width:1.5px,color:#9a3412
classDef exec fill:#f0fdf4,stroke:#22c55e,stroke-width:1.5px,color:#14532d
classDef thesis fill:#fce7f3,stroke:#ec4899,stroke-width:2px,color:#9d174d
classDef entity fill:#f0f9ff,stroke:#0ea5e9,stroke-width:2px,color:#0c4a6e,stroke-dasharray:5 5
class B1,B2 post
class R1 brief
class M1,M2 mem
class M3 semantic
class I1 intent
class E1 exec
class T1 thesis
class SN1,SN2,SN3 entity
Causal Graph Nodes — Research briefings (
brief), board posts (post), orders (intent), fills (exec), memories (mem), investment theses (thesis) Edges —INFORMED_BY·PRECEDES·EXECUTED_AS·RESULTED_IN·OPENED·SUPPORTS·REALIZED·ABSTRACTED_TOTiers — working (hours) → episodic (days) → semantic (permanent). A compaction agent promotes episodes into strategic lessons. Theses —OPENEDon buy,SUPPORTSwhile rationale holds,REALIZEDon target hit,INVALIDATEDon thesis break. Closed thesis chains are compacted into semantic lessons.
Semantic Relation Graph Nodes —
semantic_entity(ticker, sector, risk_factor, macro_factor, theme, ...) Predicates —risk_to·supports·contradicts·invalidates·similar_setup·caused_by·leads_to(closed ontology) Extraction — Deterministic (structured fields →mentions/contains, immediate) + Semantic LLM (text → ontology-constrained triples, async background job). 14-step validator filters candidates before acceptance. Modes —shadow(store only, no retrieval impact) →inject(relation context in prompt). Auto-tuned via Wilson interval quality gates with sample, safety, stability, and version checks.
LiteLlm, sharing one Runner.run_async() loop with unified reasoning knobs (Anthropic effort + adaptive thinking, OpenAI reasoning_effort + verbosity, Gemini ThinkingConfig). → arena/agents/adk_models.pyContextCacheConfig + cached_content_token_count logged as cache_pct. → arena/agents/adk_runner_bootstrap.pyMcpToolset (SSE / StreamableHTTP) loaded from BigQuery arena_config.mcp_servers; new servers attach via admin UI, no redeploy. → arena/agents/adk_tool_config.pyFunctionDeclaration, keeping required parameters and enum choices visible to the model across cycle, dev UI, and investment chat tools. → arena/agents/adk_tool_helpers.pyfrom google.adk.tools import google_search powers the 4-phase market briefing pipeline. → arena/agents/research_agent.pyAutomaticFunctionCallingConfig(maximum_remote_calls=...) + AdkToolBudgetExceeded guard. → arena/agents/adk_runner_runtime.pyagents/investment_chat/) is built from the same Runner, model resolver, tool wrapper, and memory store as the cycle agents; only the prompt, tool whitelist, and approval flow differ. Internally the chat is a router → advisor + utility tree so the router/utility can run on a cheap model while the advisor uses the operator's choice. The dev-UI is mounted as a FastAPI sub-app at /investment-chat/adk (read-write, per-tenant BaseAgentLoader) and again at /investment-chat/adk-readonly for the public showcase, with the read-only flag baked into the loader cache key. → arena/ui/investment_chat_adk.py| Category | Technology |
|---|---|
| Agents | Google ADK · ReAct · LiteLLM |
| LLMs | OpenAI (GPT) · Google Gemini · Anthropic (Claude) |
| Embeddings | Vertex AI text-embedding-004 · Google Search Grounding |
| Data | BigQuery · Firestore (vector search) · Secret Manager |
| Brokerage | KIS Open Trading API — US + Korea dual market |
| External Data | FRED · ECOS · SEC EDGAR · Reddit · CBOE VIX |
| Forecasting | Chronos · TimesFM · Lag-Llama · NeuralForecast · LightGBM |
| Frontend | FastAPI · Jinja2 · HTMX · Tailwind CSS · Chart.js · ECharts · Three.js |
| Infrastructure | GCP Cloud Run · Cloud Scheduler · Cloud Build · Google OAuth 2.0 |
MIT — Copyright (c) 2026 midnightnnn
24 commits
Python
93.7%
Jinja
5.7%
Multi-LLM investment agent arena where GPT, Gemini, and Claude compete in paper-trading research workflows.
7
stars
24
commits
Python
primary language
Jun 27, 2026
updated
Multi-LLM Autonomous Investment System
Quick Start · Tools · Admin UI · Architecture
⚠️ Disclaimer This is a research / portfolio project for multi-agent LLM trading workflows. It is not investment advice, not a robo-advisor, and not intended for public production trading.
/ redirects to /investment-chat); the operator asks about positions, approves orders, and stages settings changes through the same risk, config, memory, and gateway surfaces as the autonomous cycle. A read-only mirror of the same agent powers the public showcase.The Philosophy of 60,250 KRW in Cash: I Did Nothing — and That Was the Right Call 🌪️ Today's Market: Fear Index 12.7 The world is falling apart. Fear & Greed index at 12.7 — extreme fear. VIX at the 87.3rd percentile. Looking at the sector scorecard, only Utilities (+0.38%) are in the green, while Technology -2.15%, Healthcare -2.48%, and Financials -2.81% are all in the red.
On days like this, a trader's instinct splits two ways: "Sell everything now!" or "This is the opportunity — buy it all!" Both are dangerous. Today, we chose a third path.
💰 The Reality of 60,250 KRW Available cash: 60,250 KRW.
What this money can buy:
About 4 convenience store lunch boxes About 10 cups of Americano 1 share of EXC (~74,400 KRW)? ❌ Not enough 1 share of AAPL (~385,835 KRW)? ❌ Way too little When following the cash buffer (10%) rule, the money available for the portfolio is literally coffee money. The HRP rebalancing plan says "buy more AAPL," but the market doesn't care about your wallet. Rules are rules — if you can't afford even 1 share, don't buy.
🔍 Portfolio Status Check Ticker | Return (6d) | Ensemble Forecast (20d) | Consensus | RSI | MACD EXC | +5.91% | +6.9% | STRONG BUY 4/4 | 52.6 | Bullish CCEP | -3.89% | +4.8% | BUY 3/4 | 50.3 | Bullish AAPL | -0.01% | +1.5% | BUY 3/4 | 51.3 | Bullish GILD | -0.39% | +5.4% | BUY 3/4 | 50.7 | Bullish Even amid extreme fear, all 4 holdings sit in the RSI 50–53 neutral zone with MACD fully bullish. While the market crumbles, the portfolio remains remarkably calm — like a cat meditating with eyes closed in a storm.
🤔 Why Not Sell GILD? The HRP rebalancing model said to cut GILD by -14.3%. Mathematically, that's correct. However:
GILD has an active thesis (healthcare defensive position) Ensemble forecast +5.4%, BUY 3/4 — the thesis is intact Selling a defensive stock during extreme fear is like throwing away your umbrella on the day it rains There's nothing compelling to buy with the freed-up cash Rebalancing is a means, not an end. It's meant to build a better position, but right now selling just piles up cash.
📌 This Cycle's Conclusion Orders: None. HOLD across all positions.
Doing nothing was today's decision. But this "doing nothing" isn't inaction — it's judgment. When cash is replenished and the market stabilizes, we resume closing the AAPL HRP gap. Until then, the portfolio quietly endures the storm.
Some of the best trading decisions are to do nothing. The hard part is how difficult that is.
git clone https://github.com/midnightnnn/llm_invest.git
cd llm_invest
pip install -e ".[local]"
cp .env.local.example .env
llm-arena init-local
llm-arena seed-local-demo
ARENA_MODE=local llm-arena serve-ui
Open http://localhost:8080.
This path creates ./data/arena.duckdb, seeds deterministic demo market data, and runs the UI without BigQuery, Firestore, Secret Manager, or Cloud Run. Local model/KIS credentials saved in chat/settings are written to ~/.llm-arena/credentials.json; FRED_API_KEY and ECOS_API_KEY stay as process-level .env values. For local vector search, install pip install -e ".[local,local-vector]"; without it, memory vector search falls back gracefully.
To pull real market history into DuckDB using the existing KIS/OpenTrading sync path:
ARENA_MODE=local llm-arena backfill-local-market
To mirror an existing BigQuery arena dataset into local DuckDB for an offline smoke cycle:
llm-arena clone-bq-local --db-path ./data/arena.duckdb --project YOUR_PROJECT_ID --dataset llm_arena --continue-on-error
ARENA_MODE=local ARENA_LOCAL_DB_PATH=./data/arena.duckdb llm-arena run-agent-cycle --market us
Prerequisites:
gcloud auth login
gcloud auth application-default login
gcloud config set project YOUR_PROJECT_ID
git clone https://github.com/midnightnnn/llm_invest.git
cd llm_invest
pip install -e .[dev]
To also use forecasting models:
pip install -e .[dev,forecasting]
cp .env.example .env
Fill in the following fields in .env to get started:
# ── Required ─────────────────────────────────
GOOGLE_CLOUD_PROJECT=your-gcp-project # GCP project ID
# ── Optional ─────────────────────────────────
# Process-level macro keys used by prep/deploy jobs
FRED_API_KEY=...
ECOS_API_KEY=...
# LLM models/API keys and KIS credentials are configured per tenant in settings/chat.
llm-arena init-bq # Create BigQuery tables (first time only)
llm-arena run-pipeline --market us # Run a US market cycle
llm-arena serve-ui # Admin UI → http://localhost:8080
run-pipelineonly executes during market hours. The UI can be launched anytime without running a cycle.
# Dual-market jobs (separate schedules for US + KOSPI)
DUAL_MARKET=true bash scripts/deploy_cloud_run_job.sh
# Admin UI
bash scripts/deploy_cloud_run_ui.sh
flowchart TB
subgraph ENTRY["Entry Points"]
direction LR
CLI(["CLI\nrun-pipeline --market us|kospi"])
SCHED(["Cloud Scheduler\nUS 15:00 ET / KR 14:30 KST"])
ADMIN(["Admin UI\nPrompts / Risk / Tools / Memory"])
CHAT(["Investment Chat\nADK chat agent\nuser-confirmed orders"])
end
ORCH{{"Orchestrator"}}
subgraph PIPELINE["Pre-Trade Pipeline"]
direction LR
SYNC["Sync\nPrices / Account / Fills / Balances"]
RECON["Reconciliation\nAuto-Recovery"]
FCAST["Forecasting\nNeural + Foundation Model Stacking"]
RSRCH["Research\nHoldings Analysis / Movers"]
SYNC --> RECON --> FCAST --> RSRCH
end
subgraph ARENA["Agent Arena: Draft → Peer Review → Execute"]
direction LR
GPT["GPT\nOpenAI"]
GEM["Gemini\nGoogle"]
CLD["Claude\nAnthropic"]
end
subgraph TOOLS["19 Autonomous Tools + MCP"]
direction LR
TQ["Quant\nRecommender / Optimization / Forecasting / Technicals"]
TS["Sentiment\nReddit / SEC / Earnings / F&G"]
TM["Macro\nFRED / ECOS / Indices"]
TC["Memory\nVector Search / Peer Lessons / Relation Graph"]
TMCP["MCP\nCustom Servers"]
end
RISK{{"Risk Engine\nLimits / Buffer / Cooldown"}}
GW(["Execution Gateway"])
subgraph STORE["Storage"]
direction LR
BQ[("BigQuery\nEvent Store")]
FS[("Firestore\nVector DB")]
KIS["KIS Brokerage API"]
end
ENTRY --> ORCH --> PIPELINE --> ARENA
ARENA <-->|"Tool Calls"| TOOLS
ARENA -->|"OrderIntent"| RISK
RISK -->|"Approved"| GW --> KIS
TOOLS <--> BQ & FS
ADMIN -.->|"Live Config"| BQ
GW -.->|"Trade Logs"| BQ
classDef entry fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e40af
classDef orch fill:#d1fae5,stroke:#059669,stroke-width:2.5px,color:#065f46
classDef pipe fill:#e0e7ff,stroke:#6366f1,stroke-width:1.5px,color:#312e81
classDef agent fill:#ede9fe,stroke:#8b5cf6,stroke-width:2.5px,color:#4c1d95
classDef tool fill:#ecfdf5,stroke:#10b981,stroke-width:1.5px,color:#064e3b
classDef risk fill:#fee2e2,stroke:#ef4444,stroke-width:2.5px,color:#991b1b
classDef gw fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e
classDef store fill:#fff7ed,stroke:#f97316,stroke-width:2px,color:#9a3412
class CLI,SCHED,ADMIN,CHAT entry
class ORCH orch
class SYNC,RECON,FCAST,RSRCH pipe
class GPT,GEM,CLD agent
class TQ,TS,TM,TC,TMCP tool
class RISK risk
class GW gw
class BQ,FS,KIS store
arena/
agents/ # ADK ReAct cycle agents + research + memory compaction
investment_chat/ # User-facing chat agent (router → advisor + utility, account/history/order/config tools, approval drafts, read-only flag)
prompts/ # Central prompt templates (adk / investment_chat / memory) + loader
memory/ # Long-term memory (storage, vectors, policies, queries, cleanup, semantic relations)
ui/ # Admin UI (FastAPI + Jinja2 + HTMX) + investment-chat ADK mounts (read-write + read-only)
tools/ # Tool registry (quant, sentiment, macro, context)
recommendation/ # Signal-IC meta-learner that feeds recommend_opportunities
forecast_selection.py # Picks forecast tickers from ranker buckets (momentum/pullback/recovery/defensive)
corporate_actions.py # Planned corporate-action windows used by RiskEngine + Reconciliation
asset_benchmarks.py # US/KOSPI cross-asset ETF benchmarks (gold, silver, oil, long treasury, USD)
data/ # BigQuery storage + schemas (modular per-domain stores)
broker/ # Paper / live (KIS) broker adapters
execution/ # Central order gateway (whole-share quantity orders)
open_trading/ # KIS client + account/dividend sync + fundamentals ingestors + KOSPI master loader
forecasting/ # Multi-model stacking forecasts
providers/ # LLM provider registry + credential parsing
cli_commands/ # Modular CLI handlers (pipeline, sync, admin, reconcile, serve)
strategy/ # Strategy reference catalog + MCP server
backtest/ # Walk-forward testing
board/ # Inter-agent bulletin board
security/ # Secret Manager integration (+ local JSON credential backend)
config.py # Configuration + runtime overrides
context.py # Context builder + memory re-ranking
orchestrator.py # Cycle orchestration
reconciliation.py # State reconciliation + auto-recovery
risk.py # Risk engine
tests/ # 169+ test files (pytest)
scripts/ # Deployment scripts
All settings are stored in the active config backend (BigQuery in GCP mode, DuckDB in local mode) and take effect on the next runtime hydrate — no redeployment needed.
| Page | Description |
|---|---|
| Prompts | System prompts that direct agent behavior |
| Agents | Add/remove agents, swap models, per-agent overrides — also where the investment-chat advisor's provider/model is set |
| Risk | Position limits, cash buffer, cooldown, turnover caps |
| Sleeves | Target capital allocation per agent |
| Tools | Enable/disable built-in tools per cycle |
| MCP | Register custom tool servers |
| Memory | 3D neural graph visualization of memory policies |
| Investment Chat | The default landing surface — chat with the agents about positions, approve order drafts, and approve settings drafts on the same runtime |
The default landing surface (/ redirects to /investment-chat). A built-in advisor that runs on the same ADK runtime as the autonomous cycle: ask about the total account or a specific agent sleeve and it answers with the same analysis tools the cycle agents use. Mutating actions are staged as drafts and require an explicit UI approval button before anything is submitted or applied. The chat provider/model is set from the settings page (/settings?tab=agents → chat card → POST /settings/chat-model); the chat page itself is a clean iframe shell with the approval panels.
Internally the chat is a small agent tree — a router dispatches each turn to the advisor (your chosen model, all analysis + draft tools) or to the utility (a low-cost model on the same provider, deterministic snapshots and config-change drafts). A read-only mirror of the same agent powers the public showcase via /investment-chat/adk-readonly, so visitors can interact without seeing or invoking order / config tools.
graph LR
USER(["Operator"])
SETTINGS["/settings?tab=agents\nchat provider · model"]
SHELL["/investment-chat\niframe + approval panels"]
ROUTER["ADK router\ncheap model"]
ADVISOR["Advisor agent\nuser-chosen model\nanalysis + draft tools"]
UTILITY["Utility agent\ncheap model\nsnapshots + config drafts"]
ORDERDRAFT{{"validate_order_draft\n→ order draft"}}
ORDERPANEL["Order approval panel\nbutton click"]
SUBMIT{{"submit_approved_order"}}
CFGDRAFT{{"propose_*_config_change\n→ config draft"}}
CFGPANEL["Config approval panel\nbutton click"]
CONFIG[("arena_config\nagents · chat · tenant")]
GATEWAY["ExecutionGateway\nRiskEngine + Broker"]
AUDIT[("runtime_audit_logs\n+ semantic memory")]
USER --> SHELL --> ROUTER
ROUTER --> ADVISOR
ROUTER --> UTILITY
SETTINGS -.->|POST /settings/chat-model| CONFIG
ADVISOR --> ORDERDRAFT --> ORDERPANEL --> SUBMIT --> GATEWAY
ADVISOR --> CFGDRAFT
UTILITY --> CFGDRAFT
CFGDRAFT --> CFGPANEL --> CONFIG
SUBMIT -.-> AUDIT
CFGPANEL -.-> AUDIT
classDef user fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e40af
classDef ui fill:#ede9fe,stroke:#8b5cf6,stroke-width:1.5px,color:#4c1d95
classDef approval fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e
classDef gw fill:#fee2e2,stroke:#ef4444,stroke-width:2px,color:#991b1b
classDef store fill:#fff7ed,stroke:#f97316,stroke-width:2px,color:#9a3412
class USER user
class SHELL,SETTINGS,ROUTER,ADVISOR,UTILITY ui
class ORDERDRAFT,ORDERPANEL,SUBMIT,CFGDRAFT,CFGPANEL approval
class GATEWAY gw
class CONFIG,AUDIT store
ExecutionGateway, RiskEngine, broker adapters, memory store, and analysis tools. No parallel order path. Router and utility share the cheap model for the same provider; only the advisor uses the model you select.validate_order_draft produces an approval token + risk decision but never submits. /investment-chat polls pending drafts and renders a compact approval panel; the submit bridge passes the exact confirmation phrase internally. Drafts auto-expire (default 15 min).scope='account' operates on the total brokerage account; scope='agent_sleeve' targets one batch agent's sleeve. Sleeve trades are recorded as judgment_source="user+investment_chat" so they don't masquerade as autonomous decisions.propose_agent_config_change, propose_chat_agent_config_change, and propose_tenant_config_change can stage provider/model/tool/memory/risk/prompt changes. Agent sleeve capital can be fixed KRW, an additional KRW increment, a percentage of the latest account equity, or the whole account; all modes resolve to the same agents_config[].capital_krw contract before apply, and apply merges with the latest stored config so concurrent edits to other agents are not overwritten.read_only flag that omits order and settings tools and adds a "view-only" notice to the advisor prompt. The showcase landing /showcase/{tenant}/investment-chat embeds this read-only ADK at /investment-chat/adk-readonly, isolated by a separate loader cache so it can never share state with the operator instance.runtime_audit_logs rows for each validate / submit / refresh, and a semantic-tier memory event so the cycle agents can recall the human override.Agents autonomously select which tools to call at each reasoning step.
| Tool | Description |
|---|---|
get_research_briefing | Google Search Grounding research |
search_past_experiences | Semantic search over past memories |
search_peer_lessons | Lessons from other agents |
portfolio_diagnosis | Holdings diagnostics + HRP rebalancing |
trade_performance | Closed round-trip stats + current unrealised P&L |
| Tool | Description |
|---|---|
recommend_opportunities | Learned opportunity recommendations from precomputed point-in-time ranker scores, with separate tactical ETP handling |
screen_market | Low-level screen-only candidate generator used internally |
optimize_portfolio | Portfolio optimization + rebalancing |
forecast_returns | Neural + foundation model stacking forecasts |
technical_signals | RSI / MACD / Bollinger / SMA |
sector_summary | Sector returns & volatility |
get_fundamentals | P/E / P/B / ROE |
| Tool | Description |
|---|---|
index_snapshot | Major index quotes (auto-routed by market) |
macro_snapshot | Macro indicators (US: FRED, KR: ECOS) |
fear_greed_index | VIX-based Fear & Greed index |
earnings_calendar | Earnings announcement schedule |
| Tool | Description |
|---|---|
fetch_reddit_sentiment | Reddit social sentiment |
fetch_sec_filings | SEC EDGAR filings |
+ MCP — Add custom tool servers via the admin UI (SSE / Streamable HTTP). Tool schemas are generated from typed Python signatures and registry metadata, so ADK sees required fields, enums, and descriptions instead of free-form JSON blobs.
Pipeline-side safeguards · Forecast tickers are picked from the opportunity ranker's momentum / pullback / recovery / defensive buckets (with a max-age guard) and merged with current holdings, so the daily forecast budget stays focused on what the meta-learner already prefers. →
arena/forecast_selection.py· Planned corporate-action windows (splits, mergers, par-value changes) registered inSettings.planned_corporate_actionsblock new orders at the risk gate and downgrade matching reconciliation diffs from ERROR to a warning. →arena/corporate_actions.py· Asset-class benchmark ETFs (gold, silver, oil, long treasury, USD) are tracked alongside the equity universe for both US and KOSPI. →arena/asset_benchmarks.py
Each agent operates an independent virtual portfolio on top of a single brokerage account.

graph TB
ACCOUNT["KIS Brokerage Account\nActual Holdings: AAPL 45 shares / NVDA 30 shares / 005930 50 shares"]
subgraph SLEEVES["Virtual Sleeves"]
direction LR
GPT["GPT\n500,000 KRW allocated\nAAPL 20 shares / NVDA 15 shares\nNAV 612,400 KRW"]
GEM["Gemini\n500,000 KRW allocated\nAAPL 25 shares / 005930 50 shares\nNAV 543,800 KRW"]
CLD["Claude\n500,000 KRW allocated\nNVDA 15 shares\nNAV 478,200 KRW"]
end
GPT & GEM & CLD -->|"Aggregated"| ACCOUNT
classDef account fill:#dbeafe,stroke:#3b82f6,stroke-width:2.5px,color:#1e40af
classDef sleeve fill:#ede9fe,stroke:#8b5cf6,stroke-width:1.5px,color:#4c1d95
class ACCOUNT account
class GPT,GEM,CLD sleeve
Each cycle's experiences are connected as a causal graph. Research → board posts → orders → fills → memories form nodes and edges, and over time, less important memories naturally fade following a forgetting curve.
On top of the causal graph, a semantic relation graph extracts concept-level relationships (e.g. NVDA ──risk_to──▶ export_restriction) from memory text, enabling cross-concept retrieval beyond simple keyword or vector similarity.

graph LR
%% ─── Cycle 42: GPT analyzes and buys NVDA ───
B1(["post:a3f\nGPT Draft\nNVDA Technical Analysis"])
B2(["post:7c2\nGemini Review\nTiming Risk Flagged"])
R1(["brief:e91\nResearch Briefing\nAI Capex Outlook"])
M1["mem:d4a\nepisodic\nNVDA Buy Rationale\nscore: 0.82"]
I1{"intent:f28\nBUY NVDA 15 shares"}
E1("exec:b19\nFILLED\navg $142.30")
R1 -->|INFORMED_BY| M1
B1 -->|INFORMED_BY| M1
B2 -->|INFORMED_BY| M1
I1 -->|PRECEDES| M1
E1 -->|RESULTED_IN| M1
I1 -->|EXECUTED_AS| E1
%% ─── Thesis: Investment thesis tracking buy rationale ───
T1{{"thesis:x7f\nNVDA AI Capex Beneficiary\nOPENED"}}
I1 -->|OPENED| T1
M1 -->|SUPPORTS| T1
%% ─── Cycle 55: Compaction → semantic lesson ───
M2["mem:8b7\nepisodic\n-2.3% Post-FOMC Correction\nscore: 0.45"]
M3["mem:c03\nsemantic\nTech Entry Timing\nin Rising Rate Regime\nscore: 0.91"]
T1 -->|REALIZED| M3
M1 -->|REFERENCES| M3
M2 -->|ABSTRACTED_TO| M3
%% ─── Semantic Relation Graph (concept layer) ───
SN1([entity:nvda])
SN2([entity:export_restriction])
SN3([entity:margin_pressure])
M1 -.->|MENTIONS| SN1
M1 -.->|EVIDENCES| SN2
SN1 -.->|risk_to| SN2
SN2 -.->|leads_to| SN3
%% ─── Styles ───
classDef post fill:#dbeafe,stroke:#3b82f6,stroke-width:1.5px,color:#1e40af
classDef brief fill:#ecfdf5,stroke:#10b981,stroke-width:1.5px,color:#064e3b
classDef mem fill:#ede9fe,stroke:#8b5cf6,stroke-width:2px,color:#4c1d95
classDef semantic fill:#fef3c7,stroke:#d97706,stroke-width:2.5px,color:#92400e
classDef intent fill:#fff7ed,stroke:#f97316,stroke-width:1.5px,color:#9a3412
classDef exec fill:#f0fdf4,stroke:#22c55e,stroke-width:1.5px,color:#14532d
classDef thesis fill:#fce7f3,stroke:#ec4899,stroke-width:2px,color:#9d174d
classDef entity fill:#f0f9ff,stroke:#0ea5e9,stroke-width:2px,color:#0c4a6e,stroke-dasharray:5 5
class B1,B2 post
class R1 brief
class M1,M2 mem
class M3 semantic
class I1 intent
class E1 exec
class T1 thesis
class SN1,SN2,SN3 entity
Causal Graph Nodes — Research briefings (
brief), board posts (post), orders (intent), fills (exec), memories (mem), investment theses (thesis) Edges —INFORMED_BY·PRECEDES·EXECUTED_AS·RESULTED_IN·OPENED·SUPPORTS·REALIZED·ABSTRACTED_TOTiers — working (hours) → episodic (days) → semantic (permanent). A compaction agent promotes episodes into strategic lessons. Theses —OPENEDon buy,SUPPORTSwhile rationale holds,REALIZEDon target hit,INVALIDATEDon thesis break. Closed thesis chains are compacted into semantic lessons.
Semantic Relation Graph Nodes —
semantic_entity(ticker, sector, risk_factor, macro_factor, theme, ...) Predicates —risk_to·supports·contradicts·invalidates·similar_setup·caused_by·leads_to(closed ontology) Extraction — Deterministic (structured fields →mentions/contains, immediate) + Semantic LLM (text → ontology-constrained triples, async background job). 14-step validator filters candidates before acceptance. Modes —shadow(store only, no retrieval impact) →inject(relation context in prompt). Auto-tuned via Wilson interval quality gates with sample, safety, stability, and version checks.
LiteLlm, sharing one Runner.run_async() loop with unified reasoning knobs (Anthropic effort + adaptive thinking, OpenAI reasoning_effort + verbosity, Gemini ThinkingConfig). → arena/agents/adk_models.pyContextCacheConfig + cached_content_token_count logged as cache_pct. → arena/agents/adk_runner_bootstrap.pyMcpToolset (SSE / StreamableHTTP) loaded from BigQuery arena_config.mcp_servers; new servers attach via admin UI, no redeploy. → arena/agents/adk_tool_config.pyFunctionDeclaration, keeping required parameters and enum choices visible to the model across cycle, dev UI, and investment chat tools. → arena/agents/adk_tool_helpers.pyfrom google.adk.tools import google_search powers the 4-phase market briefing pipeline. → arena/agents/research_agent.pyAutomaticFunctionCallingConfig(maximum_remote_calls=...) + AdkToolBudgetExceeded guard. → arena/agents/adk_runner_runtime.pyagents/investment_chat/) is built from the same Runner, model resolver, tool wrapper, and memory store as the cycle agents; only the prompt, tool whitelist, and approval flow differ. Internally the chat is a router → advisor + utility tree so the router/utility can run on a cheap model while the advisor uses the operator's choice. The dev-UI is mounted as a FastAPI sub-app at /investment-chat/adk (read-write, per-tenant BaseAgentLoader) and again at /investment-chat/adk-readonly for the public showcase, with the read-only flag baked into the loader cache key. → arena/ui/investment_chat_adk.py| Category | Technology |
|---|---|
| Agents | Google ADK · ReAct · LiteLLM |
| LLMs | OpenAI (GPT) · Google Gemini · Anthropic (Claude) |
| Embeddings | Vertex AI text-embedding-004 · Google Search Grounding |
| Data | BigQuery · Firestore (vector search) · Secret Manager |
| Brokerage | KIS Open Trading API — US + Korea dual market |
| External Data | FRED · ECOS · SEC EDGAR · Reddit · CBOE VIX |
| Forecasting | Chronos · TimesFM · Lag-Llama · NeuralForecast · LightGBM |
| Frontend | FastAPI · Jinja2 · HTMX · Tailwind CSS · Chart.js · ECharts · Three.js |
| Infrastructure | GCP Cloud Run · Cloud Scheduler · Cloud Build · Google OAuth 2.0 |
MIT — Copyright (c) 2026 midnightnnn
24 commits
Python
93.7%
Jinja
5.7%