Running Autonomous AI Agents with Curupira

Running Autonomous AI Agents with Curupira

How Curupira orchestrates autonomous AI trading agents — from research to execution. Architecture overview, agent lifecycle, monitoring, and how to build your own agent on the platform.

curupiraagentsinfrastructureautomation

The Problem: From Research to Production

You’ve built a trading strategy. It backtests well. You’ve validated it out-of-sample. Now what?

For most retail quant traders, the answer is a painful multi-week process: write execution code, handle order management, build monitoring, set up alerts, manage API connections, handle errors, deploy to a server, and pray it doesn’t crash at 3 AM during a volatile move.

Curupira exists to collapse that gap. It’s the infrastructure layer that takes a strategy specification and runs it as an autonomous agent — handling execution, risk management, monitoring, and recovery so you can focus on research.

Architecture Overview

Curupira is built around three core principles: agent autonomy, hard risk boundaries, and full observability.

from dataclasses import dataclass, field
from enum import Enum

class AgentState(Enum):
    INITIALIZING = "initializing"
    RESEARCHING = "researching"
    WAITING = "waiting"
    ANALYZING = "analyzing"
    EXECUTING = "executing"
    MONITORING = "monitoring"
    ERROR = "error"
    SHUTDOWN = "shutdown"

@dataclass
class AgentConfig:
    """Configuration for an Curupira trading agent."""

    # Identity
    agent_id: str
    strategy_name: str
    description: str

    # Market configuration
    exchange: str                          # 'hyperliquid', 'binance', etc.
    symbols: list[str]                     # ['BTC-USD', 'ETH-USD']
    timeframe: str = '5m'                  # Primary analysis timeframe

    # Strategy specification
    strategy_module: str = ''              # Python module path
    analysis_type: str = 'quantitative'    # 'quantitative', 'llm', 'hybrid'
    analysis_interval_seconds: int = 300   # How often to run analysis

    # Risk boundaries (hard limits — agent cannot override)
    max_position_pct: float = 0.02         # Max 2% of capital per position
    max_total_exposure_pct: float = 0.06   # Max 6% total exposure
    max_daily_loss_pct: float = 0.03       # Stop trading at 3% daily loss
    max_daily_trades: int = 10             # Maximum trades per day
    max_drawdown_pct: float = 0.10         # Shutdown at 10% drawdown

    # Operational
    heartbeat_interval: int = 60           # Health check every 60s
    log_level: str = 'INFO'
    alert_channels: list[str] = field(default_factory=lambda: ['discord'])

The Agent Lifecycle

Every Curupira agent follows the same lifecycle, whether it’s running a pure quantitative strategy or an LLM-assisted hybrid:

import asyncio
import logging
from datetime import datetime

