Files
AITrader/docs/superpowers/plans/2026-05-14-multiagent-trading-framework.md
henry 3340fd11ca feat: add stock database with prisma for portfolio persistence
- 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
2026-05-14 10:23:56 +02:00

27 KiB

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

// 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
// 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
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

// 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
// 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
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

// 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
// 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
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

// 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
// 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
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

// 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
// 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
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

// 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
// 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
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

// 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
// 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
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

// 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
// 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
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

// 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
// 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
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?