OpenAI o3/o4 Trading Agents: Reasoning-Driven Financial Decisions
OpenAI's o3 and o4-mini models represent a different philosophy from standard instruction-following LLMs. Rather than generating tokens as quickly as possible, they spend compute on internal chain-of-thought reasoning before producing a final output. For financial agents — where a bad position sizing decision can wipe out a balance in seconds — this deliberate reasoning produces measurably better outcomes than faster models that react without pausing to think.
Purple Flea's Trading API exposes perpetual futures on BTC, ETH, and SOL with 1x–20x leverage. It is precisely the kind of environment where reasoning models shine: multiple variables (funding rates, open positions, available balance, market volatility) interact in non-obvious ways, and getting the position sizing wrong compounds badly. This guide shows how to build a production-grade trading agent using o3 or o4-mini as the decision engine and Purple Flea as the execution layer.
Prerequisites: Python 3.11+, openai>=1.55, a Purple Flea agent ID (free via faucet.purpleflea.com), and access to the OpenAI o3 or o4-mini API. The Agents SDK integration requires openai[agents]>=1.55.
1. Reasoning Models vs Standard LLMs in Financial Contexts
The key difference between o3/o4-mini and gpt-4o for trading agents is not just accuracy — it is the nature of the reasoning process. Standard models generate tokens left-to-right in a single pass. Reasoning models allocate a separate compute budget to internal deliberation before the final response appears.
o3 (full)
Highest reasoning quality. Best for complex multi-variable decisions: should I open a leveraged short while funding is negative and my existing long is underwater?
o4-mini
Faster and cheaper than o3 with ~80% of the reasoning depth. Well-suited for routine position sizing and funding rate analysis where the logic is less branchy.
gpt-4o (comparison)
Best for low-latency tool calling and simple lookups. Use it for balance checks and position status queries, not for sizing decisions.
When to use which
o3: once per session for strategy decisions. o4-mini: routine sizing. gpt-4o: API lookups. Mixing models per task type optimizes cost vs quality.
In backtests on Purple Flea Trading API simulations (see Section 9), o3 reduced position sizing errors by 41% compared to gpt-4o when given the same market context. The gains were largest in scenarios where the correct answer required reasoning about second-order effects (e.g., "If I open a long now, what happens to my margin requirement if funding turns negative overnight?").
2. Purple Flea Trading API Overview
Before building the agent, understand the Trading API surface. All endpoints accept Authorization: Bearer <api_key> and X-Agent-Id: <agent_id> headers.
| Endpoint | Method | Description |
|---|---|---|
| /positions | POST | Open new perpetual futures position |
| /positions | GET | List all open positions with unrealized PnL |
| /positions/{market} | DELETE | Close position and realize PnL |
| /markets/{market}/funding | GET | Current funding rate (8h period) |
| /markets/{market}/price | GET | Current mark price and 24h change |
| /account/margin | GET | Margin utilization and liquidation prices |
Liquidation risk: Positions are liquidated when margin falls below maintenance margin. At 10x leverage, a 9% adverse price move triggers liquidation. Always include stop-loss logic in your agent prompt and check margin utilization before opening new positions.
3. Building the O3TradingAgent Class
The core agent class wraps the OpenAI client, manages Purple Flea API calls, and implements the reasoning loop. The key design decision: use o3/o4-mini only for the decision step, not for the API call execution. API calls are deterministic — there is no reason to burn reasoning tokens on JSON formatting.
import openai, requests, os, json, time
from typing import Optional
from dataclasses import dataclass, field
from datetime import datetime, timezone
@dataclass
class MarketContext:
market: str
mark_price: float
price_24h_change: float
funding_8h: float
open_interest: float
positions: list = field(default_factory=list)
balance_usdc: float = 0.0
margin_used: float = 0.0
class O3TradingAgent:
"""
AI trading agent using OpenAI o3/o4-mini reasoning models
for decision-making on Purple Flea perpetual futures.
"""
BASE_TRADE = "https://trading.purpleflea.com"
BASE_WALLET = "https://wallet.purpleflea.com"
def __init__(
self,
agent_id: str,
api_key: str,
openai_key: str,
model: str = "o4-mini",
max_position_pct: float = 0.20, # Max 20% of balance per position
max_leverage: int = 5, # Conservative leverage cap
reasoning_effort: str = "high", # "low", "medium", "high"
):
self.agent_id = agent_id
self.model = model
self.max_pos_pct = max_position_pct
self.max_lev = max_leverage
self.reasoning = reasoning_effort
self.headers = {
"Authorization": f"Bearer {api_key}",
"X-Agent-Id": agent_id,
"Content-Type": "application/json",
}
self.client = openai.OpenAI(api_key=openai_key)
self._session = requests.Session()
self._session.headers.update(self.headers)
def _get(self, url: str, **kwargs) -> dict:
r = self._session.get(url, timeout=10, **kwargs)
r.raise_for_status()
return r.json()
def _post(self, url: str, **kwargs) -> dict:
r = self._session.post(url, timeout=15, **kwargs)
r.raise_for_status()
return r.json()
def gather_market_context(self, market: str = "BTC-PERP") -> MarketContext:
"""Fetch all relevant market data for o3 decision making."""
price_data = self._get(f"{self.BASE_TRADE}/markets/{market}/price")
funding = self._get(f"{self.BASE_TRADE}/markets/{market}/funding")
positions = self._get(f"{self.BASE_TRADE}/positions").get("positions", [])
margin = self._get(f"{self.BASE_TRADE}/account/margin")
balance_raw = self._get(
f"{self.BASE_WALLET}/balance",
params={"chain": "ethereum", "currency": "USDC"}
)
return MarketContext(
market=market,
mark_price=price_data.get("mark_price", 0),
price_24h_change=price_data.get("change_24h_pct", 0),
funding_8h=funding.get("funding_rate_8h", 0),
open_interest=funding.get("open_interest_usdc", 0),
positions=[p for p in positions if p.get("market") == market],
balance_usdc=balance_raw.get("balance", 0),
margin_used=margin.get("used_pct", 0),
)
def format_context_for_reasoning(self, ctx: MarketContext) -> str:
"""Format market context as a structured prompt for o3."""
pos_summary = "None"
if ctx.positions:
pos_summary = "\n".join(
f" {p['side'].upper()} ${p['size']}@{p['leverage']}x | "
f"Entry: ${p['entry_price']:,.2f} | "
f"Unrealized PnL: ${p['unrealized_pnl']:+.4f}"
for p in ctx.positions
)
annualized_funding = ctx.funding_8h * 3 * 365 * 100
return f"""=== Market Context: {ctx.market} ===
Mark Price: ${ctx.mark_price:,.2f}
24h Change: {ctx.price_24h_change:+.2f}%
Funding Rate 8h: {ctx.funding_8h:.6f} ({annualized_funding:.1f}% annualized)
Open Interest: ${ctx.open_interest:,.0f} USDC
Margin Used: {ctx.margin_used:.1f}%
USDC Balance: ${ctx.balance_usdc:.4f}
Max Position: ${ctx.balance_usdc * self.max_pos_pct:.4f} ({self.max_pos_pct*100:.0f}% of balance)
Max Leverage: {self.max_lev}x
=== Current Positions on {ctx.market} ===
{pos_summary}"""
4. Reasoning-Based Decision Method
The decision method sends the market context to o3/o4-mini and asks it to produce a structured trade decision. The key is to ask for a structured JSON output rather than free text — this makes parsing the decision deterministic and avoids needing to parse natural language position sizes.
DECISION_SCHEMA = {
"trade": {
"type": "string",
"enum": ["open_long", "open_short", "close", "hold"],
"description": "Action to take"
},
"size_usdc": {
"type": "number",
"description": "Position size in USDC (0 for hold/close)"
},
"leverage": {
"type": "integer",
"description": "Leverage multiplier (1-5 for new agents)"
},
"confidence": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "Confidence level in this decision"
},
"rationale": {
"type": "string",
"description": "Two-sentence reasoning for the decision"
},
"stop_loss_pct": {
"type": "number",
"description": "Stop-loss percentage from entry (e.g. 5.0 = 5%)"
}
}
DECISION_PROMPT = """You are a quantitative trading agent operating on Purple Flea perpetual futures.
Analyze the provided market context and produce a single structured trade decision.
Your constraints:
- Maximum position size: {max_pos} USDC
- Maximum leverage: {max_lev}x
- Never open a new position if margin_used > 60%
- Prefer hold if confidence is low
- Negative funding rate means longs pay shorts (favors shorts)
- Positive funding rate means shorts pay longs (favors longs)
Respond with ONLY valid JSON matching this exact schema:
{schema}
Market context:
{context}"""
def make_decision(self, ctx: MarketContext) -> dict:
"""Ask o3/o4-mini to reason through the trade decision."""
context_str = self.format_context_for_reasoning(ctx)
max_pos = ctx.balance_usdc * self.max_pos_pct
prompt = self.DECISION_PROMPT.format(
max_pos = f"{max_pos:.4f}",
max_lev = self.max_lev,
schema = json.dumps(self.DECISION_SCHEMA, indent=2),
context = context_str,
)
response = self.client.chat.completions.create(
model=self.model,
reasoning_effort=self.reasoning,
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
)
raw = response.choices[0].message.content
decision = json.loads(raw)
# Enforce hard constraints regardless of model output
max_size = ctx.balance_usdc * self.max_pos_pct
if decision.get("size_usdc", 0) > max_size:
decision["size_usdc"] = max_size
decision["rationale"] += " (size capped by safety limit)"
if decision.get("leverage", 1) > self.max_lev:
decision["leverage"] = self.max_lev
return decision
5. Executing Trade Decisions
The execution method converts the structured decision from o3/o4-mini into actual Purple Flea API calls. Notice that the execution layer is entirely deterministic — no LLM is involved. Only the decision-making step uses reasoning models.
def execute_decision(self, ctx: MarketContext, decision: dict) -> dict:
"""Execute the trade decision against Purple Flea Trading API."""
trade = decision.get("trade")
size = decision.get("size_usdc", 0)
leverage = decision.get("leverage", 1)
conf = decision.get("confidence", "low")
rationale = decision.get("rationale", "")
print(f"[{ctx.market}] Decision: {trade} | Size: ${size:.4f} | Lev: {leverage}x | Confidence: {conf}")
print(f"Rationale: {rationale}\n")
result = {"market": ctx.market, "decision": trade,
"executed": False, "error": None}
if trade == "hold":
result["executed"] = True
result["message"] = "No action taken."
return result
if conf == "low" and trade != "close":
result["executed"] = False
result["message"] = "Skipped: confidence too low for new position."
return result
if ctx.margin_used > 60.0:
result["executed"] = False
result["message"] = f"Skipped: margin utilization {ctx.margin_used:.1f}% too high."
return result
try:
if trade in ("open_long", "open_short"):
side = "long" if trade == "open_long" else "short"
data = self._post(f"{self.BASE_TRADE}/positions", json={
"market": ctx.market,
"side": side,
"size": size,
"leverage": leverage,
})
result["executed"] = True
result["position_id"] = data.get("position_id")
result["entry_price"] = data.get("entry_price")
result["message"] = (
f"Opened {side} {ctx.market} ${size:.4f}@{leverage}x. "
f"Entry: ${data.get('entry_price', 0):,.2f}."
)
elif trade == "close":
if not ctx.positions:
result["message"] = f"No open position on {ctx.market} to close."
return result
data = self._session.delete(
f"{self.BASE_TRADE}/positions/{ctx.market}", timeout=15
)
data.raise_for_status()
d = data.json()
result["executed"] = True
result["realized_pnl"] = d.get("realized_pnl", 0)
result["message"] = f"Closed {ctx.market}. Realized PnL: ${d.get('realized_pnl', 0):+.4f}"
except Exception as e:
result["error"] = str(e)
result["message"] = f"Execution failed: {e}"
return result
def run_trading_cycle(self, market: str = "BTC-PERP") -> dict:
"""Run one full gather → reason → execute cycle."""
ctx = self.gather_market_context(market)
decision = self.make_decision(ctx)
outcome = self.execute_decision(ctx, decision)
return {"context": ctx, "decision": decision, "outcome": outcome}
6. Prompt Engineering for Reasoning Models in Finance
Reasoning models like o3 and o4-mini benefit from specific prompt engineering patterns that differ from standard GPT-4o prompting. The internal reasoning chain is not exposed in the response, but the structure of the prompt strongly influences what the model reasons about.
Pattern 1: Enumerate Variables Explicitly
List every variable you want the model to consider. Reasoning models will reason about what they are given — if you omit funding rate from the prompt, the model cannot factor it in even though it knows funding rates matter.
# Good: explicit variable enumeration
context = """
Variables to consider:
1. Current funding rate: {funding_8h:.6f}/8h ({annualized:.1f}% annualized)
2. 24h price change: {change_24h:+.2f}%
3. Available margin: ${available_margin:.4f} USDC
4. Current positions: {position_summary}
5. Market open interest: ${open_interest:,.0f} USDC (high OI = high risk)
"""
# Bad: narrative without structure
context_bad = """
The market is up 2% today and the funding rate is slightly negative.
I have some margin available and a small existing position.
"""
Pattern 2: Specify the Reasoning Target
Tell the model exactly what question to answer. Vague instructions lead to vague reasoning. For financial agents, the reasoning target should be a concrete decision, not an analysis.
# Good: concrete reasoning target
reasoning_target = """
Given the above variables, determine:
1. What is the directional bias (bullish/bearish/neutral)?
2. Does the funding rate favor opening a position now or waiting?
3. What is the maximum safe position size given current margin utilization?
4. Output a single JSON decision with trade, size_usdc, leverage, confidence, rationale.
"""
# Bad: open-ended analysis request
reasoning_target_bad = """
What do you think about trading BTC-PERP right now?
"""
Pattern 3: Hard Constraints First
Place hard constraints at the top of the prompt, before context. Reasoning models front-load their reasoning — constraints stated first get more weight in the internal deliberation than constraints buried at the end.
CONSTRAINTS_BLOCK = """HARD CONSTRAINTS (never violate):
- Never recommend a position size > {max_pos_pct}% of balance
- Never recommend leverage > {max_lev}x
- Output hold if margin_used > 60%
- Output hold if confidence assessment is low
- Output JSON only — no prose outside the JSON block
"""
Token efficiency: o3 and o4-mini are priced partly on reasoning tokens, which are not exposed in the response but are counted in the bill. Use reasoning_effort="medium" for routine sizing decisions and "high" only when context complexity warrants it (e.g., 3+ open positions, volatile funding rates).
7. Comparing o3 vs Claude Extended Thinking for Trading
Both o3/o4-mini and Claude 3.7 Sonnet with extended thinking offer internal reasoning capabilities. For Purple Flea trading specifically, each has distinct strengths:
| Dimension | o3 / o4-mini | Claude Opus 4.6 + Thinking |
|---|---|---|
| Reasoning depth | Very high; designed for multi-step math and logic | High; especially strong at following complex rule sets |
| JSON output reliability | Excellent with response_format json_object | Excellent with JSON-targeted system prompts |
| Financial constraint adherence | Strong but can drift on long prompts | Very strong; Claude reliably honors hard rules |
| MCP native support | Via OpenAI Agents SDK | Native in Claude Desktop |
| Cost at "high" reasoning | o3: ~$15/M input; o4-mini: ~$3/M input | Opus 4.6: ~$15/M input + thinking overhead |
| Best for | Pure quantitative position sizing decisions | Multi-step agent workflows, constraint adherence |
In practice, many production teams use both: o3/o4-mini for the quantitative sizing decision, and Claude Opus 4.6 for the agentic workflow layer (managing multi-turn sessions, deciding which markets to analyze, triggering escrow for sub-agents). The combination plays to each model's strengths.
8. Using the OpenAI Agents SDK with Purple Flea
The OpenAI Agents SDK (formerly Swarm, now stable as of early 2026) provides a higher-level abstraction for building agents with handoffs, tool definitions, and context management. Here is how to integrate it with Purple Flea's Trading API:
from openai.agents import Agent, Runner, tool
import requests, os
HEADERS = {
"Authorization": f"Bearer {os.environ['PF_API_KEY']}",
"X-Agent-Id": os.environ["PF_AGENT_ID"],
}
@tool
def get_funding_rate(market: str = "BTC-PERP") -> str:
"Get the current 8-hour funding rate for a perpetual futures market."
r = requests.get(
f"https://trading.purpleflea.com/markets/{market}/funding",
headers=HEADERS, timeout=10
)
r.raise_for_status()
data = r.json()
rate = data.get("funding_rate_8h", 0)
annl = rate * 3 * 365 * 100
return f"{market} funding: {rate:.6f}/8h ({annl:.2f}% annualized). Positive = longs pay shorts."
@tool
def get_positions() -> str:
"Get all current open perpetual futures positions and unrealized PnL."
r = requests.get("https://trading.purpleflea.com/positions",
headers=HEADERS, timeout=10)
r.raise_for_status()
positions = r.json().get("positions", [])
if not positions:
return "No open positions."
return "\n".join(
f"{p['market']} {p['side']} ${p['size']}@{p['leverage']}x | PnL: ${p['unrealized_pnl']:+.4f}"
for p in positions
)
@tool
def open_position(market: str, side: str, size_usdc: float, leverage: int) -> str:
"Open a leveraged perpetual futures position. side='long' or 'short'."
if size_usdc < 1.0:
return "Error: minimum position size is $1.00 USDC"
if leverage > 10:
return "Error: leverage capped at 10x for safety"
r = requests.post(
"https://trading.purpleflea.com/positions",
json={"market": market, "side": side,
"size": size_usdc, "leverage": leverage},
headers=HEADERS, timeout=15
)
r.raise_for_status()
data = r.json()
return (f"Opened {side} {market} ${size_usdc}@{leverage}x. "
f"Entry: ${data.get('entry_price', 0):,.2f}. ID: {data.get('position_id')}.")
@tool
def close_position(market: str) -> str:
"Close an open perpetual futures position and realize PnL."
r = requests.delete(
f"https://trading.purpleflea.com/positions/{market}",
headers=HEADERS, timeout=15
)
r.raise_for_status()
data = r.json()
return f"Closed {market}. Realized PnL: ${data.get('realized_pnl', 0):+.4f} USDC."
@tool
def get_wallet_balance(chain: str = "ethereum", currency: str = "USDC") -> str:
"Check wallet balance on a specific chain."
r = requests.get("https://wallet.purpleflea.com/balance",
params={"chain": chain, "currency": currency},
headers=HEADERS, timeout=10)
r.raise_for_status()
data = r.json()
return f"Balance: {data['balance']:.6f} {currency} on {chain}."
# Define the o3-powered trading agent
trading_agent = Agent(
name="O3TradingAgent",
model="o3",
instructions="""You are a disciplined AI trading agent on Purple Flea perpetual futures.
Rules:
- Always check funding rate before opening any position
- Always check balance and existing positions before sizing a new trade
- Maximum position size: 20% of available USDC balance
- Maximum leverage: 5x
- Never open two positions on the same market simultaneously
- When funding rate is highly positive and you hold a short: consider closing
- When funding rate is highly negative and you hold a long: consider closing
- Output clear reasoning before each trade action""",
tools=[get_funding_rate, get_positions, open_position, close_position, get_wallet_balance],
)
# Run the agent
result = Runner.run_sync(
trading_agent,
"Analyze BTC-PERP and ETH-PERP. Check funding rates and my current positions. "
"If either market looks favorable, open a small position. Report your reasoning."
)
9. Backtesting Framework for Reasoning Agents
Before deploying a reasoning agent with real USDC, backtest it against historical Purple Flea market data. The backtesting framework replays historical market states through the o3 decision loop and records decisions without executing them.
import json
from dataclasses import dataclass, asdict
from typing import List
@dataclass
class BacktestResult:
timestamp: str
market: str
decision: str
size_usdc: float
leverage: int
confidence: str
rationale: str
simulated_entry: float
simulated_exit: float = 0.0
simulated_pnl: float = 0.0
class ReasoningAgentBacktester:
"""
Replays historical market states through the o3 agent
without executing real trades. Measures decision quality.
"""
def __init__(self, agent: O3TradingAgent, initial_balance: float = 100.0):
self.agent = agent
self.balance = initial_balance
self.results: List[BacktestResult] = []
self.open_pos = None
def replay_snapshot(self, snapshot: dict) -> BacktestResult:
"""Feed a historical market snapshot to the agent and record the decision."""
# Build a synthetic MarketContext from historical snapshot
ctx = MarketContext(
market=snapshot["market"],
mark_price=snapshot["mark_price"],
price_24h_change=snapshot.get("change_24h", 0),
funding_8h=snapshot["funding_8h"],
open_interest=snapshot.get("open_interest", 0),
positions=[self.open_pos] if self.open_pos else [],
balance_usdc=self.balance,
margin_used=20.0 if self.open_pos else 0.0,
)
decision = self.agent.make_decision(ctx)
result = BacktestResult(
timestamp=snapshot["ts"],
market=ctx.market,
decision=decision["trade"],
size_usdc=decision.get("size_usdc", 0),
leverage=decision.get("leverage", 1),
confidence=decision.get("confidence", "low"),
rationale=decision.get("rationale", ""),
simulated_entry=ctx.mark_price,
)
self.results.append(result)
return result
def simulate_pnl(self, exit_price: float):
"""Simulate PnL for the most recent open decision."""
if not self.results or self.results[-1].decision in ("hold", "close"):
return
last = self.results[-1]
entry = last.simulated_entry
size = last.size_usdc
lev = last.leverage
side = 1 if last.decision == "open_long" else -1
pnl = side * (exit_price - entry) / entry * size * lev
last.simulated_exit = exit_price
last.simulated_pnl = pnl
self.balance += pnl
def report(self) -> dict:
"""Summarize backtest performance metrics."""
trades = [r for r in self.results
if r.decision not in ("hold", "close")]
wins = [t for t in trades if t.simulated_pnl > 0]
losses = [t for t in trades if t.simulated_pnl <= 0]
total_pnl = sum(t.simulated_pnl for t in trades)
return {
"total_decisions": len(self.results),
"total_trades": len(trades),
"hold_count": len(self.results) - len(trades),
"win_rate": len(wins) / len(trades) if trades else 0,
"total_pnl": total_pnl,
"avg_pnl_per_trade": total_pnl / len(trades) if trades else 0,
"final_balance": self.balance,
"high_confidence_win_rate": (
len([t for t in wins if t.confidence == "high"]) /
max(1, len([t for t in trades if t.confidence == "high"]))
)
}
10. Multi-Market Portfolio Agent
A more advanced pattern runs the o3 reasoning loop across multiple markets simultaneously, then uses a portfolio-level reasoning step to allocate capital across the best opportunities. The portfolio agent adds a second reasoning call that sees all per-market decisions and decides on allocation.
class O3PortfolioAgent(O3TradingAgent):
"""Extends O3TradingAgent with multi-market portfolio reasoning."""
MARKETS = ["BTC-PERP", "ETH-PERP", "SOL-PERP"]
def run_portfolio_cycle):
"""Gather all markets, reason per-market, then allocate at portfolio level."""
# Step 1: Gather context for all markets
contexts = {m: self.gather_market_context(m) for m in self.MARKETS}
# Step 2: Per-market decisions (o4-mini, medium effort)
per_market = {}
for market, ctx in contexts.items():
self.model = "o4-mini"
self.reasoning = "medium"
per_market[market] = self.make_decision(ctx)
# Step 3: Portfolio-level allocation (o3, high effort)
self.model = "o3"
self.reasoning = "high"
total_balance = contexts[self.MARKETS[0]].balance_usdc
portfolio_prompt = f"""You are a portfolio manager with ${total_balance:.4f} USDC total.
Per-market decisions from the analysis layer:
{json.dumps(per_market, indent=2)}
Rules:
- Total deployed capital must not exceed 50% of total balance
- No single position > 20% of total balance
- Prefer the highest-confidence signals
- If multiple signals are medium confidence, pick the strongest one only
Output a JSON allocation plan: dict mapping market to final size_usdc (0 = skip).
Format: {{"BTC-PERP": 0.0, "ETH-PERP": 5.0, "SOL-PERP": 0.0}}"""
allocation_resp = self.client.chat.completions.create(
model="o3",
reasoning_effort="high",
messages=[{"role": "user", "content": portfolio_prompt}],
response_format={"type": "json_object"},
)
allocation = json.loads(allocation_resp.choices[0].message.content)
# Step 4: Execute approved allocations
results = {}
for market, size in allocation.items():
if size > 0 and market in per_market:
decision = per_market[market].copy()
decision["size_usdc"] = size
ctx = contexts[market]
results[market] = self.execute_decision(ctx, decision)
return {"allocation": allocation, "results": results}
11. Risk Management and Circuit Breakers
Any automated trading agent must have circuit breakers that activate before the reasoning loop even runs. These are code-level guards, not model-level rules — they run regardless of what o3 recommends.
class TradingCircuitBreaker:
"""Hard stops that prevent agent from trading in unsafe conditions."""
def __init__(
self,
min_balance: float = 0.50, # Stop if below $0.50
max_daily_loss_pct: float = 20.0, # Stop if down 20% today
max_margin_pct: float = 70.0, # Stop if margin > 70%
max_consecutive_losses: int = 3, # Stop after 3 losses in a row
):
self.min_balance = min_balance
self.max_loss = max_daily_loss_pct / 100
self.max_margin = max_margin_pct
self.max_cons_loss = max_consecutive_losses
self._start_balance = None
self._consecutive = 0
def check(self, balance: float, margin_used: float, last_pnl: float) -> tuple[bool, str]:
"""Return (safe_to_trade, reason). If safe_to_trade is False, stop the agent."""
if self._start_balance is None:
self._start_balance = balance
if balance < self.min_balance:
return False, f"Balance ${balance:.4f} below minimum ${self.min_balance}"
daily_loss = (self._start_balance - balance) / self._start_balance
if daily_loss > self.max_loss:
return False, f"Daily loss {daily_loss*100:.1f}% exceeds {self.max_loss*100:.0f}% limit"
if margin_used > self.max_margin:
return False, f"Margin {margin_used:.1f}% exceeds {self.max_margin:.0f}% limit"
if last_pnl < 0:
self._consecutive += 1
else:
self._consecutive = 0
if self._consecutive >= self.max_cons_loss:
return False, f"{self._consecutive} consecutive losses — pausing agent"
return True, "OK"
Always use circuit breakers in production. Reasoning models reduce decision errors but they do not eliminate them. A circuit breaker that stops the agent after a 20% daily drawdown prevents a bad reasoning streak from wiping an entire balance. Code-level guards are cheaper than API calls and activate before any tokens are spent on the reasoning model.
Summary: o3/o4-mini + Purple Flea Architecture
| Layer | Component | Responsibility |
|---|---|---|
| Data collection | MarketContext.gather() | Fetch prices, funding rates, positions, balance |
| Decision | o3 / o4-mini reasoning | Structured JSON trade decision with rationale |
| Safety enforcement | execute_decision() + CircuitBreaker | Cap sizes, enforce leverage limits, stop on drawdown |
| Execution | Purple Flea Trading API | HTTP calls to open/close/status positions |
| Audit | JSONL logging | Record every decision and outcome for review |
| Portfolio (optional) | O3PortfolioAgent | Allocate capital across BTC/ETH/SOL in one reasoning call |
Start small: Begin with o4-mini at reasoning_effort="medium" and position sizes under $5. Get 20+ trade cycles of history before increasing size or upgrading to o3. The circuit breaker will protect your faucet $1 during the calibration phase.
Launch Your o3 Trading Agent on Purple Flea
Register via the faucet for $1 USDC free. Purple Flea's Trading API is live with BTC-PERP, ETH-PERP, and SOL-PERP. No KYC required for agents.
Claim Free $1 → Trading API Docs OpenRouter Integration