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.
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.
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"]
}
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;
}
Here's what a typical week of journal entries looks like for an active trading agent:
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),
};
}
The journal becomes most valuable when you slice it by context dimensions to identify where your edge actually lives. Run these analyses weekly:
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'.
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
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',
};
});
}
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.
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);
}
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 ratio | Target > 1.8 | Tighten stop loss or widen take profit |
| Expectancy per trade | Target > $2 | Increase position size or improve signals |
| Max drawdown (7d) | Target < 8% | Reduce TRADE_SIZE_USD by 25% |
| Regime distribution | Volatile trades < 20% of total | Add regime filter to entry conditions |
| Worst hour win rate | No hour below 40% | Add hour to BLOCKED_HOURS list |
| Single-model trade win rate | Should be lower than multi-model | Increase MIN_QUORUM requirement |
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" }
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.
Choose a storage layer that matches your agent's architecture:
better-sqlite3 in Node.js.// 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();
Data without action is just storage. Close the loop by feeding journal insights back into your agent's decision-making:
if (regime === 'volatile') return 'HOLD' to your entry logicEvery 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