Writing Agents
This guide covers everything you need to build trading agents on Qynt Labs. Agents are written in Python using Backtrader, a popular backtesting framework. If you’re familiar with Python, you’ll feel right at home.
Agent Requirements
Before your agent can run on Qynt Labs, it must meet specific naming and structural requirements. The platform uses these conventions to locate and execute your code.
Every agent must follow these requirements:
| Requirement | Value |
|---|---|
| Filename | agent.py (exactly) |
| Class name | Agent (exactly) |
| Base class | bt.Strategy |
| Required methods | __init__(), prenext(), next() |
Basic Structure
Every agent follows the same basic template. You define parameters for configuration, initialize indicators in __init__(), and implement your trading logic in next(). The prenext() method ensures your agent can start trading before all indicators have accumulated enough data.
import backtrader as bt
class Agent(bt.Strategy):
"""Your agent description here."""
# Optional: define tunable parameters
params = (
('period', 20),
('threshold', 0.02),
)
def __init__(self):
"""Initialize indicators and state. Runs once at startup."""
self.sma = bt.indicators.SMA(self.data.close, period=self.p.period)
def prenext(self):
"""Called before all indicator data is available. Must call next()."""
self.next()
def next(self):
"""Called on each new bar. Your trading logic goes here."""
pass
The prenext() Method
Every indicator has a minimum period—the number of bars required before it can produce valid values. A 20-period SMA (Simple Moving Average) needs 20 bars of data before it can calculate a meaningful result. By default, Backtrader calls prenext() while accumulating data, and only calls next() once all indicators have reached their minimum period.
To start trading before all indicators are ready, implement prenext() to call next():
def prenext(self):
self.next()
Inside next(), check if your indicator has valid data before using it:
class Agent(bt.Strategy):
params = (
('sma_period', 20), # Number of bars for the Simple Moving Average
)
def __init__(self):
# Create the indicator in __init__
self.sma = bt.indicators.SMA(self.data.close, period=self.p.sma_period)
def prenext(self):
self.next()
def next(self):
# Check if SMA has enough data
if len(self.data) < self.p.sma_period:
return # Skip until we have enough data
# Now safe to use the indicator
if self.data.close[0] > self.sma[0]:
self.buy()
Parameters
Parameters let you configure your agent’s behavior without changing code. Define them as a tuple of name-value pairs, then access them throughout your agent using self.p or self.params. This makes it easy to experiment with different settings during backtesting.
Use the params tuple to define tunable parameters:
class Agent(bt.Strategy):
params = (
('fast_period', 10),
('slow_period', 30),
('size', 100),
)
def __init__(self):
# Access params with self.p or self.params
self.fast_ma = bt.indicators.SMA(period=self.p.fast_period)
self.slow_ma = bt.indicators.SMA(period=self.p.slow_period)
def next(self):
self.buy(size=self.p.size)
Accessing Data
Your agent receives data through self.data, which can include market prices, news, social media sentiment, geospatial data, and more. You can access the current bar using index [0] and previous bars using negative indices like [-1] for one bar ago.
Current and Historical Prices
def next(self):
# Current bar (index 0)
current_close = self.data.close[0]
current_open = self.data.open[0]
current_high = self.data.high[0]
current_low = self.data.low[0]
current_volume = self.data.volume[0]
# Previous bars (negative index)
previous_close = self.data.close[-1] # 1 bar ago
two_bars_ago = self.data.close[-2] # 2 bars ago
Data Length
def next(self):
# Number of bars processed so far
bars_available = len(self.data)
if bars_available < 20:
return # Not enough data yet
Indicators
Indicators are pre-built calculations that analyze price data to identify trends, momentum, and volatility. Backtrader includes dozens of built-in indicators that you can use in your agents. Always define indicators in __init__() so they’re calculated automatically as new data arrives.
Backtrader includes many built-in indicators. Define them in __init__():
Moving Averages
def __init__(self):
self.sma = bt.indicators.SMA(self.data.close, period=20) # Simple Moving Average
self.ema = bt.indicators.EMA(self.data.close, period=20) # Exponential Moving Average
self.wma = bt.indicators.WMA(self.data.close, period=20) # Weighted Moving Average
Momentum Indicators
def __init__(self):
self.rsi = bt.indicators.RSI(self.data.close, period=14) # Relative Strength Index
self.macd = bt.indicators.MACD(self.data.close) # Moving Average Convergence Divergence
self.stoch = bt.indicators.Stochastic(self.data)
Volatility Indicators
def __init__(self):
self.atr = bt.indicators.ATR(self.data, period=14) # Average True Range
self.bbands = bt.indicators.BollingerBands(self.data.close)
Using Indicators
def next(self):
# Access indicator values like data
current_sma = self.sma[0]
previous_sma = self.sma[-1]
# RSI example
if self.rsi[0] < 30: # Oversold
self.buy()
elif self.rsi[0] > 70: # Overbought
self.sell()
# Bollinger Bands
if self.data.close[0] < self.bbands.bot[0]:
self.buy()
See the Backtrader indicator reference for the full list.
Placing Orders
When your agent identifies a trading opportunity, it places orders using built-in methods like buy(), sell(), and close(). You can place simple market orders or specify prices for limit and stop orders. Orders are executed on the next available bar.
Basic Orders
def next(self):
# Market buy
self.buy()
# Market sell
self.sell()
# Close entire position
self.close()
Order with Size
def next(self):
# Buy 100 shares
self.buy(size=100)
# Sell 50 shares
self.sell(size=50)
Limit and Stop Orders
def next(self):
# Limit order: buy at specific price
self.buy(size=100, price=150.00, exectype=bt.Order.Limit)
# Stop order: sell if price falls to level
self.sell(size=100, price=145.00, exectype=bt.Order.Stop)
# Stop-limit order
self.buy(size=100, price=155.00, pricelimit=156.00,
exectype=bt.Order.StopLimit)
Position Management
Your agent needs to know its current holdings to make smart trading decisions. The self.position object tells you whether you’re in a trade, how many shares you hold, and at what average price. Use this information to avoid duplicate orders or to implement exit logic.
Checking Position
def next(self):
# Check if we have a position
if self.position:
print(f"Position size: {self.position.size}")
print(f"Average price: {self.position.price}")
else:
print("No position")
Position-Based Logic
def next(self):
# Only buy if not in a position
if not self.position:
if self.buy_signal():
self.buy()
# Only sell if in a long position
if self.position.size > 0:
if self.sell_signal():
self.sell()
Complete Example
This section brings everything together with a fully working agent. The example below uses Amazon’s Chronos time-series forecasting model to predict price movements. It looks at the last 20 bars of price data, generates a one-step-ahead forecast, and compares that prediction to the current price. If the model predicts the price will rise, it buys using 15% of available cash; if it predicts a decline while holding a position, it closes that position.
Warning:
This example is public and available to everyone, so don't expect it to give you an edge on its own. Use it as a starting point to understand the structure, then build your own unique agent with different models, indicators, and logic. The best-performing agents on the platform are the ones developers have customized and refined.
import backtrader as bt
from chronos import ChronosPipeline
import torch
class Agent(bt.Strategy):
params = (
('lookback', 20), # Number of bars to use for prediction
('model_name', 'amazon/chronos-t5-small'), # Chronos model variant
)
def __init__(self):
self.forecaster = ChronosPipeline.from_pretrained(
self.params.model_name,
device_map="auto",
torch_dtype=torch.float32
)
def prenext(self):
self.next()
def next(self):
for d in self.datas:
if len(d) < self.params.lookback:
continue
prices = list(d.close.get(size=self.params.lookback))
context = torch.tensor(prices).unsqueeze(0)
forecast = self.forecaster.predict(context, prediction_length=1)
predicted = forecast[0].mean().item()
pos = self.getposition(d).size
if not pos and predicted > d.close[0]:
size = int(self.broker.getcash() * 0.15 / d.close[0])
self.buy(data=d, size=size)
elif pos and predicted < d.close[0]:
self.close(data=d)
FMEL Explainability
Qynt Labs maintains a list of approved AI models that we have archived, ensuring we can always recreate exactly how your agent behaved. Our Foundation Model Explainability Layer (FMEL) automatically records every data field your agent accesses, the exact timestamps of each access, all orders placed and executed, and the portfolio state at each decision point. You don’t need to add any code—it just works.
This infrastructure enables explainability for AI-driven trading decisions. We handle all regulatory compliance questions on your behalf—you just build the agent.
Best Practices
Follow these guidelines to build robust, maintainable agents that perform well in backtesting and paper trading.
| Practice | Description |
|---|---|
Always implement prenext() |
Call self.next() to start trading immediately. |
| Check data availability | Verify indicators have enough data before using them. |
| Use parameters | Make your agent tunable via params. |
| Handle pending orders | Track order state to avoid duplicate orders. |
| Use approved models only | Agents using unapproved AI models will fail during backtesting. |
| Start simple | Get a basic agent working before adding complexity. |
Further Reading
Want to dive deeper? These resources provide comprehensive documentation on Backtrader’s features and a community forum where you can ask questions and share ideas.