Tools Guide

Autonomous Trading Journals: How AI Agents Track and Improve Performance

An agent that doesn't track its trades can't learn from them. A structured trading journal — automatically populated from Purple Flea's API audit logs — gives your agent the data it needs to identify winning patterns, eliminate losing setups, and continuously improve its edge over time.


Why Agents Need Trading Journals

Human traders keep journals to avoid repeating mistakes. AI agents have the same problem but at scale: without structured logging, a trading agent accumulates thousands of trades with no systematic way to analyze which strategies work, under what conditions, and how performance is trending.

The good news is that AI agents can implement far more rigorous journaling than humans. Every trade can be tagged with the full context that generated it: which indicators triggered it, what the market regime was, which model provided the signal, what time of day it was, and more. This rich data becomes a training ground for continuous improvement.

47+
Metadata fields per trade entry
Weekly
Recommended review cycle
~15%
Average win rate improvement after 4-week journal review
100%
Purple Flea trades automatically logged

The Anatomy of a Trade Log Entry

A minimal trade log entry captures the facts. A production-quality entry captures context, decision rationale, and outcome metadata for pattern analysis:

{
  // Identity
  "tradeId": "tr_20260306_eth_001",
  "agentId": "agent_xyzabc",
  "timestamp": "2026-03-06T14:32:17Z",

  // Instrument
  "asset": "ETH/USDC",
  "chain": "arbitrum",
  "venue": "uniswap-v3",

  // Entry
  "side": "BUY",
  "entryPrice": 3847.50,
  "entrySize": 0.52,  // ETH
  "entrySizeUsd": 2000.70,
  "entryGasUsd": 0.08,

  // Exit
  "exitPrice": 3921.30,
  "exitTimestamp": "2026-03-06T16:18:44Z",
  "exitReason": "take_profit",  // take_profit | stop_loss | signal_reversal | timeout
  "exitGasUsd": 0.06,

  // P&L
  "grossPnlUsd": 38.37,
  "feesPaid": 2.00,
  "netPnlUsd": 36.31,
  "netPnlPct": 0.0182,

  // Signal context
  "signalSource": "multi-model-consensus",
  "signalConfidence": 0.73,
  "signalDirection": "BUY",
  "modelsAgreement": ["claude", "gpt4"],  // which models agreed
  "timeHorizon": "4h",

  // Market context
  "marketRegime": "trending_up",  // trending_up | ranging | volatile | trending_down
  "btcCorrelation": 0.82,
  "hourOfDay": 14,
  "dayOfWeek": "Thursday",
  "newsEvents": [],  // any scheduled events during trade

  // Risk management
  "stopLossPrice": 3770.0,
  "takeProfitPrice": 3920.0,
  "riskRewardRatio": 2.1,
  "maxAdverseExcursion": -0.008,  // worst drawdown during trade
  "maxFavorableExcursion": 0.025, // best unrealized profit

  // Tags (for slice-and-dice analysis)
  "tags": ["morning-session", "trend-follow", "multi-model", "4h-signal"]
}

Pulling Data from Purple Flea Audit Logs

Purple Flea automatically logs every trade executed via the Trading API. Your journal agent can pull this data on a schedule and enrich it with additional context fields:

// journal-builder.js
import { PurpleFlea } from '@purpleflea/sdk';

const pf = new PurpleFlea({ apiKey: process.env.PF_API_KEY });

async function buildJournalEntries(sinceDays = 7) {
  const since = new Date(Date.now() - sinceDays * 86400000).toISOString();

  // Fetch raw trade log from Purple Flea
  const trades = await pf.trading.getHistory({ since, limit: 500 });

  const entries = [];

  for (const trade of trades) {
    // Enrich with context
    const marketData = await pf.market.getContextAtTime({
      asset: trade.asset,
      timestamp: trade.entryTimestamp,
    });

    const entry = {
      tradeId: trade.id,
      agentId: trade.agentId,
      timestamp: trade.entryTimestamp,
      asset: trade.asset,
      chain: trade.chain,
      side: trade.side,
      entryPrice: trade.entryPrice,
      exitPrice: trade.exitPrice,
      exitReason: trade.closeReason,
      netPnlUsd: trade.netPnlUsd,
      netPnlPct: trade.netPnlPct,
      feesPaid: trade.totalFees,
      signalSource: trade.metadata?.source,
      signalConfidence: trade.metadata?.confidence,
      marketRegime: marketData.regime,
      hourOfDay: new Date(trade.entryTimestamp).getUTCHours(),
      dayOfWeek: new Date(trade.entryTimestamp).toLocaleDateString('en', { weekday: 'long' }),
      tags: trade.metadata?.tags || [],
    };

    entries.push(entry);
  }

  return entries;
}

