Connecting...
Last Update: --:--:--

Trading Agent Implementation Plan

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build a production-grade autonomous trading agent on Cloudflare Workers + Durable Objects that trades across Crypto.com and Coinbase with ML-driven signals and mandatory risk guardrails.

Architecture: TypeScript Worker with 6 Durable Objects for state, unified exchange adapters for Crypto.com and Coinbase, cron-driven decision loop with smart order routing, React dashboard on Cloudflare Pages, Python ML inference service on Fly.io, and an MCP server for Claude integration.

Tech Stack: TypeScript, Cloudflare Workers + Durable Objects + KV, Hono (lightweight Worker router), Vitest (testing), Python + FastAPI (ML service), React + Vite + Tailwind (dashboard)

Spec: docs/superpowers/specs/2026-03-14-trading-agent-design.md


File Structure

The new trading-agent/ directory lives at the repo root alongside the existing fintech-dashboard/.

trading-agent/
├── wrangler.toml
├── package.json
├── tsconfig.json
├── vitest.config.ts
├── src/
│   ├── index.ts                      # Worker entry + cron handler
│   ├── router.ts                     # Hono API routes
│   ├── auth.ts                       # JWT + CORS + API key validation
│   ├── env.ts                        # Env type definition (secrets, bindings)
│   ├── exchanges/
│   │   ├── types.ts                  # Exchange interface + all shared types
│   │   ├── crypto-com.ts             # Crypto.com adapter
│   │   ├── coinbase.ts               # Coinbase adapter
│   │   ├── paper-trading.ts          # PaperTradingAdapter wrapper
│   │   └── order-router.ts           # Smart order router
│   ├── durable-objects/
│   │   ├── agent-state.ts            # AgentStateDO
│   │   ├── order-manager.ts          # OrderManagerDO
│   │   ├── guardrails.ts             # GuardrailsDO
│   │   ├── market-data.ts            # MarketDataDO
│   │   ├── trading-pairs.ts          # TradingPairsDO
│   │   └── audit-log.ts              # AuditLogDO
│   ├── agent/
│   │   ├── decision-loop.ts          # Main trading loop
│   │   ├── signal-client.ts          # ML inference client (5s timeout)
│   │   └── scheduler.ts              # Cron dispatch
│   ├── api/
│   │   ├── status.ts                 # GET /api/status
│   │   ├── portfolio.ts              # GET /api/portfolio
│   │   ├── trades.ts                 # GET /api/trades
│   │   ├── orders.ts                 # GET /api/orders/open, DELETE /api/orders/:id
│   │   ├── signals.ts                # GET /api/signals
│   │   ├── guardrails.ts             # GET/PUT /api/guardrails
│   │   ├── pairs.ts                  # GET/PUT /api/pairs
│   │   ├── kill-switch.ts            # PUT /api/kill-switch
│   │   ├── events.ts                 # GET /api/events
│   │   ├── market.ts                 # GET /api/market/:pair
│   │   └── websocket.ts              # WS /api/ws real-time updates
│   └── mcp/
│       └── server.ts                 # MCP tool definitions
├── ml-service/
│   ├── Dockerfile
│   ├── fly.toml
│   ├── requirements.txt
│   ├── main.py                       # FastAPI app
│   ├── predict.py                    # Prediction logic
│   ├── models/                       # Trained model artifacts
│   └── tests/
│       └── test_predict.py
└── tests/
    ├── exchanges/
    │   ├── types.test.ts
    │   ├── crypto-com.test.ts
    │   ├── coinbase.test.ts
    │   ├── paper-trading.test.ts
    │   └── order-router.test.ts
    ├── durable-objects/
    │   ├── agent-state.test.ts
    │   ├── order-manager.test.ts
    │   ├── guardrails.test.ts
    │   ├── market-data.test.ts
    │   ├── trading-pairs.test.ts
    │   └── audit-log.test.ts
    ├── agent/
    │   ├── decision-loop.test.ts
    │   └── signal-client.test.ts
    └── api/
        ├── status.test.ts
        ├── portfolio.test.ts
        └── guardrails.test.ts

Phase 1: Foundation — Project Scaffold + Types + Exchange Interface

This phase produces: a buildable TypeScript project with all shared types, the Exchange interface, and test infrastructure.


Task 1: Project Scaffold

Files:

{
  "name": "trading-agent",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "wrangler dev",
    "build": "wrangler deploy --dry-run",
    "test": "vitest run",
    "test:watch": "vitest",
    "deploy": "wrangler deploy",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "hono": "^4.0.0"
  },
  "devDependencies": {
    "@cloudflare/workers-types": "^4.0.0",
    "typescript": "^5.4.0",
    "vitest": "^2.0.0",
    "wrangler": "^3.0.0"
  }
}
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "bundler",
    "lib": ["ES2022"],
    "types": ["@cloudflare/workers-types"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules", "dist", "tests"]
}
import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['tests/**/*.test.ts'],
    coverage: {
      provider: 'v8',
      include: ['src/**/*.ts'],
    },
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});
name = "trading-agent"
main = "src/index.ts"
compatibility_date = "2026-03-01"
compatibility_flags = ["nodejs_compat"]

[vars]
TRADING_MODE = "paper"
DASHBOARD_ORIGIN = "https://letsgetcrypto.pages.dev"

[[kv_namespaces]]
binding = "TRADE_HISTORY_KV"
id = ""  # Fill after `wrangler kv:namespace create TRADE_HISTORY_KV`

[[kv_namespaces]]
binding = "AUDIT_LOG_KV"
id = ""  # Fill after `wrangler kv:namespace create AUDIT_LOG_KV`

[durable_objects]
bindings = [
  { name = "AGENT_STATE", class_name = "AgentStateDO" },
  { name = "ORDER_MANAGER", class_name = "OrderManagerDO" },
  { name = "GUARDRAILS", class_name = "GuardrailsDO" },
  { name = "MARKET_DATA", class_name = "MarketDataDO" },
  { name = "TRADING_PAIRS", class_name = "TradingPairsDO" },
  { name = "AUDIT_LOG", class_name = "AuditLogDO" },
]

[[migrations]]
tag = "v1"
new_classes = [
  "AgentStateDO",
  "OrderManagerDO",
  "GuardrailsDO",
  "MarketDataDO",
  "TradingPairsDO",
  "AuditLogDO",
]

[triggers]
crons = [
  "* * * * *",       # Every 1 min: main trading loop
  "*/5 * * * *",     # Every 5 min: rebalance check
  "0 * * * *",       # Every 1 hour: pair discovery
  "0 0 * * *",       # Every 24 hours: daily report
]
export interface Env {
  // Secrets
  CRYPTO_COM_API_KEY: string;
  CRYPTO_COM_API_SECRET: string;
  COINBASE_API_KEY: string;
  COINBASE_API_SECRET: string;
  ML_SERVICE_URL: string;
  ML_SERVICE_KEY: string;
  DASHBOARD_API_KEY: string;
  JWT_SECRET: string;

  // Vars
  TRADING_MODE: 'live' | 'paper';
  DASHBOARD_ORIGIN: string;

  // KV
  TRADE_HISTORY_KV: KVNamespace;
  AUDIT_LOG_KV: KVNamespace;

  // Durable Objects
  AGENT_STATE: DurableObjectNamespace;
  ORDER_MANAGER: DurableObjectNamespace;
  GUARDRAILS: DurableObjectNamespace;
  MARKET_DATA: DurableObjectNamespace;
  TRADING_PAIRS: DurableObjectNamespace;
  AUDIT_LOG: DurableObjectNamespace;
}

Run: cd trading-agent && npm install Expected: node_modules/ created, package-lock.json generated.

Run: cd trading-agent && npx tsc --noEmit Expected: No errors (env.ts compiles cleanly).

git add trading-agent/
git commit -m "feat: scaffold trading-agent project with Cloudflare Workers config"

Task 2: Shared Types

Files:

// tests/exchanges/types.test.ts
import { describe, it, expect } from 'vitest';
import type {
  Ticker, Orderbook, Candle, Trade, TradingPair,
  Balance, Order, Signal, AuditEvent, Exchange,
} from '@/exchanges/types';

