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
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
This phase produces: a buildable TypeScript project with all shared types, the Exchange interface, and test infrastructure.
Files:
trading-agent/package.jsontrading-agent/tsconfig.jsontrading-agent/vitest.config.tstrading-agent/wrangler.tomlCreate: trading-agent/src/env.ts
trading-agent/ directory and package.json{
"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"
}
}
tsconfig.json{
"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"]
}
vitest.config.tsimport { 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'),
},
},
});
wrangler.tomlname = "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
]
src/env.tsexport 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"
Files:
trading-agent/src/exchanges/types.tsCreate: trading-agent/tests/exchanges/types.test.ts
// 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"
This phase produces: all 6 Durable Objects with full test coverage. Each DO is independently testable.
Files:
trading-agent/src/durable-objects/guardrails.tsCreate: trading-agent/tests/durable-objects/guardrails.test.ts
// 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"
Files:
trading-agent/src/durable-objects/agent-state.tsCreate: trading-agent/tests/durable-objects/agent-state.test.ts
// 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"
Files:
trading-agent/src/durable-objects/order-manager.tsCreate: trading-agent/tests/durable-objects/order-manager.test.ts
// 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"
Files:
trading-agent/src/durable-objects/audit-log.tstrading-agent/src/durable-objects/market-data.tstrading-agent/src/durable-objects/trading-pairs.tstrading-agent/tests/durable-objects/audit-log.test.tstrading-agent/tests/durable-objects/market-data.test.tsCreate: trading-agent/tests/durable-objects/trading-pairs.test.ts
// 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"
The pure functions from Tasks 3-6 must be wrapped in actual Cloudflare Durable Object classes to be deployable.
Files:
trading-agent/src/durable-objects/agent-state.tstrading-agent/src/durable-objects/order-manager.tstrading-agent/src/durable-objects/guardrails.tstrading-agent/src/durable-objects/market-data.tstrading-agent/src/durable-objects/trading-pairs.tsModify: trading-agent/src/durable-objects/audit-log.ts
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"
Files:
trading-agent/src/durable-objects/order-manager.tsModify: trading-agent/src/durable-objects/audit-log.ts
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"
This phase produces: working Crypto.com and Coinbase adapters, paper trading wrapper, and smart order router.
Files:
trading-agent/src/exchanges/crypto-com.tstrading-agent/tests/exchanges/crypto-com.test.tsReference 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:
normalizePair('BTC_USDT') returns 'BTC-USDT'normalizePair('BTC-USDT') returns 'BTC-USDT' (already canonical)getTicker calls the correct endpoint and normalizes the responseplaceMarketOrder includes HMAC signature in the requestRate limiter delays requests when approaching the limit
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:
https://api.crypto.com/exchange/v1crypto.subtle (available in Workers)BTC-USDT ↔ BTC_USDTRate limiting: Track request timestamps, delay if approaching 1 req/100ms for market data
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"
Files:
trading-agent/src/exchanges/coinbase.tstrading-agent/tests/exchanges/coinbase.test.tsReference docs: Coinbase Advanced Trade API — https://docs.cdp.coinbase.com/advanced-trade/docs/welcome
Same pattern as Task 7. Key differences:
crypto.subtle)https://api.coinbase.com/api/v3/brokeragePair format: already BTC-USDT (canonical matches)
Step 2: Run tests to verify they fail
Step 3: Implement Coinbase adapter
Step 4: Run tests to verify they pass
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"
Files:
trading-agent/src/exchanges/paper-trading.tstrading-agent/src/exchanges/order-router.tstrading-agent/tests/exchanges/paper-trading.test.tsCreate: trading-agent/tests/exchanges/order-router.test.ts
// 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');
});
});
Step 3: Run tests to verify they fail
Step 4: Implement PaperTradingAdapter
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"
This phase produces: the main trading loop, ML signal client, and cron scheduling.
Files:
trading-agent/src/agent/signal-client.tsCreate: trading-agent/tests/agent/signal-client.test.ts
Test cases:
Includes Authorization: Bearer <key> header
Step 2: Run tests to verify they fail
Step 3: Implement signal client with 5s timeout and fail-to-HOLD
Step 4: Run tests to verify they pass
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"
Files:
trading-agent/src/agent/decision-loop.tsCreate: trading-agent/tests/agent/decision-loop.test.ts
Test the orchestration logic:
Does not execute when guardrails reject
Step 2: Run tests to verify they fail
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.
Step 4: Run tests to verify they pass
Step 5: Commit
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"
Files:
trading-agent/src/agent/scheduler.tsCreate: trading-agent/src/index.ts
Maps cron patterns to handlers:
* * * * * → runTradingLoop*/5 * * * * → runRebalanceCheck0 * * * * → runPairDiscovery0 0 * * * → runDailyReportEach 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"
This phase produces: all REST endpoints, JWT auth, and CORS middleware.
Files:
Create: trading-agent/src/auth.ts
Step 1: Implement JWT creation, validation, and CORS middleware
Uses Hono’s built-in middleware patterns:
POST /api/auth/login: accepts { key }, validates against DASHBOARD_API_KEY, returns JWT (HS256, 24h)Authorization: Bearer <jwt> on all /api/* routes (except login)CORS middleware: allows DASHBOARD_ORIGIN, credentials allowed
git add trading-agent/src/auth.ts
git commit -m "feat: add JWT auth and CORS middleware"
Files:
trading-agent/src/router.tstrading-agent/src/api/status.tstrading-agent/src/api/portfolio.tstrading-agent/src/api/trades.tstrading-agent/src/api/orders.tstrading-agent/src/api/signals.tstrading-agent/src/api/guardrails.tstrading-agent/src/api/pairs.tstrading-agent/src/api/kill-switch.tstrading-agent/src/api/events.tsCreate: trading-agent/src/api/market.ts
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:
env.AGENT_STATE.get(id)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"
This phase produces: a deployable FastAPI service on Fly.io wrapping the existing Python ML pipeline.
Files:
trading-agent/ml-service/main.pytrading-agent/ml-service/predict.pytrading-agent/ml-service/requirements.txttrading-agent/ml-service/Dockerfiletrading-agent/ml-service/fly.tomlCreate: trading-agent/ml-service/tests/test_predict.py
# 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.
Step 5: Create Dockerfile and fly.toml
Step 6: Run tests to verify they pass
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"
This phase produces: an MCP server exposing all trading agent tools to Claude.
Files:
Create: trading-agent/src/mcp/server.ts
Step 1: Implement MCP tool definitions
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"
This phase produces: GitHub Actions workflow for automated deployment.
Files:
Create: .github/workflows/deploy-trading-agent.yml
Step 1: Write deployment workflow
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"
| 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.