3340fd11ca
- Initialize Prisma with SQLite and Stock model - Create database service layer with singleton client - Add API routes for stock CRUD operations - Integrate database with analyze page to persist ticker entries - Add Playwright tests for stock database functionality
1015 lines
27 KiB
Markdown
1015 lines
27 KiB
Markdown
# Multi-Agent Trading Framework Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Build a multi-agent LLM trading framework inspired by TradingAgents with OpenRouter integration and free model support.
|
|
|
|
**Architecture:** Create agent-based architecture where specialized LLM agents analyze stocks through different lenses (fundamentals, sentiment, technical) and debate before making trading decisions. Use OpenRouter for model access with ability to select free models.
|
|
|
|
**Tech Stack:** OpenRouter API, TypeScript, React Router 7, existing Alpaca integration, node-fetch for HTTP requests
|
|
|
|
---
|
|
|
|
## Phase 1: OpenRouter Client Setup
|
|
|
|
### Task 1: Create OpenRouter API Client
|
|
|
|
**Files:**
|
|
- Create: `app/lib/openrouter.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```typescript
|
|
// app/lib/__tests__/openrouter.test.ts
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
import { OpenRouterClient } from '../openrouter'
|
|
|
|
describe('OpenRouterClient', () => {
|
|
it('should create instance with API key', () => {
|
|
const client = new OpenRouterClient('test-key')
|
|
expect(client).toBeDefined()
|
|
})
|
|
|
|
it('should have default free models list', () => {
|
|
const client = new OpenRouterClient('test-key')
|
|
expect(client.getFreeModels()).toContain('google/gemini-2.0-flash-exp:free')
|
|
})
|
|
|
|
it('should have available model providers', () => {
|
|
const client = new OpenRouterClient('test-key')
|
|
const providers = client.getProviders()
|
|
expect(providers).toContain('openai')
|
|
expect(providers).toContain('google')
|
|
expect(providers).toContain('anthropic')
|
|
})
|
|
})
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `npm run test -- app/lib/__tests__/openrouter.test.ts`
|
|
Expected: FAIL with "Cannot find module" or similar
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
```typescript
|
|
// app/lib/openrouter.ts
|
|
import type { ChatCompletion } from '../types'
|
|
|
|
export interface OpenRouterConfig {
|
|
apiKey: string
|
|
baseURL?: string
|
|
defaultModel?: string
|
|
}
|
|
|
|
export class OpenRouterClient {
|
|
private apiKey: string
|
|
private baseURL: string
|
|
private defaultModel: string
|
|
private freeModels = [
|
|
'google/gemini-2.0-flash-exp:free',
|
|
'deepseek/deepseek-chat:free',
|
|
'meta/llama-3.3-70b-instruct:free'
|
|
]
|
|
|
|
constructor(apiKey: string, config?: Partial<OpenRouterConfig>) {
|
|
this.apiKey = apiKey
|
|
this.baseURL = config?.baseURL || 'https://openrouter.ai/api/v1'
|
|
this.defaultModel = config?.defaultModel || 'google/gemini-2.0-flash-exp:free'
|
|
}
|
|
|
|
getFreeModels(): string[] {
|
|
return [...this.freeModels]
|
|
}
|
|
|
|
getProviders(): string[] {
|
|
return ['openai', 'google', 'anthropic', 'deepseek', 'meta', 'xai']
|
|
}
|
|
|
|
async createChatCompletion(
|
|
messages: Array<{ role: string; content: string }>,
|
|
model?: string
|
|
): Promise<ChatCompletion> {
|
|
const response = await fetch(`${this.baseURL}/chat/completions`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
'Content-Type': 'application/json',
|
|
'HTTP-Referer': 'https://aitrader.local',
|
|
'X-Title': 'AITrader'
|
|
},
|
|
body: JSON.stringify({
|
|
model: model || this.defaultModel,
|
|
messages
|
|
})
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`OpenRouter API error: ${response.status}`)
|
|
}
|
|
|
|
return response.json()
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run tests to verify they pass**
|
|
|
|
Run: `npm run test -- app/lib/__tests__/openrouter.test.ts`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add app/lib/openrouter.ts app/lib/__tests__/openrouter.test.ts
|
|
git commit -m "feat: add OpenRouter API client with free model support"
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 2: Agent Type Definitions
|
|
|
|
### Task 2: Define Agent Types and Interfaces
|
|
|
|
**Files:**
|
|
- Create: `app/types/agents.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```typescript
|
|
// app/types/__tests__/agents.test.ts
|
|
import { describe, it, expect } from 'vitest'
|
|
import type { AgentSignal, AnalystReport } from '../agents'
|
|
|
|
describe('Agent Types', () => {
|
|
it('should define valid agent signal structure', () => {
|
|
const signal: AgentSignal = {
|
|
agent: 'fundamentals',
|
|
signal: 'bullish',
|
|
confidence: 0.85,
|
|
reasoning: 'Strong fundamentals'
|
|
}
|
|
expect(signal.agent).toBe('fundamentals')
|
|
expect(signal.signal).toBe('bullish')
|
|
})
|
|
|
|
it('should allow neutral signal', () => {
|
|
const signal: AgentSignal = {
|
|
agent: 'technical',
|
|
signal: 'neutral',
|
|
confidence: 0.5,
|
|
reasoning: 'Market indecision'
|
|
}
|
|
expect(signal.signal).toBe('neutral')
|
|
})
|
|
})
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `npm run test -- app/types/__tests__/agents.test.ts`
|
|
Expected: FAIL - types file doesn't exist
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
```typescript
|
|
// app/types/agents.ts
|
|
export type SignalType = 'bullish' | 'bearish' | 'neutral'
|
|
|
|
export interface AgentSignal {
|
|
agent: 'fundamentals' | 'sentiment' | 'news' | 'technical' | 'trader'
|
|
signal: SignalType
|
|
confidence: number
|
|
reasoning: string
|
|
timestamp: string
|
|
}
|
|
|
|
export interface AnalystReport {
|
|
analyst: 'fundamentals' | 'sentiment' | 'news' | 'technical'
|
|
report: string
|
|
signal: AgentSignal
|
|
}
|
|
|
|
export interface DebateRound {
|
|
bullishView: string
|
|
bearishView: string
|
|
researcher: 'bullish' | 'bearish'
|
|
}
|
|
|
|
export interface TradingDecision {
|
|
action: 'buy' | 'sell' | 'hold'
|
|
confidence: number
|
|
targetPrice?: number
|
|
stopLoss?: number
|
|
reasoning: string
|
|
agentSignals: AgentSignal[]
|
|
debateRounds: DebateRound[]
|
|
}
|
|
|
|
export interface AgentConfig {
|
|
llmProvider: 'openrouter'
|
|
model: string
|
|
maxDebateRounds: number
|
|
temperature: number
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run tests to verify they pass**
|
|
|
|
Run: `npm run test -- app/types/__tests__/agents.test.ts`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add app/types/agents.ts app/types/__tests__/agents.test.ts
|
|
git commit -m "feat: add agent types and interfaces"
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 3: Fundamentals Analyst Agent
|
|
|
|
### Task 3: Implement Fundamentals Analyst Agent
|
|
|
|
**Files:**
|
|
- Create: `app/agents/fundamentals.ts`
|
|
- Create: `app/agents/__tests__/fundamentals.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```typescript
|
|
// app/agents/__tests__/fundamentals.test.ts
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
import { FundamentalsAnalyst } from '../fundamentals'
|
|
import type { OpenRouterClient } from '../../lib/openrouter'
|
|
|
|
describe('FundamentalsAnalyst', () => {
|
|
const mockClient = {
|
|
createChatCompletion: vi.fn().mockResolvedValue({
|
|
choices: [{ message: { content: 'Bullish: Strong revenue growth' } }]
|
|
})
|
|
} as unknown as OpenRouterClient
|
|
|
|
it('should analyze company fundamentals', async () => {
|
|
const analyst = new FundamentalsAnalyst(mockClient)
|
|
const result = await analyst.analyze('AAPL', {
|
|
revenue: 394300000000,
|
|
netIncome: 97000000000,
|
|
debtToEquity: 1.43
|
|
})
|
|
|
|
expect(result.analyst).toBe('fundamentals')
|
|
expect(result.signal.signal).toBe('bullish')
|
|
})
|
|
|
|
it('should use specified model', () => {
|
|
const client = { createChatCompletion: vi.fn() } as unknown as OpenRouterClient
|
|
const analyst = new FundamentalsAnalyst(client, { model: 'custom-model' })
|
|
expect(analyst.getModel()).toBe('custom-model')
|
|
})
|
|
})
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `npm run test -- app/agents/__tests__/fundamentals.test.ts`
|
|
Expected: FAIL
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
```typescript
|
|
// app/agents/fundamentals.ts
|
|
import type { OpenRouterClient } from '../lib/openrouter'
|
|
import type { AnalystReport, AgentSignal } from '../types/agents'
|
|
|
|
interface FinancialData {
|
|
revenue?: number
|
|
netIncome?: number
|
|
debtToEquity?: number
|
|
earningsPerShare?: number
|
|
priceToEarnings?: number
|
|
}
|
|
|
|
interface AnalystConfig {
|
|
model?: string
|
|
}
|
|
|
|
export class FundamentalsAnalyst {
|
|
private client: OpenRouterClient
|
|
private model: string
|
|
|
|
constructor(client: OpenRouterClient, config?: AnalystConfig) {
|
|
this.client = client
|
|
this.model = config?.model || 'google/gemini-2.0-flash-exp:free'
|
|
}
|
|
|
|
getModel(): string {
|
|
return this.model
|
|
}
|
|
|
|
async analyze(ticker: string, financialData: FinancialData): Promise<AnalystReport> {
|
|
const messages = [
|
|
{
|
|
role: 'system',
|
|
content: `You are a fundamental analyst. Analyze the financial data and provide a trading signal (bullish/bearish/neutral) with confidence and concise reasoning.`
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: `Analyze ${ticker} with this data:\n${JSON.stringify(financialData, null, 2)}`
|
|
}
|
|
]
|
|
|
|
const response = await this.client.createChatCompletion(messages, this.model)
|
|
const content = response.choices[0].message.content
|
|
|
|
// Parse signal from response
|
|
const isBullish = content.toLowerCase().includes('bullish')
|
|
const isBearish = content.toLowerCase().includes('bearish')
|
|
const signal: AgentSignal['signal'] = isBullish ? 'bullish' : isBearish ? 'bearish' : 'neutral'
|
|
|
|
return {
|
|
analyst: 'fundamentals',
|
|
report: content,
|
|
signal: {
|
|
agent: 'fundamentals',
|
|
signal,
|
|
confidence: 0.75,
|
|
reasoning: content,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run tests to verify they pass**
|
|
|
|
Run: `npm run test -- app/agents/__tests__/fundamentals.test.ts`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add app/agents/fundamentals.ts app/agents/__tests__/fundamentals.test.ts
|
|
git commit -m "feat: add fundamentals analyst agent"
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 4: Technical Analyst Agent (Wraps Existing Indicators)
|
|
|
|
### Task 4: Implement Technical Analyst Agent
|
|
|
|
**Files:**
|
|
- Create: `app/agents/technical.ts`
|
|
- Create: `app/agents/__tests__/technical.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```typescript
|
|
// app/agents/__tests__/technical.test.ts
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
import { TechnicalAnalyst } from '../technical'
|
|
import type { OpenRouterClient } from '../../lib/openrouter'
|
|
|
|
describe('TechnicalAnalyst', () => {
|
|
const mockClient = {
|
|
createChatCompletion: vi.fn().mockResolvedValue({
|
|
choices: [{ message: { content: 'Bullish: MACD crossover positive' } }]
|
|
})
|
|
} as unknown as OpenRouterClient
|
|
|
|
it('should analyze technical indicators', async () => {
|
|
const analyst = new TechnicalAnalyst(mockClient)
|
|
const result = await analyst.analyze('AAPL', {
|
|
prices: [100, 102, 101, 103, 105],
|
|
sma: 102,
|
|
rsi: 65,
|
|
macd: 1.2
|
|
})
|
|
|
|
expect(result.analyst).toBe('technical')
|
|
expect(result.signal.signal).toBe('bullish')
|
|
})
|
|
})
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `npm run test -- app/agents/__tests__/technical.test.ts`
|
|
Expected: FAIL
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
```typescript
|
|
// app/agents/technical.ts
|
|
import type { OpenRouterClient } from '../lib/openrouter'
|
|
import type { AnalystReport, AgentSignal } from '../types/agents'
|
|
|
|
interface TechnicalData {
|
|
prices: number[]
|
|
sma?: number
|
|
ema?: number
|
|
rsi?: number
|
|
macd?: number
|
|
}
|
|
|
|
export class TechnicalAnalyst {
|
|
private client: OpenRouterClient
|
|
private model: string
|
|
|
|
constructor(client: OpenRouterClient, model?: string) {
|
|
this.client = client
|
|
this.model = model || 'google/gemini-2.0-flash-exp:free'
|
|
}
|
|
|
|
async analyze(ticker: string, data: TechnicalData): Promise<AnalystReport> {
|
|
const messages = [
|
|
{
|
|
role: 'system',
|
|
content: `You are a technical analyst. Analyze indicators and provide signal.`
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: `Analyze ${ticker} technicals:\nSMA: ${data.sma}, RSI: ${data.rsi}, MACD: ${data.macd}`
|
|
}
|
|
]
|
|
|
|
const response = await this.client.createChatCompletion(messages, this.model)
|
|
const content = response.choices[0].message.content
|
|
|
|
const isBullish = content.toLowerCase().includes('bullish')
|
|
const isBearish = content.toLowerCase().includes('bearish')
|
|
const signal: AgentSignal['signal'] = isBullish ? 'bullish' : isBearish ? 'bearish' : 'neutral'
|
|
|
|
return {
|
|
analyst: 'technical',
|
|
report: content,
|
|
signal: {
|
|
agent: 'technical',
|
|
signal,
|
|
confidence: 0.7,
|
|
reasoning: content,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run tests to verify they pass**
|
|
|
|
Run: `npm run test -- app/agents/__tests__/technical.test.ts`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add app/agents/technical.ts app/agents/__tests__/technical.test.ts
|
|
git commit -m "feat: add technical analyst agent"
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 5: Sentiment Analyst Agent
|
|
|
|
### Task 5: Implement Sentiment Analyst Agent
|
|
|
|
**Files:**
|
|
- Create: `app/agents/sentiment.ts`
|
|
- Create: `app/agents/__tests__/sentiment.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```typescript
|
|
// app/agents/__tests__/sentiment.test.ts
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
import { SentimentAnalyst } from '../sentiment'
|
|
import type { OpenRouterClient } from '../../lib/openrouter'
|
|
|
|
describe('SentimentAnalyst', () => {
|
|
const mockClient = {
|
|
createChatCompletion: vi.fn().mockResolvedValue({
|
|
choices: [{ message: { content: 'Bullish: Positive sentiment from news' } }]
|
|
})
|
|
} as unknown as OpenRouterClient
|
|
|
|
it('should analyze sentiment from headlines', async () => {
|
|
const analyst = new SentimentAnalyst(mockClient)
|
|
const result = await analyst.analyze('AAPL', {
|
|
headlines: ['Apple beats earnings', 'New iPhone launch'],
|
|
source: 'news'
|
|
})
|
|
|
|
expect(result.analyst).toBe('sentiment')
|
|
expect(result.signal.signal).toBe('bullish')
|
|
})
|
|
})
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `npm run test -- app/agents/__tests__/sentiment.test.ts`
|
|
Expected: FAIL
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
```typescript
|
|
// app/agents/sentiment.ts
|
|
import type { OpenRouterClient } from '../lib/openrouter'
|
|
import type { AnalystReport, AgentSignal } from '../types/agents'
|
|
|
|
interface SentimentData {
|
|
headlines: string[]
|
|
source?: 'news' | 'social' | 'stocktwits'
|
|
}
|
|
|
|
export class SentimentAnalyst {
|
|
private client: OpenRouterClient
|
|
private model: string
|
|
|
|
constructor(client: OpenRouterClient, model?: string) {
|
|
this.client = client
|
|
this.model = model || 'google/gemini-2.0-flash-exp:free'
|
|
}
|
|
|
|
async analyze(ticker: string, data: SentimentData): Promise<AnalystReport> {
|
|
const messages = [
|
|
{
|
|
role: 'system',
|
|
content: `Analyze sentiment from headlines. Provide signal.`
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: `Analyze ${ticker} sentiment:\n${data.headlines.join('\n')}`
|
|
}
|
|
]
|
|
|
|
const response = await this.client.createChatCompletion(messages, this.model)
|
|
const content = response.choices[0].message.content
|
|
|
|
const isBullish = content.toLowerCase().includes('bullish')
|
|
const isBearish = content.toLowerCase().includes('bearish')
|
|
const signal: AgentSignal['signal'] = isBullish ? 'bullish' : isBearish ? 'bearish' : 'neutral'
|
|
|
|
return {
|
|
analyst: 'sentiment',
|
|
report: content,
|
|
signal: {
|
|
agent: 'sentiment',
|
|
signal,
|
|
confidence: 0.65,
|
|
reasoning: content,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run tests to verify they pass**
|
|
|
|
Run: `npm run test -- app/agents/__tests__/sentiment.test.ts`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add app/agents/sentiment.ts app/agents/__tests__/sentiment.test.ts
|
|
git commit -m "feat: add sentiment analyst agent"
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 6: Researcher Agents (Bullish/Bearish)
|
|
|
|
### Task 6: Implement Researcher Agents
|
|
|
|
**Files:**
|
|
- Create: `app/agents/researchers.ts`
|
|
- Create: `app/agents/__tests__/researchers.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```typescript
|
|
// app/agents/__tests__/researchers.test.ts
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
import { BullishResearcher, BearishResearcher } from '../researchers'
|
|
import type { AnalystReport } from '../../types/agents'
|
|
import type { OpenRouterClient } from '../../lib/openrouter'
|
|
|
|
describe('Researchers', () => {
|
|
const mockClient = {
|
|
createChatCompletion: vi.fn().mockResolvedValue({
|
|
choices: [{ message: { content: 'Bullish case: Strong fundamentals' } }]
|
|
})
|
|
} as unknown as OpenRouterClient
|
|
|
|
const sampleReports: AnalystReport[] = [
|
|
{
|
|
analyst: 'fundamentals',
|
|
report: 'Strong revenue growth',
|
|
signal: { agent: 'fundamentals', signal: 'bullish', confidence: 0.8, reasoning: '', timestamp: '' }
|
|
}
|
|
]
|
|
|
|
it('should create bullish researcher', async () => {
|
|
const researcher = new BullishResearcher(mockClient)
|
|
const result = await researcher.research('AAPL', sampleReports)
|
|
expect(result.researcher).toBe('bullish')
|
|
})
|
|
|
|
it('should create bearish researcher', async () => {
|
|
const researcher = new BearishResearcher(mockClient)
|
|
const result = await researcher.research('AAPL', sampleReports)
|
|
expect(result.researcher).toBe('bearish')
|
|
})
|
|
})
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `npm run test -- app/agents/__tests__/researchers.test.ts`
|
|
Expected: FAIL
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
```typescript
|
|
// app/agents/researchers.ts
|
|
import type { OpenRouterClient } from '../lib/openrouter'
|
|
import type { AnalystReport, DebateRound } from '../types/agents'
|
|
|
|
export class BullishResearcher {
|
|
private client: OpenRouterClient
|
|
private model: string
|
|
|
|
constructor(client: OpenRouterClient, model?: string) {
|
|
this.client = client
|
|
this.model = model || 'google/gemini-2.0-flash-exp:free'
|
|
}
|
|
|
|
async research(ticker: string, reports: AnalystReport[]): Promise<{
|
|
researcher: 'bullish'
|
|
view: string
|
|
debate: DebateRound
|
|
}> {
|
|
const messages = [
|
|
{ role: 'system', content: 'Synthesize bullish case from analyst reports.' },
|
|
{ role: 'user', content: `Create bullish thesis for ${ticker} using:\n${reports.map(r => r.report).join('\n')}` }
|
|
]
|
|
|
|
const response = await this.client.createChatCompletion(messages, this.model)
|
|
const view = response.choices[0].message.content
|
|
|
|
return {
|
|
researcher: 'bullish',
|
|
view,
|
|
debate: {
|
|
bullishView: view,
|
|
bearishView: '',
|
|
researcher: 'bullish'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export class BearishResearcher {
|
|
private client: OpenRouterClient
|
|
private model: string
|
|
|
|
constructor(client: OpenRouterClient, model?: string) {
|
|
this.client = client
|
|
this.model = model || 'google/gemini-2.0-flash-exp:free'
|
|
}
|
|
|
|
async research(ticker: string, reports: AnalystReport[]): Promise<{
|
|
researcher: 'bearish'
|
|
view: string
|
|
debate: DebateRound
|
|
}> {
|
|
const messages = [
|
|
{ role: 'system', content: 'Synthesize bearish case from analyst reports.' },
|
|
{ role: 'user', content: `Create bearish thesis for ${ticker} using:\n${reports.map(r => r.report).join('\n')}` }
|
|
]
|
|
|
|
const response = await this.client.createChatCompletion(messages, this.model)
|
|
const view = response.choices[0].message.content
|
|
|
|
return {
|
|
researcher: 'bearish',
|
|
view,
|
|
debate: {
|
|
bullishView: '',
|
|
bearishView: view,
|
|
researcher: 'bearish'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run tests to verify they pass**
|
|
|
|
Run: `npm run test -- app/agents/__tests__/researchers.test.ts`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add app/agents/researchers.ts app/agents/__tests__/researchers.test.ts
|
|
git commit -m "feat: add bullish and bearish researcher agents"
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 7: Trader Agent
|
|
|
|
### Task 7: Implement Trader Agent
|
|
|
|
**Files:**
|
|
- Create: `app/agents/trader.ts`
|
|
- Create: `app/agents/__tests__/trader.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```typescript
|
|
// app/agents/__tests__/trader.test.ts
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
import { Trader } from '../trader'
|
|
import type { OpenRouterClient } from '../../lib/openrouter'
|
|
import type { AnalystReport, DebateRound } from '../../types/agents'
|
|
|
|
describe('Trader', () => {
|
|
const mockClient = {
|
|
createChatCompletion: vi.fn().mockResolvedValue({
|
|
choices: [{ message: { content: 'Decision: BUY with high confidence' } }]
|
|
})
|
|
} as unknown as OpenRouterClient
|
|
|
|
it('should make trading decision', async () => {
|
|
const trader = new Trader(mockClient)
|
|
const reports: AnalystReport[] = [{
|
|
analyst: 'fundamentals',
|
|
report: 'Strong fundamentals',
|
|
signal: { agent: 'fundamentals', signal: 'bullish', confidence: 0.8, reasoning: '', timestamp: '' }
|
|
}]
|
|
const debates: DebateRound[] = [{
|
|
bullishView: 'Strong long term',
|
|
bearishView: 'Short term risks',
|
|
researcher: 'bullish'
|
|
}]
|
|
|
|
const decision = await trader.decide('AAPL', reports, debates)
|
|
expect(decision.action).toBe('buy')
|
|
})
|
|
})
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `npm run test -- app/agents/__tests__/trader.test.ts`
|
|
Expected: FAIL
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
```typescript
|
|
// app/agents/trader.ts
|
|
import type { OpenRouterClient } from '../lib/openrouter'
|
|
import type { AnalystReport, DebateRound, TradingDecision, AgentSignal } from '../types/agents'
|
|
|
|
export class Trader {
|
|
private client: OpenRouterClient
|
|
private model: string
|
|
|
|
constructor(client: OpenRouterClient, model?: string) {
|
|
this.client = client
|
|
this.model = model || 'google/gemini-2.0-flash-exp:free'
|
|
}
|
|
|
|
async decide(
|
|
ticker: string,
|
|
reports: AnalystReport[],
|
|
debates: DebateRound[]
|
|
): Promise<TradingDecision> {
|
|
const messages = [
|
|
{ role: 'system', content: 'Make trading decision based on all signals.' },
|
|
{ role: 'user', content: `Decide for ${ticker}:\nReports: ${JSON.stringify(reports)}\nDebates: ${JSON.stringify(debates)}` }
|
|
]
|
|
|
|
const response = await this.client.createChatCompletion(messages, this.model)
|
|
const content = response.choices[0].message.content
|
|
|
|
const lower = content.toLowerCase()
|
|
const action = lower.includes('buy') ? 'buy' : lower.includes('sell') ? 'sell' : 'hold'
|
|
|
|
const agentSignals: AgentSignal[] = reports.map(r => r.signal)
|
|
|
|
return {
|
|
action,
|
|
confidence: 0.7,
|
|
reasoning: content,
|
|
agentSignals,
|
|
debateRounds: debates
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run tests to verify they pass**
|
|
|
|
Run: `npm run test -- app/agents/__tests__/trader.test.ts`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add app/agents/trader.ts app/agents/__tests__/trader.test.ts
|
|
git commit -m "feat: add trader agent"
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 8: Trading Graph Orchestrator
|
|
|
|
### Task 8: Create Trading Graph Orchestrator
|
|
|
|
**Files:**
|
|
- Create: `app/agents/tradingGraph.ts`
|
|
- Create: `app/agents/__tests__/tradingGraph.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```typescript
|
|
// app/agents/__tests__/tradingGraph.test.ts
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
import { TradingGraph } from '../tradingGraph'
|
|
import type { OpenRouterClient } from '../../lib/openrouter'
|
|
|
|
describe('TradingGraph', () => {
|
|
const mockClient = {
|
|
createChatCompletion: vi.fn().mockResolvedValue({
|
|
choices: [{ message: { content: 'Bullish signal' } }]
|
|
})
|
|
} as unknown as OpenRouterClient
|
|
|
|
it('should run full analysis', async () => {
|
|
const graph = new TradingGraph(mockClient)
|
|
const decision = await graph.propagate('AAPL', {
|
|
ticker: 'AAPL',
|
|
date: '2026-01-15'
|
|
})
|
|
|
|
expect(decision).toHaveProperty('action')
|
|
expect(decision).toHaveProperty('confidence')
|
|
})
|
|
})
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `npm run test -- app/agents/__tests__/tradingGraph.test.ts`
|
|
Expected: FAIL
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
```typescript
|
|
// app/agents/tradingGraph.ts
|
|
import { FundamentalsAnalyst } from './fundamentals'
|
|
import { TechnicalAnalyst } from './technical'
|
|
import { SentimentAnalyst } from './sentiment'
|
|
import { BullishResearcher, BearishResearcher } from './researchers'
|
|
import { Trader } from './trader'
|
|
import type { OpenRouterClient } from '../lib/openrouter'
|
|
import type { TradingDecision } from '../types/agents'
|
|
|
|
interface AnalysisInput {
|
|
ticker: string
|
|
date: string
|
|
}
|
|
|
|
export class TradingGraph {
|
|
private client: OpenRouterClient
|
|
private fundamentals: FundamentalsAnalyst
|
|
private technical: TechnicalAnalyst
|
|
private sentiment: SentimentAnalyst
|
|
private bullishResearcher: BullishResearcher
|
|
private bearishResearcher: BearishResearcher
|
|
private trader: Trader
|
|
|
|
constructor(client: OpenRouterClient) {
|
|
this.client = client
|
|
const model = 'google/gemini-2.0-flash-exp:free'
|
|
this.fundamentals = new FundamentalsAnalyst(client, { model })
|
|
this.technical = new TechnicalAnalyst(client, model)
|
|
this.sentiment = new SentimentAnalyst(client, model)
|
|
this.bullishResearcher = new BullishResearcher(client, model)
|
|
this.bearishResearcher = new BearishResearcher(client, model)
|
|
this.trader = new Trader(client, model)
|
|
}
|
|
|
|
async propagate(ticker: string, input: AnalysisInput): Promise<TradingDecision> {
|
|
// Analyst phase
|
|
const reports = await this.runAnalysts(ticker)
|
|
|
|
// Researcher phase
|
|
const debates = await this.runDebate(ticker, reports)
|
|
|
|
// Trader phase
|
|
const decision = await this.trader.decide(ticker, reports, debates)
|
|
|
|
return decision
|
|
}
|
|
|
|
private async runAnalysts(ticker: string) {
|
|
// Placeholder - would integrate with real data sources
|
|
return []
|
|
}
|
|
|
|
private async runDebate(ticker: string, reports: any[]) {
|
|
const bullish = await this.bullishResearcher.research(ticker, reports)
|
|
return [bullish.debate]
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run tests to verify they pass**
|
|
|
|
Run: `npm run test -- app/agents/__tests__/tradingGraph.test.ts`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add app/agents/tradingGraph.ts app/agents/__tests__/tradingGraph.test.ts
|
|
git commit -m "feat: add trading graph orchestrator"
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 9: API Routes for Analysis
|
|
|
|
### Task 9: Create API Routes
|
|
|
|
**Files:**
|
|
- Create: `app/routes/api/analyze.ts`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
```typescript
|
|
// tests/api/analyze.test.ts
|
|
import { describe, it, expect } from 'vitest'
|
|
|
|
describe('Analyze API', () => {
|
|
it('should be defined', () => {
|
|
expect(true).toBe(true)
|
|
})
|
|
})
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `npm run test -- tests/api/analyze.test.ts`
|
|
Expected: PASS (placeholder)
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
```typescript
|
|
// app/routes/api/analyze.ts
|
|
import { TradingGraph } from '../../agents/tradingGraph'
|
|
import { OpenRouterClient } from '../../lib/openrouter'
|
|
|
|
export async function action({ request }: { request: Request }) {
|
|
const formData = await request.formData()
|
|
const ticker = formData.get('ticker') as string
|
|
const date = formData.get('date') as string
|
|
|
|
const apiKey = process.env.OPENROUTER_API_KEY!
|
|
const client = new OpenRouterClient(apiKey)
|
|
const graph = new TradingGraph(client)
|
|
|
|
const decision = await graph.propagate(ticker, { ticker, date })
|
|
return Response.json(decision)
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run tests to verify they pass**
|
|
|
|
Run: `npm run test -- tests/api/analyze.test.ts`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add app/routes/api/analyze.ts tests/api/analyze.test.ts
|
|
git commit -m "feat: add analysis API route"
|
|
```
|
|
|
|
---
|
|
|
|
Plan complete. Two execution options:
|
|
|
|
**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks
|
|
**2. Inline Execution** - Execute tasks in this session with checkpoints
|
|
|
|
Which approach? |