describe('Exchange Types', () => {
  it('should create a valid Ticker', () => {
    const ticker: Ticker = {
      pair: 'BTC-USDT',
      bid: 62380,
      ask: 62400,
      last: 62390,
      volume24h: 1500.5,
      timestamp: '2026-03-14T10:30:00Z',
    };
    expect(ticker.pair).toBe('BTC-USDT');
    expect(ticker.ask).toBeGreaterThan(ticker.bid);
  });

  it('should create a valid Order with clientOrderId and fees', () => {
    const order: Order = {
      id: 'exchange-123',
      clientOrderId: '550e8400-e29b-41d4-a716-446655440000',
      pair: 'ETH-USDT',
      side: 'buy',
      type: 'market',
      status: 'filled',
      price: null,
      filledPrice: 3200,
      qty: 1.0,
      filledQty: 1.0,
      fee: 2.56,
      feeAsset: 'USDT',
      exchange: 'coinbase',
      createdAt: '2026-03-14T10:00:00Z',
      updatedAt: '2026-03-14T10:00:01Z',
    };
    expect(order.clientOrderId).toHaveLength(36);
    expect(order.fee).toBe(2.56);
    expect(order.feeAsset).toBe('USDT');
  });

  it('should create a valid Signal', () => {
    const signal: Signal = {
      action: 'BUY',
      confidence: 0.85,
      pair: 'BTC-USDT',
      timestamp: '2026-03-14T10:30:00Z',
    };
    expect(signal.confidence).toBeGreaterThanOrEqual(0);
    expect(signal.confidence).toBeLessThanOrEqual(1);
  });

  it('should create a valid AuditEvent', () => {
    const event: AuditEvent = {
      id: 'evt-001',
      type: 'trade_executed',
      actor: 'system',
      details: { pair: 'BTC-USDT', side: 'buy', qty: 0.1 },
      timestamp: '2026-03-14T10:30:00Z',
    };
    expect(event.actor).toBe('system');
  });

  it('should validate TradingPair includes priceStep', () => {
    const pair: TradingPair = {
      symbol: 'BTC-USDT',
      base: 'BTC',
      quote: 'USDT',
      minQty: 0.00001,
      maxQty: 100,
      qtyStep: 0.00001,
      priceStep: 0.01,
      minNotional: 10,
    };
    expect(pair.priceStep).toBe(0.01);
  });
});

Run: cd trading-agent && npx vitest run tests/exchanges/types.test.ts Expected: FAIL — module @/exchanges/types not found.

// src/exchanges/types.ts

// === Market Data Types ===

export interface Ticker {
  pair: string;
  bid: number;
  ask: number;
  last: number;
  volume24h: number;
  timestamp: string;
}

export interface Orderbook {
  pair: string;
  bids: [number, number][];
  asks: [number, number][];
  timestamp: string;
}

export interface Candle {
  open: number;
  high: number;
  low: number;
  close: number;
  volume: number;
  timestamp: string;
}

export interface Trade {
  id: string;
  pair: string;
  side: 'buy' | 'sell';
  price: number;
  qty: number;
  timestamp: string;
}

export interface TradingPair {
  symbol: string;
  base: string;
  quote: string;
  minQty: number;
  maxQty: number;
  qtyStep: number;
  priceStep: number;
  minNotional: number;
}

// === Account Types ===

export interface Balance {
  asset: string;
  available: number;
  locked: number;
  total: number;
}

// === Order Types ===

export type OrderSide = 'buy' | 'sell';
export type OrderType = 'limit' | 'market';
export type OrderStatus = 'open' | 'filled' | 'partially_filled' | 'cancelled';
export type ExchangeName = 'crypto.com' | 'coinbase';

export interface Order {
  id: string;
  clientOrderId: string;
  pair: string;
  side: OrderSide;
  type: OrderType;
  status: OrderStatus;
  price: number | null;
  filledPrice: number | null;
  qty: number;
  filledQty: number;
  fee: number | null;
  feeAsset: string | null;
  exchange: ExchangeName;
  createdAt: string;
  updatedAt: string;
}

// === Signal Types ===

export interface Signal {
  action: 'BUY' | 'SELL' | 'HOLD';
  confidence: number;
  pair: string;
  timestamp: string;
}

// === Audit Types ===

export type AuditEventType =
  | 'trade_executed'
  | 'trade_rejected'
  | 'guardrail_triggered'
  | 'kill_switch'
  | 'config_changed'
  | 'pair_changed'
  | 'balance_sync'
  | 'agent_status_change';

export type AuditActor = 'system' | 'user' | 'mcp';

export interface AuditEvent {
  id: string;
  type: AuditEventType;
  actor: AuditActor;
  details: Record<string, unknown>;
  timestamp: string;
}

// === Exchange Interface ===

export interface FeeSchedule {
  makerRate: number;
  takerRate: number;
}

export interface Exchange {
  readonly name: ExchangeName;

  // Market Data
  getTicker(pair: string): Promise<Ticker>;
  getOrderbook(pair: string, depth?: number): Promise<Orderbook>;
  getCandles(pair: string, interval: string, limit?: number): Promise<Candle[]>;
  getTrades(pair: string, limit?: number): Promise<Trade[]>;
  getAvailablePairs(): Promise<TradingPair[]>;

  // Account
  getBalances(): Promise<Balance[]>;
  getBalance(asset: string): Promise<Balance>;
  getFeeSchedule(): Promise<FeeSchedule>;

  // Orders (clientOrderId mandatory for idempotency)
  placeLimitOrder(pair: string, side: OrderSide, price: number, qty: number, clientOrderId: string): Promise<Order>;
  placeMarketOrder(pair: string, side: OrderSide, qty: number, clientOrderId: string): Promise<Order>;
  cancelOrder(orderId: string): Promise<void>;
  getOrder(orderId: string): Promise<Order>;
  getOpenOrders(pair?: string): Promise<Order[]>;

  // WebSocket
  subscribeToTicker(pair: string, callback: (ticker: Ticker) => void): void;
  subscribeToOrderUpdates(callback: (order: Order) => void): void;
  disconnect(): void;

  // Normalization
  normalizePair(pair: string): string;
}

Run: cd trading-agent && npx vitest run tests/exchanges/types.test.ts Expected: ALL PASS.

git add trading-agent/src/exchanges/types.ts trading-agent/tests/exchanges/types.test.ts
git commit -m "feat: add Exchange interface and all shared types"

Phase 2: Durable Objects — State Layer

This phase produces: all 6 Durable Objects with full test coverage. Each DO is independently testable.


Task 3: GuardrailsDO

Files:

// tests/durable-objects/guardrails.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import {
  GuardrailsConfig,
  GuardrailsState,
  defaultGuardrailsConfig,
  checkGuardrails,
  type TradeRequest,
  type PortfolioSnapshot,
} from '@/durable-objects/guardrails';