Example Journal Entries

Here's what a typical week of journal entries looks like for an active trading agent:

tr_20260306_eth_001 — ETH/USDC — BUY +$36.31 (+1.82%)
Entry: $3,847.50 → Exit: $3,921.30  |  Regime: trending_up  |  Exit: take_profit  |  Hold: 1h 46m
multi-model4h-signalmorningconfidence:0.73
tr_20260305_btc_003 — BTC/USDC — SELL -$22.10 (-1.11%)
Entry: $88,420 → Exit: $89,400  |  Regime: volatile  |  Exit: stop_loss  |  Hold: 23m
single-model1h-signaleveningconfidence:0.58
tr_20260304_arb_007 — ARB/USDC — BUY +$18.60 (+1.86%)
Entry: $1.840 → Exit: $1.875  |  Regime: ranging  |  Exit: take_profit  |  Hold: 3h 12m
multi-model24h-signalafternoonconfidence:0.81

Win Rate Tracking

Win rate is the foundational metric. But raw win rate is misleading without factoring in average win vs. average loss size. A 45% win rate can be extremely profitable if winners are 3× the size of losers.

function computeWinRateStats(entries) {
  const wins = entries.filter(e => e.netPnlUsd > 0);
  const losses = entries.filter(e => e.netPnlUsd < 0);

  const winRate = wins.length / entries.length;
  const avgWin = wins.reduce((s, e) => s + e.netPnlUsd, 0) / (wins.length || 1);
  const avgLoss = Math.abs(losses.reduce((s, e) => s + e.netPnlUsd, 0)) / (losses.length || 1);
  const expectancy = winRate * avgWin - (1 - winRate) * avgLoss;

  // Sharpe-like metric
  const pnls = entries.map(e => e.netPnlPct);
  const mean = pnls.reduce((a, b) => a + b, 0) / pnls.length;
  const std = Math.sqrt(pnls.map(x => (x-mean)**2).reduce((a,b)=>a+b,0)/pnls.length);
  const sharpe = mean / (std || 0.0001) * Math.sqrt(252);

  return {
    totalTrades: entries.length,
    winRate: (winRate * 100).toFixed(1) + '%',
    avgWinUsd: avgWin.toFixed(2),
    avgLossUsd: avgLoss.toFixed(2),
    winLossRatio: (avgWin / avgLoss).toFixed(2),
    expectancy: expectancy.toFixed(2),
    annualizedSharpe: sharpe.toFixed(2),
  };
}

Pattern Recognition: Finding Your Edge

The journal becomes most valuable when you slice it by context dimensions to identify where your edge actually lives. Run these analyses weekly:

Win Rate by Market Regime

function winRateByRegime(entries) {
  const regimes = ['trending_up', 'trending_down', 'ranging', 'volatile'];
  return Object.fromEntries(
    regimes.map(regime => {
      const subset = entries.filter(e => e.marketRegime === regime);
      const wins = subset.filter(e => e.netPnlUsd > 0).length;
      return [regime, subset.length > 0 ? (wins / subset.length * 100).toFixed(1) + '%' : 'n/a'];
    })
  );
}

// Example output:
// {
//   trending_up:   "74.1%",   <- strong edge here
//   trending_down: "68.3%",   <- decent edge short-side
//   ranging:       "51.2%",   <- barely above breakeven
//   volatile:      "38.7%",   <- losing money here!
// }

This analysis immediately tells you: trade less during volatile regimes, or not at all. Add a regime filter to your signal pipeline to prevent entries when marketRegime === 'volatile'.

Win Rate by Hour of Day

function winRateByHour(entries) {
  const byHour = {};
  for (const entry of entries) {
    const h = entry.hourOfDay;
    if (!byHour[h]) byHour[h] = { wins: 0, total: 0 };
    byHour[h].total++;
    if (entry.netPnlUsd > 0) byHour[h].wins++;
  }
  return Object.fromEntries(
    Object.entries(byHour)
      .sort(([a], [b]) => Number(a) - Number(b))
      .map(([h, s]) => [`${h}:00 UTC`, (s.wins / s.total * 100).toFixed(1) + '%'])
  );
}

// Use this to build a trading schedule:
// If hours 2-6 UTC have win rate < 50%, disable the agent during those hours

Win Rate by Signal Confidence Band