class CurupiraAgent:
    """
    Base class for all Curupira trading agents.
    Handles lifecycle, risk management, and monitoring.
    Strategy logic is injected via the strategy module.
    """

    def __init__(self, config: AgentConfig):
        self.config = config
        self.state = AgentState.INITIALIZING
        self.logger = logging.getLogger(f"agent.{config.agent_id}")
        self.daily_trades = 0
        self.daily_pnl = 0.0
        self.positions = {}
        self.start_time = datetime.utcnow()

    async def run(self):
        """Main agent loop."""
        self.state = AgentState.INITIALIZING
        await self._initialize()

        self.logger.info(f"Agent {self.config.agent_id} started: {self.config.strategy_name}")

        while self.state != AgentState.SHUTDOWN:
            try:
                # 1. Check risk limits before doing anything
                if not self._check_risk_limits():
                    self.state = AgentState.WAITING
                    await asyncio.sleep(self.config.analysis_interval_seconds)
                    continue

                # 2. Fetch market data
                self.state = AgentState.RESEARCHING
                market_data = await self._fetch_market_data()

                # 3. Run strategy analysis
                self.state = AgentState.ANALYZING
                signal = await self._analyze(market_data)

                # 4. Execute if signal is actionable
                if signal and signal.get('action') != 'HOLD':
                    self.state = AgentState.EXECUTING
                    await self._execute(signal)

                # 5. Monitor existing positions
                self.state = AgentState.MONITORING
                await self._monitor_positions()

                # 6. Wait for next cycle
                self.state = AgentState.WAITING
                await asyncio.sleep(self.config.analysis_interval_seconds)

            except Exception as e:
                self.state = AgentState.ERROR
                self.logger.error(f"Agent error: {e}")
                await self._handle_error(e)

    def _check_risk_limits(self) -> bool:
        """Hard risk limits that the agent cannot override."""
        if self.daily_trades >= self.config.max_daily_trades:
            self.logger.warning("Daily trade limit reached")
            return False

        if self.daily_pnl <= -self.config.max_daily_loss_pct:
            self.logger.warning("Daily loss limit reached")
            return False

        total_exposure = sum(
            abs(p.get('size_pct', 0)) for p in self.positions.values()
        )
        if total_exposure >= self.config.max_total_exposure_pct:
            self.logger.warning("Max exposure reached")
            return False

        return True

    async def _initialize(self):
        """Connect to exchange, load strategy, verify configuration."""
        pass  # Implementation specific to exchange

    async def _fetch_market_data(self):
        """Fetch OHLCV, orderbook, funding, OI data."""
        pass  # Implementation specific to exchange

    async def _analyze(self, market_data) -> dict:
        """Run strategy analysis — delegated to strategy module."""
        pass  # Loaded from config.strategy_module

    async def _execute(self, signal: dict):
        """Execute trade with position sizing and order management."""
        pass  # Implementation specific to exchange

    async def _monitor_positions(self):
        """Check stops, targets, and position health."""
        pass

    async def _handle_error(self, error: Exception):
        """Error recovery: alert, retry, or shutdown."""
        pass

The Risk Boundary System

The most critical design decision in Curupira: risk limits are enforced at the infrastructure level, not the strategy level. The agent’s strategy code cannot disable, modify, or bypass risk checks.

@dataclass
class RiskBoundary:
    """
    Immutable risk boundaries set at agent creation.
    These cannot be modified by the agent at runtime.
    Changes require a full redeploy with manual approval.
    """

    # Position limits
    max_position_size_usd: float
    max_total_exposure_usd: float
    max_positions: int = 3

    # Loss limits
    max_loss_per_trade_usd: float
    max_daily_loss_usd: float
    max_weekly_loss_usd: float
    max_total_drawdown_usd: float

    # Activity limits
    max_trades_per_hour: int = 5
    max_trades_per_day: int = 10
    min_time_between_trades_seconds: int = 60

    # Circuit breakers
    emergency_shutdown_loss_pct: float = 0.15
    max_slippage_pct: float = 0.5
    max_consecutive_losses: int = 8

    def validate_order(self, order: dict, portfolio: dict) -> tuple[bool, str]:
        """
        Validate a proposed order against all risk boundaries.
        Returns (approved, reason).
        """
        # Check position size
        if order['size_usd'] > self.max_position_size_usd:
            return False, f"Position size ${order['size_usd']:.0f} exceeds limit ${self.max_position_size_usd:.0f}"

        # Check daily loss
        if portfolio['daily_pnl'] <= -self.max_daily_loss_usd:
            return False, f"Daily loss limit reached: ${portfolio['daily_pnl']:.0f}"

        # Check consecutive losses
        if portfolio['consecutive_losses'] >= self.max_consecutive_losses:
            return False, f"Consecutive loss limit reached: {portfolio['consecutive_losses']}"

        # Check trade frequency
        if portfolio['trades_today'] >= self.max_trades_per_day:
            return False, f"Daily trade limit reached: {portfolio['trades_today']}"

        return True, "Approved"