describe('GuardrailsDO Logic', () => {
  let config: GuardrailsConfig;
  let state: GuardrailsState;

  beforeEach(() => {
    config = { ...defaultGuardrailsConfig };
    state = {
      todayDate: '2026-03-14',
      todayRealizedPnL: 0,
      trailing7DayPeak: 10000,
      recentPnLWindow: [],
      lastTradePerPair: {},
      recentTradeTimestamps: [],
      pausedUntil: null,
      haltReason: null,
    };
  });

  it('should reject when kill switch is on', () => {
    config.killSwitch = true;
    const result = checkGuardrails(
      { pair: 'BTC-USDT', side: 'buy', qty: 0.01, notionalUSD: 100, exchange: 'coinbase' },
      config,
      state,
      { totalUSD: 10000, positions: {} },
    );
    expect(result.allowed).toBe(false);
    expect(result.reason).toContain('kill switch');
  });

  it('should reject when trade exceeds max size (absolute)', () => {
    const result = checkGuardrails(
      { pair: 'BTC-USDT', side: 'buy', qty: 1, notionalUSD: 600, exchange: 'coinbase' },
      config,
      state,
      { totalUSD: 100000, positions: {} },
    );
    expect(result.allowed).toBe(false);
    expect(result.reason).toContain('max trade size');
  });

  it('should reject when trade exceeds max size (percentage)', () => {
    const result = checkGuardrails(
      { pair: 'BTC-USDT', side: 'buy', qty: 0.1, notionalUSD: 300, exchange: 'coinbase' },
      config,
      state,
      { totalUSD: 10000, positions: {} },
    );
    // 2% of 10000 = 200, trade is 300 → reject
    expect(result.allowed).toBe(false);
    expect(result.reason).toContain('max trade size');
  });

  it('should reject when daily loss limit exceeded', () => {
    state.todayRealizedPnL = -600; // -6% of 10000
    const result = checkGuardrails(
      { pair: 'BTC-USDT', side: 'buy', qty: 0.01, notionalUSD: 100, exchange: 'coinbase' },
      config,
      state,
      { totalUSD: 9400, positions: {} },
    );
    expect(result.allowed).toBe(false);
    expect(result.reason).toContain('daily loss');
  });

  it('should reject when rolling drawdown exceeded', () => {
    state.trailing7DayPeak = 10000;
    const result = checkGuardrails(
      { pair: 'BTC-USDT', side: 'buy', qty: 0.01, notionalUSD: 100, exchange: 'coinbase' },
      config,
      state,
      { totalUSD: 8900, positions: {} }, // 11% drawdown
    );
    expect(result.allowed).toBe(false);
    expect(result.reason).toContain('rolling drawdown');
  });

  it('should reject when pair not whitelisted', () => {
    const result = checkGuardrails(
      { pair: 'SHIB-USDT', side: 'buy', qty: 1000, notionalUSD: 50, exchange: 'coinbase' },
      config,
      state,
      { totalUSD: 10000, positions: {} },
    );
    expect(result.allowed).toBe(false);
    expect(result.reason).toContain('whitelist');
  });

  it('should reject when per-pair cooldown not elapsed', () => {
    const now = new Date('2026-03-14T10:30:30Z');
    state.lastTradePerPair['BTC-USDT'] = '2026-03-14T10:30:00Z'; // 30s ago
    const result = checkGuardrails(
      { pair: 'BTC-USDT', side: 'buy', qty: 0.01, notionalUSD: 100, exchange: 'coinbase' },
      config,
      state,
      { totalUSD: 10000, positions: {} },
      now,
    );
    expect(result.allowed).toBe(false);
    expect(result.reason).toContain('cooldown');
  });

  it('should reject when global rate limit exceeded', () => {
    const now = new Date('2026-03-14T10:30:00Z');
    // 5 trades in last 5 minutes
    state.recentTradeTimestamps = [
      '2026-03-14T10:26:00Z',
      '2026-03-14T10:27:00Z',
      '2026-03-14T10:28:00Z',
      '2026-03-14T10:29:00Z',
      '2026-03-14T10:29:30Z',
    ];
    const result = checkGuardrails(
      { pair: 'ETH-USDT', side: 'buy', qty: 0.5, notionalUSD: 100, exchange: 'coinbase' },
      config,
      state,
      { totalUSD: 10000, positions: {} },
      now,
    );
    expect(result.allowed).toBe(false);
    expect(result.reason).toContain('global rate limit');
  });

  it('should reject when concentration limit exceeded', () => {
    const result = checkGuardrails(
      { pair: 'BTC-USDT', side: 'buy', qty: 0.05, notionalUSD: 2000, exchange: 'coinbase' },
      config,
      state,
      {
        totalUSD: 10000,
        positions: { 'BTC-USDT': { valueUSD: 2000 } },
      },
    );
    // Current 2000 + new 2000 = 4000, 40% of 10000 → reject (limit 25%)
    expect(result.allowed).toBe(false);
    expect(result.reason).toContain('concentration');
  });

  it('should allow a valid trade', () => {
    const result = checkGuardrails(
      { pair: 'BTC-USDT', side: 'buy', qty: 0.001, notionalUSD: 62, exchange: 'coinbase' },
      config,
      state,
      { totalUSD: 10000, positions: {} },
    );
    expect(result.allowed).toBe(true);
    expect(result.reason).toBeNull();
  });
});

Run: cd trading-agent && npx vitest run tests/durable-objects/guardrails.test.ts Expected: FAIL — module not found.

// src/durable-objects/guardrails.ts

export interface GuardrailsConfig {
  maxTradeUSD: number;
  maxTradePercent: number;
  dailyLossLimit: number;
  rollingDrawdownLimit: number;
  rollingDrawdownWindowDays: number;
  rateOfLossLimit: number;
  rateOfLossWindowMinutes: number;
  rateOfLossPauseMinutes: number;
  maxConcentration: number;
  cooldownSeconds: number;
  globalRateLimitTrades: number;
  globalRateLimitWindowMinutes: number;
  stablecoins: string[];
  killSwitch: boolean;
  approvedPairs: string[];
}

export const defaultGuardrailsConfig: GuardrailsConfig = {
  maxTradeUSD: 500,
  maxTradePercent: 0.02,
  dailyLossLimit: 0.05,
  rollingDrawdownLimit: 0.10,
  rollingDrawdownWindowDays: 7,
  rateOfLossLimit: 0.02,
  rateOfLossWindowMinutes: 30,
  rateOfLossPauseMinutes: 15,
  maxConcentration: 0.25,
  cooldownSeconds: 60,
  globalRateLimitTrades: 5,
  globalRateLimitWindowMinutes: 5,
  stablecoins: ['USDT', 'USDC', 'DAI', 'BUSD'],
  killSwitch: false,
  approvedPairs: [
    'BTC-USDT', 'ETH-USDT', 'SOL-USDT', 'BNB-USDT', 'XRP-USDT',
    'ADA-USDT', 'DOGE-USDT', 'AVAX-USDT', 'DOT-USDT', 'MATIC-USDT',
  ],
};

export interface GuardrailsState {
  todayDate: string;
  todayRealizedPnL: number;
  trailing7DayPeak: number;
  recentPnLWindow: Array<{ pnl: number; timestamp: string }>;
  lastTradePerPair: Record<string, string>;
  recentTradeTimestamps: string[];
  pausedUntil: string | null;
  haltReason: string | null;
}

export interface TradeRequest {
  pair: string;
  side: 'buy' | 'sell';
  qty: number;
  notionalUSD: number;
  exchange: 'crypto.com' | 'coinbase';
}

export interface PortfolioSnapshot {
  totalUSD: number;
  dayOpenBalance?: number;  // Used for daily loss limit calculation
  positions: Record<string, { valueUSD: number }>;
}

export interface GuardrailResult {
  allowed: boolean;
  reason: string | null;
  autoHalt: boolean;
}