function winRateByConfidence(entries) {
  const bands = [
    { label: '0.55–0.65', min: 0.55, max: 0.65 },
    { label: '0.65–0.75', min: 0.65, max: 0.75 },
    { label: '0.75–0.85', min: 0.75, max: 0.85 },
    { label: '0.85–1.00', min: 0.85, max: 1.00 },
  ];
  return bands.map(band => {
    const subset = entries.filter(e =>
      e.signalConfidence >= band.min && e.signalConfidence < band.max
    );
    const wins = subset.filter(e => e.netPnlUsd > 0).length;
    return {
      band: band.label,
      tradeCount: subset.length,
      winRate: subset.length > 0 ? (wins / subset.length * 100).toFixed(1) + '%' : 'n/a',
    };
  });
}
Key Insight Pattern

Most agents find their win rate below 0.65 confidence is near 50% (coin flip), and above 0.80 confidence is 70%+. This validates raising the minimum confidence threshold and reducing position sizes for lower-confidence signals.

The Self-Improving Agent Loop

A truly autonomous trading agent doesn't just log trades — it reads the journal, extracts lessons, and updates its own parameters automatically. Here's the architecture:

// self-improvement-loop.js
import { analyzeJournal } from './journal-analyzer.js';
import { updateConfig } from './agent-config.js';
import Anthropic from '@anthropic-ai/sdk';

const claude = new Anthropic();

async function weeklyImprovementCycle(entries) {
  const stats = analyzeJournal(entries);

  // Ask Claude to interpret the stats and suggest parameter changes
  const response = await claude.messages.create({
    model: 'claude-opus-4-6',
    max_tokens: 1000,
    messages: [{
      role: 'user',
      content: `You are a trading performance analyst reviewing an AI agent's weekly stats.

Current parameters:
- MIN_CONFIDENCE: ${process.env.MIN_CONFIDENCE}
- TRADE_SIZE_USD: ${process.env.TRADE_SIZE_USD}
- ACTIVE_HOURS_UTC: ${process.env.ACTIVE_HOURS_UTC}
- ACTIVE_REGIMES: ${process.env.ACTIVE_REGIMES}

Performance analysis:
${JSON.stringify(stats, null, 2)}

Suggest specific parameter changes to improve win rate and expectancy.
Return as JSON with keys: MIN_CONFIDENCE, TRADE_SIZE_USD, ACTIVE_HOURS_UTC, ACTIVE_REGIMES.
Only suggest changes you are confident about. Keep changes conservative (max 20% adjustment).`,
    }],
  });

  const suggestions = JSON.parse(response.content[0].text);
  await updateConfig(suggestions);

  console.log('Parameters updated:', suggestions);
}

Weekly Journal Review Checklist

Run this review every Monday before the trading week begins:

Metric Check Action if Below Threshold
Win rate (7d)Target > 58%Raise MIN_CONFIDENCE by 0.05
Avg win / avg loss ratioTarget > 1.8Tighten stop loss or widen take profit
Expectancy per tradeTarget > $2Increase position size or improve signals
Max drawdown (7d)Target < 8%Reduce TRADE_SIZE_USD by 25%
Regime distributionVolatile trades < 20% of totalAdd regime filter to entry conditions
Worst hour win rateNo hour below 40%Add hour to BLOCKED_HOURS list
Single-model trade win rateShould be lower than multi-modelIncrease MIN_QUORUM requirement

Integrating Purple Flea Audit Logs

Purple Flea's audit log API provides every trade with full metadata, making it the authoritative source of truth for your journal. Key endpoints:

# Get full trade history with metadata
GET /api/v1/trading/history
  ?since=2026-03-01T00:00:00Z
  &until=2026-03-07T23:59:59Z
  &include_metadata=true
  &limit=1000

# Get aggregated performance stats
GET /api/v1/trading/stats
  ?period=7d
  &group_by=asset,hour,regime

# Get individual trade detail
GET /api/v1/trading/trades/:tradeId

# Webhook for real-time trade close events (better than polling)
POST /api/v1/webhooks
  body: { event: "trade.closed", url: "https://your-agent.com/hooks/trade" }
Use Webhooks Over Polling

Instead of polling the history endpoint every minute, register a webhook for trade.closed events. Your journal builder runs instantly on every trade close, keeping your database always current with zero polling overhead.

Journal Storage Options

Choose a storage layer that matches your agent's architecture:

Complete Implementation: Trade Journal Service

// trade-journal-service.js — full production implementation

import Database from 'better-sqlite3';
import { PurpleFlea } from '@purpleflea/sdk';

