Strategy

OpenAI o3/o4 Trading Agents: Reasoning-Driven Financial Decisions

March 6, 2026 25 min read Purple Flea Team

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