export function checkGuardrails(
  trade: TradeRequest,
  config: GuardrailsConfig,
  state: GuardrailsState,
  portfolio: PortfolioSnapshot,
  now: Date = new Date(),
): GuardrailResult {
  // 1. Kill switch
  if (config.killSwitch) {
    return { allowed: false, reason: 'Rejected: kill switch is active', autoHalt: false };
  }

  // 2. Max trade size
  const maxByPercent = portfolio.totalUSD * config.maxTradePercent;
  const effectiveMax = Math.min(config.maxTradeUSD, maxByPercent);
  if (trade.notionalUSD > effectiveMax) {
    return {
      allowed: false,
      reason: `Rejected: max trade size exceeded ($${trade.notionalUSD} > $${effectiveMax.toFixed(2)})`,
      autoHalt: false,
    };
  }

  // 3. Daily loss limit (uses day-open balance, not current)
  const dayOpenBalance = portfolio.dayOpenBalance ?? portfolio.totalUSD;
  const dailyLossThreshold = dayOpenBalance * config.dailyLossLimit;
  if (Math.abs(state.todayRealizedPnL) > dailyLossThreshold && state.todayRealizedPnL < 0) {
    return {
      allowed: false,
      reason: `Rejected: daily loss limit exceeded ($${state.todayRealizedPnL.toFixed(2)})`,
      autoHalt: true,
    };
  }

  // 4. Rolling drawdown
  if (state.trailing7DayPeak > 0) {
    const drawdown = (state.trailing7DayPeak - portfolio.totalUSD) / state.trailing7DayPeak;
    if (drawdown > config.rollingDrawdownLimit) {
      return {
        allowed: false,
        reason: `Rejected: rolling drawdown exceeded (${(drawdown * 100).toFixed(1)}%)`,
        autoHalt: true,
      };
    }
  }

  // 5. Rate-of-loss + pause enforcement
  if (state.pausedUntil && new Date(state.pausedUntil) > now) {
    return {
      allowed: false,
      reason: `Rejected: rate-of-loss pause active until ${state.pausedUntil}`,
      autoHalt: false,
    };
  }
  const windowStart = new Date(now.getTime() - config.rateOfLossWindowMinutes * 60 * 1000);
  const recentLoss = state.recentPnLWindow
    .filter(e => new Date(e.timestamp) >= windowStart)
    .reduce((sum, e) => sum + e.pnl, 0);
  if (portfolio.totalUSD > 0 && Math.abs(recentLoss) / portfolio.totalUSD > config.rateOfLossLimit && recentLoss < 0) {
    return {
      allowed: false,
      reason: `Rejected: rate-of-loss exceeded (${((recentLoss / portfolio.totalUSD) * 100).toFixed(1)}% in ${config.rateOfLossWindowMinutes}min)`,
      autoHalt: false,
    };
  }

  // 6. Pair whitelist
  if (!config.approvedPairs.includes(trade.pair)) {
    return { allowed: false, reason: `Rejected: ${trade.pair} not in whitelist`, autoHalt: false };
  }

  // 7. Per-pair cooldown
  const lastTrade = state.lastTradePerPair[trade.pair];
  if (lastTrade) {
    const elapsed = (now.getTime() - new Date(lastTrade).getTime()) / 1000;
    if (elapsed < config.cooldownSeconds) {
      return {
        allowed: false,
        reason: `Rejected: cooldown not elapsed for ${trade.pair} (${elapsed.toFixed(0)}s < ${config.cooldownSeconds}s)`,
        autoHalt: false,
      };
    }
  }

  // 8. Global rate limit
  const rateLimitWindow = new Date(now.getTime() - config.globalRateLimitWindowMinutes * 60 * 1000);
  const recentTrades = state.recentTradeTimestamps.filter(ts => new Date(ts) >= rateLimitWindow);
  if (recentTrades.length >= config.globalRateLimitTrades) {
    return {
      allowed: false,
      reason: `Rejected: global rate limit (${recentTrades.length}/${config.globalRateLimitTrades} trades in ${config.globalRateLimitWindowMinutes}min)`,
      autoHalt: false,
    };
  }

  // 9. Portfolio concentration (exclude stablecoins)
  const baseAsset = trade.pair.split('-')[0];
  if (!config.stablecoins.includes(baseAsset)) {
    const currentValue = portfolio.positions[trade.pair]?.valueUSD ?? 0;
    const newValue = currentValue + trade.notionalUSD;
    const concentration = newValue / portfolio.totalUSD;
    if (concentration > config.maxConcentration) {
      return {
        allowed: false,
        reason: `Rejected: concentration limit (${(concentration * 100).toFixed(1)}% > ${config.maxConcentration * 100}%)`,
        autoHalt: false,
      };
    }
  }

  // 10. Sufficient balance — checked by caller before invoking guardrails

  return { allowed: true, reason: null, autoHalt: false };
}

Run: cd trading-agent && npx vitest run tests/durable-objects/guardrails.test.ts Expected: ALL PASS.

git add trading-agent/src/durable-objects/guardrails.ts trading-agent/tests/durable-objects/guardrails.test.ts
git commit -m "feat: implement GuardrailsDO 10-check risk pipeline with tests"

Task 4: AgentStateDO

Files:

// tests/durable-objects/agent-state.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import {
  AgentStateData,
  defaultAgentState,
  updatePosition,
  syncBalances,
  updateDailyPnL,
} from '@/durable-objects/agent-state';

describe('AgentStateDO Logic', () => {
  let state: AgentStateData;

  beforeEach(() => {
    state = defaultAgentState();
  });

  it('should initialize with empty positions and running status', () => {
    expect(state.positions).toEqual({});
    expect(state.status).toBe('running');
    expect(state.totalPnL.initialDeposit).toBe(0);
  });

  it('should update position on buy (new position)', () => {
    const updated = updatePosition(state, 'BTC-USDT', 'coinbase', 'buy', 0.1, 62000);
    expect(updated.positions['BTC-USDT']['coinbase'].qty).toBe(0.1);
    expect(updated.positions['BTC-USDT']['coinbase'].avgEntry).toBe(62000);
  });

  it('should update position on buy (add to existing)', () => {
    let s = updatePosition(state, 'BTC-USDT', 'coinbase', 'buy', 0.1, 62000);
    s = updatePosition(s, 'BTC-USDT', 'coinbase', 'buy', 0.1, 64000);
    expect(s.positions['BTC-USDT']['coinbase'].qty).toBe(0.2);
    expect(s.positions['BTC-USDT']['coinbase'].avgEntry).toBe(63000); // weighted avg
  });

  it('should update position on sell (reduce)', () => {
    let s = updatePosition(state, 'BTC-USDT', 'coinbase', 'buy', 0.2, 62000);
    s = updatePosition(s, 'BTC-USDT', 'coinbase', 'sell', 0.1, 64000);
    expect(s.positions['BTC-USDT']['coinbase'].qty).toBe(0.1);
    expect(s.positions['BTC-USDT']['coinbase'].avgEntry).toBe(62000); // unchanged on sell
  });

  it('should track same asset on different exchanges independently', () => {
    let s = updatePosition(state, 'BTC-USDT', 'coinbase', 'buy', 0.1, 62000);
    s = updatePosition(s, 'BTC-USDT', 'crypto.com', 'buy', 0.2, 61500);
    expect(s.positions['BTC-USDT']['coinbase'].qty).toBe(0.1);
    expect(s.positions['BTC-USDT']['crypto.com'].qty).toBe(0.2);
  });

  it('should sync balances from exchange data', () => {
    const updated = syncBalances(state, 'coinbase', [
      { asset: 'USDT', available: 5000, locked: 100, total: 5100 },
      { asset: 'BTC', available: 0.5, locked: 0, total: 0.5 },
    ]);
    expect(updated.balances['coinbase']['USDT']).toBe(5100);
    expect(updated.balances['coinbase']['BTC']).toBe(0.5);
  });

  it('should update daily PnL', () => {
    state.dailyPnL = { date: '2026-03-14', startBalance: 10000, current: 10000 };
    const updated = updateDailyPnL(state, 10250);
    expect(updated.dailyPnL.current).toBe(10250);
  });
});

Run: cd trading-agent && npx vitest run tests/durable-objects/agent-state.test.ts Expected: FAIL.

// src/durable-objects/agent-state.ts
import type { ExchangeName, Balance } from '@/exchanges/types';

export interface PositionEntry {
  qty: number;
  avgEntry: number;
}

export interface AgentStateData {
  positions: Record<string, Partial<Record<ExchangeName, PositionEntry>>>;
  balances: Record<string, Record<string, number>>;
  dailyPnL: { date: string; startBalance: number; current: number };
  totalPnL: { initialDeposit: number; allTimeUSD: number; percentReturn: number };
  lastSync: string;
  status: 'running' | 'halted';
}

export function defaultAgentState(): AgentStateData {
  return {
    positions: {},
    balances: {},
    dailyPnL: { date: new Date().toISOString().slice(0, 10), startBalance: 0, current: 0 },
    totalPnL: { initialDeposit: 0, allTimeUSD: 0, percentReturn: 0 },
    lastSync: new Date().toISOString(),
    status: 'running',
  };
}