This separation matters because autonomous agents, by definition, make decisions without human intervention. If the agent can modify its own risk limits, you don’t have risk management — you have suggestions. Curupira treats risk boundaries as immutable infrastructure, not parameters.

Monitoring and Observability

An autonomous agent that you can’t observe is a liability. Curupira provides three layers of observability:

1. Real-time state streaming. Every state transition, every analysis result, every order is logged and streamed to a monitoring dashboard.

2. Heartbeat monitoring. Agents send heartbeats every 60 seconds. If a heartbeat is missed, the monitoring system alerts and can trigger automatic position closure.

3. Decision audit trail. Every decision the agent makes — including decisions not to trade — is logged with the full context that informed it. This is critical for debugging and for improving strategies.

@dataclass
class DecisionLog:
    """Complete audit trail for every agent decision."""
    timestamp: str
    agent_id: str
    state: str
    market_context: dict        # What the agent saw
    signal: dict                # What the strategy produced
    risk_check: dict            # Risk validation result
    execution: dict             # What actually happened (or why it didn't)
    position_after: dict        # Portfolio state after decision
    latency_ms: float           # Total decision latency

Running Your First Agent

Here’s how to deploy a strategy on Curupira. We’ll use the entropy collapse strategy as an example:

# agent_config.yaml
agent_id: "entropy-eurusd-h1"
strategy_name: "Entropy Collapse Volatility Timing"
description: "Shannon entropy collapse detection on EURUSD H1"

exchange: "hyperliquid"
symbols: ["ETH-USD"]  # Using ETH as proxy for demo
timeframe: "1h"

strategy_module: "strategies.entropy_collapse"
analysis_type: "quantitative"
analysis_interval_seconds: 3600  # Every hour for H1

# Risk boundaries
max_position_pct: 0.01
max_total_exposure_pct: 0.03
max_daily_loss_pct: 0.02
max_daily_trades: 4
max_drawdown_pct: 0.08

alert_channels: ["discord"]
# strategies/entropy_collapse.py
from curupira.strategy import BaseStrategy
from curupira.signals.entropy import entropy_collapse_signal

class EntropyCollapseStrategy(BaseStrategy):
    """
    Entropy collapse strategy adapted for Curupira agent framework.
    """

    def __init__(self, lookback=50, threshold=1.8, bins=10):
        self.lookback = lookback
        self.threshold = threshold
        self.bins = bins

    async def analyze(self, market_data: dict) -> dict:
        df = market_data['ohlcv']

        # Compute entropy signal
        result = entropy_collapse_signal(
            df,
            lookback=self.lookback,
            entropy_bins=self.bins,
            collapse_threshold=self.threshold
        )

        latest = result.iloc[-1]

        if not latest['collapse_signal']:
            return {'action': 'HOLD', 'reason': 'No entropy collapse detected'}

        # Directional bias from EMA slope
        ema_slope = latest.get('ema_slope', 0)
        direction = 'LONG' if ema_slope > 0 else 'SHORT'

        return {
            'action': direction,
            'confidence': min(abs(latest['entropy_z']) / 3.0, 1.0),
            'stop_loss_atr': 1.5,
            'take_profit_atr': 2.0,
            'reasoning': f"Entropy z-score: {latest['entropy_z']:.2f}, EMA slope: {ema_slope:.6f}",
        }

What’s Next for Curupira

We’re building toward a platform where:

  • Strategy development uses our research tools (entropy, Hurst, FVG analysis) as composable building blocks
  • Agent deployment is a single command with sensible risk defaults
  • Multi-agent coordination allows agents to share information without conflicting positions
  • Community strategies can be deployed by anyone, with transparent performance tracking

The goal isn’t to build a black box. It’s to build infrastructure that makes transparent, reproducible, risk-managed autonomous trading accessible to anyone willing to do the research.


For the strategies that run on Curupira, see Entropy Collapse, FVG Magnetism, and our $1.30/Day LLM Agent. For the research philosophy, start with Why We Open-Source Our Quant Research.