const db = new Database('./trade-journal.db');
const pf = new PurpleFlea({ apiKey: process.env.PF_API_KEY });

// Initialize schema
db.exec(`
  CREATE TABLE IF NOT EXISTS trades (
    id TEXT PRIMARY KEY,
    timestamp TEXT NOT NULL,
    asset TEXT, chain TEXT, side TEXT,
    entry_price REAL, exit_price REAL,
    net_pnl_usd REAL, net_pnl_pct REAL,
    fees_paid REAL, hold_minutes INTEGER,
    signal_confidence REAL, signal_source TEXT,
    market_regime TEXT, hour_of_day INTEGER, day_of_week TEXT,
    exit_reason TEXT, tags TEXT
  );
  CREATE INDEX IF NOT EXISTS idx_timestamp ON trades(timestamp);
  CREATE INDEX IF NOT EXISTS idx_regime ON trades(market_regime);
`);

const insertTrade = db.prepare(`
  INSERT OR REPLACE INTO trades VALUES (
    @id, @timestamp, @asset, @chain, @side,
    @entry_price, @exit_price, @net_pnl_usd, @net_pnl_pct,
    @fees_paid, @hold_minutes, @signal_confidence, @signal_source,
    @market_regime, @hour_of_day, @day_of_week, @exit_reason, @tags
  )
`);

// Sync from Purple Flea on startup
async function syncFromAuditLog() {
  const latest = db.prepare('SELECT MAX(timestamp) as ts FROM trades').get();
  const since = latest.ts ?? '2026-01-01T00:00:00Z';

  const trades = await pf.trading.getHistory({ since, limit: 1000 });
  for (const t of trades) {
    insertTrade.run({
      id: t.id, timestamp: t.entryTimestamp,
      asset: t.asset, chain: t.chain, side: t.side,
      entry_price: t.entryPrice, exit_price: t.exitPrice,
      net_pnl_usd: t.netPnlUsd, net_pnl_pct: t.netPnlPct,
      fees_paid: t.totalFees,
      hold_minutes: Math.round((new Date(t.exitTimestamp) - new Date(t.entryTimestamp)) / 60000),
      signal_confidence: t.metadata?.confidence ?? null,
      signal_source: t.metadata?.source ?? null,
      market_regime: t.metadata?.regime ?? null,
      hour_of_day: new Date(t.entryTimestamp).getUTCHours(),
      day_of_week: new Date(t.entryTimestamp).toLocaleDateString('en', { weekday: 'long' }),
      exit_reason: t.closeReason,
      tags: JSON.stringify(t.metadata?.tags ?? []),
    });
  }
  console.log(`Synced ${trades.length} trades from audit log`);
}

// Query win rate by regime
function winRateByRegime() {
  return db.prepare(`
    SELECT market_regime,
           COUNT(*) as total,
           SUM(CASE WHEN net_pnl_usd > 0 THEN 1 ELSE 0 END) as wins,
           ROUND(AVG(net_pnl_usd), 2) as avg_pnl_usd
    FROM trades WHERE market_regime IS NOT NULL
    GROUP BY market_regime ORDER BY wins * 1.0 / total DESC
  `).all();
}

// Weekly report
function weeklyReport() {
  const stats = db.prepare(`
    SELECT COUNT(*) as total,
           SUM(CASE WHEN net_pnl_usd > 0 THEN 1 ELSE 0 END) as wins,
           ROUND(SUM(net_pnl_usd), 2) as total_pnl,
           ROUND(AVG(net_pnl_usd), 2) as avg_pnl,
           ROUND(AVG(signal_confidence), 3) as avg_confidence
    FROM trades WHERE timestamp > datetime('now', '-7 days')
  `).get();

  const winRate = (stats.wins / stats.total * 100).toFixed(1);
  console.log(`\n=== WEEKLY REPORT ===`);
  console.log(`Total trades: ${stats.total}`);
  console.log(`Win rate: ${winRate}%`);
  console.log(`Total P&L: $${stats.total_pnl}`);
  console.log(`Avg P&L/trade: $${stats.avg_pnl}`);
  console.log(`Avg signal confidence: ${stats.avg_confidence}`);
  console.log(`\nBy regime:`, winRateByRegime());
}

await syncFromAuditLog();
weeklyReport();

Making the Journal Actionable

Data without action is just storage. Close the loop by feeding journal insights back into your agent's decision-making:

Start Your Agent's Trading Journal Today

Every Purple Flea trade is automatically logged with full metadata. Register your agent, connect to the audit log API, and start building your edge through systematic performance tracking.

Read the Docs Trading API