export function updatePosition(
  state: AgentStateData,
  pair: string,
  exchange: ExchangeName,
  side: 'buy' | 'sell',
  qty: number,
  price: number,
): AgentStateData {
  const newState = structuredClone(state);
  if (!newState.positions[pair]) newState.positions[pair] = {};
  const existing = newState.positions[pair][exchange];

  if (side === 'buy') {
    if (existing) {
      const totalQty = existing.qty + qty;
      const avgEntry = (existing.avgEntry * existing.qty + price * qty) / totalQty;
      newState.positions[pair][exchange] = { qty: totalQty, avgEntry };
    } else {
      newState.positions[pair][exchange] = { qty, avgEntry: price };
    }
  } else {
    if (existing) {
      const remainingQty = existing.qty - qty;
      newState.positions[pair][exchange] = {
        qty: Math.max(0, remainingQty),
        avgEntry: existing.avgEntry,
      };
      if (remainingQty <= 0) {
        delete newState.positions[pair][exchange];
        if (Object.keys(newState.positions[pair]).length === 0) {
          delete newState.positions[pair];
        }
      }
    }
  }

  return newState;
}

export function syncBalances(
  state: AgentStateData,
  exchange: ExchangeName,
  balances: Balance[],
): AgentStateData {
  const newState = structuredClone(state);
  newState.balances[exchange] = {};
  for (const b of balances) {
    newState.balances[exchange][b.asset] = b.total;
  }
  newState.lastSync = new Date().toISOString();
  return newState;
}

export function updateDailyPnL(state: AgentStateData, currentBalance: number): AgentStateData {
  const newState = structuredClone(state);
  newState.dailyPnL.current = currentBalance;
  return newState;
}

Run: cd trading-agent && npx vitest run tests/durable-objects/agent-state.test.ts Expected: ALL PASS.

git add trading-agent/src/durable-objects/agent-state.ts trading-agent/tests/durable-objects/agent-state.test.ts
git commit -m "feat: implement AgentStateDO with multi-exchange position tracking"

Task 5: OrderManagerDO

Files:

// tests/durable-objects/order-manager.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import {
  OrderManagerData,
  defaultOrderManager,
  addOrder,
  updateOrderFill,
  cancelOrder,
  getOpenOrders,
} from '@/durable-objects/order-manager';
import type { Order } from '@/exchanges/types';

const makeOrder = (overrides: Partial<Order> = {}): Order => ({
  id: 'ord-1',
  clientOrderId: 'cli-1',
  pair: 'BTC-USDT',
  side: 'buy',
  type: 'market',
  status: 'open',
  price: null,
  filledPrice: null,
  qty: 0.1,
  filledQty: 0,
  fee: null,
  feeAsset: null,
  exchange: 'coinbase',
  createdAt: '2026-03-14T10:00:00Z',
  updatedAt: '2026-03-14T10:00:00Z',
  ...overrides,
});

describe('OrderManagerDO Logic', () => {
  let data: OrderManagerData;

  beforeEach(() => {
    data = defaultOrderManager();
  });

  it('should add an order to open orders', () => {
    const updated = addOrder(data, makeOrder());
    expect(updated.openOrders).toHaveLength(1);
    expect(updated.openOrders[0].id).toBe('ord-1');
  });

  it('should move fully filled order to history', () => {
    let d = addOrder(data, makeOrder());
    d = updateOrderFill(d, 'ord-1', { filledQty: 0.1, filledPrice: 62000, fee: 4.96, feeAsset: 'USDT', status: 'filled' });
    expect(d.openOrders).toHaveLength(0);
    expect(d.history).toHaveLength(1);
    expect(d.history[0].status).toBe('filled');
    expect(d.history[0].fee).toBe(4.96);
  });

  it('should keep partially filled order in open orders', () => {
    let d = addOrder(data, makeOrder());
    d = updateOrderFill(d, 'ord-1', { filledQty: 0.05, filledPrice: 62000, fee: 2.48, feeAsset: 'USDT', status: 'partially_filled' });
    expect(d.openOrders).toHaveLength(1);
    expect(d.openOrders[0].status).toBe('partially_filled');
    expect(d.openOrders[0].filledQty).toBe(0.05);
  });

  it('should cancel an order and move to history', () => {
    let d = addOrder(data, makeOrder());
    d = cancelOrder(d, 'ord-1');
    expect(d.openOrders).toHaveLength(0);
    expect(d.history).toHaveLength(1);
    expect(d.history[0].status).toBe('cancelled');
  });

  it('should filter open orders by pair', () => {
    let d = addOrder(data, makeOrder({ id: 'o1', pair: 'BTC-USDT' }));
    d = addOrder(d, makeOrder({ id: 'o2', pair: 'ETH-USDT' }));
    expect(getOpenOrders(d, 'BTC-USDT')).toHaveLength(1);
    expect(getOpenOrders(d)).toHaveLength(2);
  });
});

Run: cd trading-agent && npx vitest run tests/durable-objects/order-manager.test.ts Expected: FAIL.

// src/durable-objects/order-manager.ts
import type { Order } from '@/exchanges/types';

export interface OrderManagerData {
  openOrders: Order[];
  history: Order[];
}

export function defaultOrderManager(): OrderManagerData {
  return { openOrders: [], history: [] };
}

export function addOrder(data: OrderManagerData, order: Order): OrderManagerData {
  return { ...data, openOrders: [...data.openOrders, order] };
}

interface FillUpdate {
  filledQty: number;
  filledPrice: number;
  fee: number;
  feeAsset: string;
  status: 'filled' | 'partially_filled';
}

export function updateOrderFill(data: OrderManagerData, orderId: string, fill: FillUpdate): OrderManagerData {
  const idx = data.openOrders.findIndex(o => o.id === orderId);
  if (idx === -1) return data;

  const order = { ...data.openOrders[idx], ...fill, updatedAt: new Date().toISOString() };

  if (fill.status === 'filled') {
    return {
      openOrders: data.openOrders.filter((_, i) => i !== idx),
      history: [...data.history, order],
    };
  }
  const newOpen = [...data.openOrders];
  newOpen[idx] = order;
  return { openOrders: newOpen, history: data.history };
}

export function cancelOrder(data: OrderManagerData, orderId: string): OrderManagerData {
  const idx = data.openOrders.findIndex(o => o.id === orderId);
  if (idx === -1) return data;
  const order = { ...data.openOrders[idx], status: 'cancelled' as const, updatedAt: new Date().toISOString() };
  return {
    openOrders: data.openOrders.filter((_, i) => i !== idx),
    history: [...data.history, order],
  };
}

export function getOpenOrders(data: OrderManagerData, pair?: string): Order[] {
  if (pair) return data.openOrders.filter(o => o.pair === pair);
  return data.openOrders;
}

Run: cd trading-agent && npx vitest run tests/durable-objects/order-manager.test.ts Expected: ALL PASS.

git add trading-agent/src/durable-objects/order-manager.ts trading-agent/tests/durable-objects/order-manager.test.ts
git commit -m "feat: implement OrderManagerDO with partial fill handling"

Task 6: AuditLogDO, MarketDataDO, TradingPairsDO

Files:

// tests/durable-objects/audit-log.test.ts
import { describe, it, expect } from 'vitest';
import { AuditLogData, defaultAuditLog, appendEvent, pruneOlderThan } from '@/durable-objects/audit-log';

describe('AuditLogDO Logic', () => {
  it('should append events', () => {
    let log = defaultAuditLog();
    log = appendEvent(log, 'trade_executed', 'system', { pair: 'BTC-USDT' });
    expect(log.events).toHaveLength(1);
    expect(log.events[0].type).toBe('trade_executed');
  });

  it('should prune events older than cutoff', () => {
    let log = defaultAuditLog();
    log.events = [
      { id: '1', type: 'trade_executed', actor: 'system', details: {}, timestamp: '2026-03-07T00:00:00Z' },
      { id: '2', type: 'trade_executed', actor: 'system', details: {}, timestamp: '2026-03-14T00:00:00Z' },
    ];
    const pruned = pruneOlderThan(log, new Date('2026-03-10T00:00:00Z'));
    expect(pruned.events).toHaveLength(1);
    expect(pruned.events[0].id).toBe('2');
  });
});
// tests/durable-objects/market-data.test.ts
import { describe, it, expect } from 'vitest';
import { MarketDataState, defaultMarketData, updateTicker, addCandles, MAX_CANDLES } from '@/durable-objects/market-data';
import type { Ticker, Candle } from '@/exchanges/types';

describe('MarketDataDO Logic', () => {
  it('should store tickers per pair per exchange', () => {
    let md = defaultMarketData();
    const ticker: Ticker = { pair: 'BTC-USDT', bid: 62000, ask: 62050, last: 62025, volume24h: 1000, timestamp: new Date().toISOString() };
    md = updateTicker(md, 'BTC-USDT', 'coinbase', ticker);
    expect(md.tickers['BTC-USDT']['coinbase'].bid).toBe(62000);
  });

  it('should cap candles at MAX_CANDLES per key', () => {
    let md = defaultMarketData();
    const candles: Candle[] = Array.from({ length: 250 }, (_, i) => ({
      open: 100 + i, high: 110 + i, low: 90 + i, close: 105 + i,
      volume: 1000, timestamp: new Date(Date.now() + i * 3600000).toISOString(),
    }));
    md = addCandles(md, 'BTC-USDT:1h', candles);
    expect(md.candles['BTC-USDT:1h'].length).toBeLessThanOrEqual(MAX_CANDLES);
  });
});
// tests/durable-objects/trading-pairs.test.ts
import { describe, it, expect } from 'vitest';
import { TradingPairsData, defaultTradingPairs, computeActivePairs, updateWhitelist } from '@/durable-objects/trading-pairs';

describe('TradingPairsDO Logic', () => {
  it('should compute active pairs as whitelist ∩ intersection', () => {
    let tp = defaultTradingPairs();
    tp.whitelist = ['BTC-USDT', 'ETH-USDT', 'DOGE-USDT'];
    tp.intersection = ['BTC-USDT', 'ETH-USDT', 'SOL-USDT'];
    tp = computeActivePairs(tp);
    expect(tp.activePairs).toEqual(['BTC-USDT', 'ETH-USDT']);
  });

  it('should update whitelist and recompute active pairs', () => {
    let tp = defaultTradingPairs();
    tp.intersection = ['BTC-USDT', 'ETH-USDT'];
    tp = updateWhitelist(tp, ['BTC-USDT', 'SOL-USDT']);
    expect(tp.whitelist).toEqual(['BTC-USDT', 'SOL-USDT']);
    expect(tp.activePairs).toEqual(['BTC-USDT']); // SOL not in intersection
  });
});

Run: cd trading-agent && npx vitest run tests/durable-objects/audit-log.test.ts tests/durable-objects/market-data.test.ts tests/durable-objects/trading-pairs.test.ts Expected: FAIL.

// src/durable-objects/audit-log.ts
import type { AuditEvent, AuditEventType, AuditActor } from '@/exchanges/types';

export interface AuditLogData {
  events: AuditEvent[];
}

export function defaultAuditLog(): AuditLogData {
  return { events: [] };
}

export function appendEvent(
  data: AuditLogData,
  type: AuditEventType,
  actor: AuditActor,
  details: Record<string, unknown>,
): AuditLogData {
  const event: AuditEvent = {
    id: `evt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
    type,
    actor,
    details,
    timestamp: new Date().toISOString(),
  };
  return { events: [...data.events, event] };
}

export function pruneOlderThan(data: AuditLogData, cutoff: Date): AuditLogData {
  return { events: data.events.filter(e => new Date(e.timestamp) >= cutoff) };
}
// src/durable-objects/market-data.ts
import type { Ticker, Candle, ExchangeName } from '@/exchanges/types';

export const MAX_CANDLES = 200;

export interface MarketDataState {
  tickers: Record<string, Record<string, Ticker>>;
  candles: Record<string, Candle[]>;
  lastUpdate: string;
}

export function defaultMarketData(): MarketDataState {
  return { tickers: {}, candles: {}, lastUpdate: new Date().toISOString() };
}

export function updateTicker(
  state: MarketDataState,
  pair: string,
  exchange: ExchangeName,
  ticker: Ticker,
): MarketDataState {
  const newState = structuredClone(state);
  if (!newState.tickers[pair]) newState.tickers[pair] = {};
  newState.tickers[pair][exchange] = ticker;
  newState.lastUpdate = new Date().toISOString();
  return newState;
}

export function addCandles(
  state: MarketDataState,
  key: string,
  candles: Candle[],
): MarketDataState {
  const newState = structuredClone(state);
  const existing = newState.candles[key] ?? [];
  const merged = [...existing, ...candles].slice(-MAX_CANDLES);
  newState.candles[key] = merged;
  newState.lastUpdate = new Date().toISOString();
  return newState;
}
// src/durable-objects/trading-pairs.ts
import type { TradingPair, ExchangeName } from '@/exchanges/types';

export interface TradingPairsData {
  whitelist: string[];
  exchangePairs: Record<string, string[]>;
  intersection: string[];
  activePairs: string[];
  pairMetadata: Record<string, Partial<Record<ExchangeName, TradingPair>>>;
  lastDiscovery: string;
}

export function defaultTradingPairs(): TradingPairsData {
  return {
    whitelist: [
      'BTC-USDT', 'ETH-USDT', 'SOL-USDT', 'BNB-USDT', 'XRP-USDT',
      'ADA-USDT', 'DOGE-USDT', 'AVAX-USDT', 'DOT-USDT', 'MATIC-USDT',
    ],
    exchangePairs: {},
    intersection: [],
    activePairs: [],
    pairMetadata: {},
    lastDiscovery: '',
  };
}

export function computeActivePairs(data: TradingPairsData): TradingPairsData {
  const active = data.whitelist.filter(p => data.intersection.includes(p));
  return { ...data, activePairs: active };
}

export function updateWhitelist(data: TradingPairsData, whitelist: string[]): TradingPairsData {
  const updated = { ...data, whitelist };
  return computeActivePairs(updated);
}

export function computeIntersection(data: TradingPairsData): TradingPairsData {
  const sets = Object.values(data.exchangePairs).map(pairs => new Set(pairs));
  if (sets.length === 0) return { ...data, intersection: [], activePairs: [] };
  const inter = [...sets[0]].filter(p => sets.every(s => s.has(p)));
  const updated = { ...data, intersection: inter };
  return computeActivePairs(updated);
}

Run: cd trading-agent && npx vitest run tests/durable-objects/ Expected: ALL PASS.

git add trading-agent/src/durable-objects/ trading-agent/tests/durable-objects/
git commit -m "feat: implement AuditLogDO, MarketDataDO, TradingPairsDO with tests"

Task 6b: Durable Object Class Wrappers

The pure functions from Tasks 3-6 must be wrapped in actual Cloudflare Durable Object classes to be deployable.

Files:

Each Durable Object class follows this pattern:

// Example: GuardrailsDO class wrapper (add to bottom of guardrails.ts)
import type { Env } from '@/env';

export class GuardrailsDO {
  private state: DurableObjectState;
  private env: Env;
  private config: GuardrailsConfig | null = null;
  private guardrailState: GuardrailsState | null = null;

  constructor(state: DurableObjectState, env: Env) {
    this.state = state;
    this.env = env;
  }

  private async load(): Promise<{ config: GuardrailsConfig; state: GuardrailsState }> {
    if (!this.config) {
      this.config = (await this.state.storage.get<GuardrailsConfig>('config')) ?? { ...defaultGuardrailsConfig };
    }
    if (!this.guardrailState) {
      this.guardrailState = (await this.state.storage.get<GuardrailsState>('state')) ?? {
        todayDate: new Date().toISOString().slice(0, 10),
        todayRealizedPnL: 0,
        trailing7DayPeak: 0,
        recentPnLWindow: [],
        lastTradePerPair: {},
        recentTradeTimestamps: [],
        pausedUntil: null,
        haltReason: null,
      };
    }
    return { config: this.config, state: this.guardrailState };
  }

  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);
    const { config, state } = await this.load();

    if (url.pathname === '/check' && request.method === 'POST') {
      const { trade, portfolio } = await request.json() as {
        trade: TradeRequest;
        portfolio: PortfolioSnapshot;
      };
      const result = checkGuardrails(trade, config, state, portfolio);

      // Handle auto-halt
      if (result.autoHalt) {
        this.guardrailState = { ...state, haltReason: result.reason };
        this.config = { ...config, killSwitch: true };
        await this.state.storage.put('state', this.guardrailState);
        await this.state.storage.put('config', this.config);
      }

      // Handle rate-of-loss pause
      if (result.reason?.includes('rate-of-loss') && !state.pausedUntil) {
        const pauseUntil = new Date(Date.now() + config.rateOfLossPauseMinutes * 60 * 1000).toISOString();
        this.guardrailState = { ...state, pausedUntil: pauseUntil };
        await this.state.storage.put('state', this.guardrailState);
      }

      return Response.json(result);
    }

    if (url.pathname === '/config' && request.method === 'GET') {
      return Response.json({ config, state });
    }

    if (url.pathname === '/config' && request.method === 'PUT') {
      const updates = await request.json() as Partial<GuardrailsConfig>;
      this.config = { ...config, ...updates };
      await this.state.storage.put('config', this.config);
      return Response.json({ config: this.config });
    }

    if (url.pathname === '/record-trade' && request.method === 'POST') {
      const { pair, pnl, timestamp } = await request.json() as { pair: string; pnl: number; timestamp: string };
      this.guardrailState = {
        ...state,
        todayRealizedPnL: state.todayRealizedPnL + pnl,
        lastTradePerPair: { ...state.lastTradePerPair, [pair]: timestamp },
        recentTradeTimestamps: [...state.recentTradeTimestamps, timestamp],
        recentPnLWindow: [...state.recentPnLWindow, { pnl, timestamp }],
      };
      await this.state.storage.put('state', this.guardrailState);
      return Response.json({ ok: true });
    }

    return new Response('Not found', { status: 404 });
  }
}

Apply the same pattern (constructor, load, fetch with route dispatch, storage persistence) to all 6 DOs. Each DO exposes a small HTTP API that the Worker routes to.

Run: cd trading-agent && npx tsc --noEmit Expected: No errors.

git add trading-agent/src/durable-objects/
git commit -m "feat: add Durable Object class wrappers with storage persistence"

Task 6c: KV Archival

Files:

OrderManagerDO: Archive orders older than 30 days to TRADE_HISTORY_KV (key: orders:{YYYY-MM-DD}). AuditLogDO: Archive events older than 7 days to AUDIT_LOG_KV (key: events:{YYYY-MM-DD}).

Both use the DO’s alarm() method, triggered daily, to perform the archival.

git add trading-agent/src/durable-objects/
git commit -m "feat: add KV archival for order history and audit log"

Phase 3: Exchange Adapters

This phase produces: working Crypto.com and Coinbase adapters, paper trading wrapper, and smart order router.


Task 7: Crypto.com Exchange Adapter

Files:

Reference docs: Crypto.com Exchange API v1 — https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html

Test file should cover: normalizePair, getTicker, getBalances, placeMarketOrder, and HMAC signing. Use vitest.fn() to mock the global fetch.

Key test cases:

Run: cd trading-agent && npx vitest run tests/exchanges/crypto-com.test.ts Expected: FAIL.

Implement CryptoComExchange class that implements the Exchange interface. Key implementation details:

Run: cd trading-agent && npx vitest run tests/exchanges/crypto-com.test.ts Expected: ALL PASS.

git add trading-agent/src/exchanges/crypto-com.ts trading-agent/tests/exchanges/crypto-com.test.ts
git commit -m "feat: implement Crypto.com exchange adapter with HMAC auth"

Task 8: Coinbase Advanced Trade Adapter

Files:

Reference docs: Coinbase Advanced Trade API — https://docs.cdp.coinbase.com/advanced-trade/docs/welcome

Same pattern as Task 7. Key differences:

git add trading-agent/src/exchanges/coinbase.ts trading-agent/tests/exchanges/coinbase.test.ts
git commit -m "feat: implement Coinbase Advanced Trade adapter with JWT auth"

Task 9: Paper Trading Adapter + Smart Order Router

Files:

// tests/exchanges/paper-trading.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { PaperTradingAdapter } from '@/exchanges/paper-trading';
import type { Exchange, Ticker } from '@/exchanges/types';

describe('PaperTradingAdapter', () => {
  it('should passthrough market data calls to real exchange', async () => {
    const mockExchange = {
      name: 'coinbase' as const,
      getTicker: vi.fn().mockResolvedValue({ pair: 'BTC-USDT', bid: 62000, ask: 62050, last: 62025, volume24h: 1000, timestamp: '' }),
    } as unknown as Exchange;

    const paper = new PaperTradingAdapter(mockExchange);
    const ticker = await paper.getTicker('BTC-USDT');
    expect(mockExchange.getTicker).toHaveBeenCalledWith('BTC-USDT');
    expect(ticker.bid).toBe(62000);
  });

  it('should simulate market order fills locally', async () => {
    const mockExchange = {
      name: 'coinbase' as const,
      getTicker: vi.fn().mockResolvedValue({ pair: 'BTC-USDT', bid: 62000, ask: 62050, last: 62025, volume24h: 1000, timestamp: '' }),
      getFeeSchedule: vi.fn().mockResolvedValue({ makerRate: 0.001, takerRate: 0.002 }),
    } as unknown as Exchange;

    const paper = new PaperTradingAdapter(mockExchange);
    const order = await paper.placeMarketOrder('BTC-USDT', 'buy', 0.1, 'cli-1');
    expect(order.status).toBe('filled');
    expect(order.filledPrice).toBe(62050); // filled at ask for buys
    expect(order.exchange).toBe('coinbase');
  });
});
// tests/exchanges/order-router.test.ts
import { describe, it, expect, vi } from 'vitest';
import { routeOrder } from '@/exchanges/order-router';
import type { Exchange } from '@/exchanges/types';

describe('Smart Order Router', () => {
  const makeMockExchange = (name: 'crypto.com' | 'coinbase', ask: number, bid: number): Exchange => ({
    name,
    getTicker: vi.fn().mockResolvedValue({ pair: 'BTC-USDT', bid, ask, last: (bid + ask) / 2, volume24h: 1000, timestamp: '' }),
    getOrderbook: vi.fn().mockResolvedValue({ pair: 'BTC-USDT', bids: [[bid, 10]], asks: [[ask, 10]], timestamp: '' }),
    getFeeSchedule: vi.fn().mockResolvedValue({ makerRate: 0.001, takerRate: 0.002 }),
  } as unknown as Exchange);

  it('should route buy to exchange with lower effective price', async () => {
    const cryptoCom = makeMockExchange('crypto.com', 62000, 61950);
    const coinbase = makeMockExchange('coinbase', 62050, 61980);
    const result = await routeOrder('BTC-USDT', 'buy', 0.1, 'market', { cryptoCom, coinbase });
    expect(result.exchange.name).toBe('crypto.com'); // lower ask
  });

  it('should route sell to exchange with higher effective price', async () => {
    const cryptoCom = makeMockExchange('crypto.com', 62000, 61950);
    const coinbase = makeMockExchange('coinbase', 62050, 62020);
    const result = await routeOrder('BTC-USDT', 'sell', 0.1, 'market', { cryptoCom, coinbase });
    expect(result.exchange.name).toBe('coinbase'); // higher bid
  });

  it('should fallback to single exchange if one is down', async () => {
    const cryptoCom = {
      name: 'crypto.com' as const,
      getTicker: vi.fn().mockRejectedValue(new Error('timeout')),
      getOrderbook: vi.fn().mockRejectedValue(new Error('timeout')),
      getFeeSchedule: vi.fn().mockRejectedValue(new Error('timeout')),
    } as unknown as Exchange;
    const coinbase = makeMockExchange('coinbase', 62050, 61980);
    const result = await routeOrder('BTC-USDT', 'buy', 0.1, 'market', { cryptoCom, coinbase });
    expect(result.exchange.name).toBe('coinbase');
  });
});

The adapter wraps a real Exchange, passes through all read methods, and simulates order fills locally using current market prices.

Implement routeOrder using Promise.allSettled, fee-aware price comparison, and orderbook depth checking per the spec.

Run: cd trading-agent && npx vitest run tests/exchanges/ Expected: ALL PASS.

git add trading-agent/src/exchanges/ trading-agent/tests/exchanges/
git commit -m "feat: add PaperTradingAdapter and smart order router"

Phase 4: Agent Core — Decision Loop + Signal Client

This phase produces: the main trading loop, ML signal client, and cron scheduling.


Task 10: Signal Client

Files:

Test cases:

git add trading-agent/src/agent/signal-client.ts trading-agent/tests/agent/signal-client.test.ts
git commit -m "feat: implement ML signal client with 5s timeout and fail-to-HOLD"

Task 11: Decision Loop

Files:

Test the orchestration logic:

The loop iterates over active pairs, fetches data, requests signals, checks guardrails, routes orders, and executes. All DO interactions happen through stub interfaces (injected, not direct DO calls) so the logic is testable without Cloudflare runtime.

git add trading-agent/src/agent/decision-loop.ts trading-agent/tests/agent/decision-loop.test.ts
git commit -m "feat: implement trading decision loop with signal → guardrails → execute flow"

Task 12: Cron Scheduler + Worker Entry Point

Files:

Maps cron patterns to handlers:

Each handler acquires its DO lock via the AgentStateDO before proceeding.

Exports the Hono app for HTTP requests and the scheduled handler for crons. Exports all 6 DO classes.

Run: cd trading-agent && npx tsc --noEmit Expected: No errors.

git add trading-agent/src/agent/scheduler.ts trading-agent/src/index.ts
git commit -m "feat: add cron scheduler and Worker entry point"

Phase 5: API Layer — REST + Auth

This phase produces: all REST endpoints, JWT auth, and CORS middleware.


Task 13: Auth Middleware

Files:

Uses Hono’s built-in middleware patterns:

git add trading-agent/src/auth.ts
git commit -m "feat: add JWT auth and CORS middleware"

Task 14: API Routes

Files:

Each API handler is a thin layer that reads from / writes to Durable Objects. Handlers validate input, delegate to DOs, and return JSON responses.

Follow the endpoint table from the spec. Each handler:

  1. Validates request (auth, params, body)
  2. Gets DO stub via env.AGENT_STATE.get(id)
  3. Sends request to DO
  4. Returns JSON response

Test GET /api/status, GET /api/portfolio, PUT /api/guardrails, PUT /api/kill-switch.

Run: cd trading-agent && npx vitest run Expected: ALL PASS.

git add trading-agent/src/router.ts trading-agent/src/api/ trading-agent/tests/api/
git commit -m "feat: implement full REST API with auth, CORS, and all endpoints"

Phase 6: ML Service

This phase produces: a deployable FastAPI service on Fly.io wrapping the existing Python ML pipeline.


Task 15: ML Inference Service

Files:

# ml-service/tests/test_predict.py
import pytest
from predict import generate_signal

def test_generate_signal_returns_valid_action():
    result = generate_signal(
        pair="BTC-USDT",
        candles=[{"open": 62000, "high": 62500, "low": 61500, "close": 62200, "volume": 100, "timestamp": "2026-03-14T10:00:00Z"}] * 100,
        orderbook={"bids": [[62000, 1.0]], "asks": [[62050, 1.0]]},
        volume_24h=15000.0,
    )
    assert result["action"] in ("BUY", "SELL", "HOLD")
    assert 0 <= result["confidence"] <= 1
    assert result["pair"] == "BTC-USDT"

def test_generate_signal_handles_empty_candles():
    result = generate_signal(pair="BTC-USDT", candles=[], orderbook={"bids": [], "asks": []}, volume_24h=0)
    assert result["action"] == "HOLD"
    assert result["confidence"] == 0

Run: cd trading-agent/ml-service && python -m pytest tests/ -v Expected: FAIL.

# ml-service/main.py
from fastapi import FastAPI, Depends, HTTPException, Header
from pydantic import BaseModel
from predict import generate_signal
import os

app = FastAPI(title="Trading Agent ML Service")

async def verify_api_key(authorization: str = Header(...)):
    expected = f"Bearer {os.environ['ML_SERVICE_KEY']}"
    if authorization != expected:
        raise HTTPException(status_code=401, detail="Invalid API key")

class PredictRequest(BaseModel):
    pair: str
    candles: list[dict]
    orderbook: dict
    volume_24h: float

class PredictResponse(BaseModel):
    action: str
    confidence: float
    pair: str

@app.post("/predict", response_model=PredictResponse, dependencies=[Depends(verify_api_key)])
async def predict(req: PredictRequest):
    return generate_signal(req.pair, req.candles, req.orderbook, req.volume_24h)

@app.get("/health")
async def health():
    return {"status": "healthy", "modelsLoaded": ["xgboost", "lstm", "logistic"], "lastRetrain": ""}

@app.get("/retrain/status")
async def retrain_status():
    return {"status": "idle", "lastCompleted": "", "currentPair": None}

Port the signal generation logic from the root-level trading_engine.py into a clean function interface. Start with a simplified version that uses technical indicators (RSI, MACD, Bollinger Bands) computed from the candle data, with the full ML ensemble as a follow-up.

Run: cd trading-agent/ml-service && python -m pytest tests/ -v Expected: ALL PASS.

git add trading-agent/ml-service/
git commit -m "feat: implement ML inference service with FastAPI and prediction endpoint"

Phase 7: MCP Server

This phase produces: an MCP server exposing all trading agent tools to Claude.


Task 16: MCP Server

Files:

Define all 14 MCP tools from the spec. Each tool calls the corresponding REST API endpoint on the Worker.

Tools: get_agent_status, get_portfolio, get_signals, execute_trade, get_open_orders, cancel_order, get_trade_history, get_guardrails, set_guardrails, manage_pairs, kill_switch, get_market_data, get_pnl_report, get_events.

git add trading-agent/src/mcp/
git commit -m "feat: implement MCP server with 14 trading agent tools"

Phase 8: CI/CD + Deployment

This phase produces: GitHub Actions workflow for automated deployment.


Task 17: GitHub Actions Workflow

Files:

name: Deploy Trading Agent
on:
  push:
    branches: [main]
    paths: ['trading-agent/**']
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: trading-agent
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npm run typecheck
      - run: npm test

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    defaults:
      run:
        working-directory: trading-agent
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npx wrangler deploy
        env:
          CLOUDFLARE_API_TOKEN: $
          CLOUDFLARE_ACCOUNT_ID: $
git add .github/workflows/deploy-trading-agent.yml
git commit -m "feat: add CI/CD workflow for trading agent deployment"

Execution Order Summary

Phase Tasks Depends On Produces
1 1-2 Nothing Buildable project + types
2 3-6b-6c Phase 1 All 6 Durable Objects with class wrappers + KV archival
3 7-9 Phase 1 Exchange adapters + router
4 10-12 Phases 2+3 Trading loop + scheduler
5 13-14 Phase 4 REST API + auth + WebSocket
6 15 Nothing (independent) ML service
7 16 Phase 5 MCP server
8 17 All phases CI/CD (Worker + ML service)

After Phase 1 completes, Phases 2, 3, and 6 can run in parallel. Phase 4 requires 2+3. Phase 5 requires 4. Phase 7 requires 5. Phase 8 is last.

Note on implementation detail: Tasks 7-8 (exchange adapters), 10-12 (agent core), 13-14 (API layer), and 16 (MCP server) describe the architecture and test cases but defer full implementation code to execution time. The implementing agent should reference the spec at docs/superpowers/specs/2026-03-14-trading-agent-design.md and the exchange API documentation for each adapter. Use @context7 to fetch up-to-date Crypto.com and Coinbase API docs during implementation.