Skip to main content
On this page

Backtrader Multiple Stocks: Multi-Asset Trading Strategies

· 15 min read
Pineify Team
Pine Script and AI trading workflow research team

A multi-stock strategy in Backtrader loads several stock data feeds into a single Cerebro engine so you can trade them all at once. Juggling multiple stocks in a backtest doesn't have to be a headache — Backtrader handles it by letting you load several data feeds into its main engine, Cerebro. Inside your strategy, you access each stock's data using simple references like self.data0 for the first stock, self.data1 for the second, and so on.

This setup opens the door to testing more advanced ideas. You can build strategies for an entire portfolio, experiment with pairs trading (where you trade one stock against another), or manage separate positions across different securities, all in a single run. The examples below illustrate feed references and order bookkeeping; they are not a completed portfolio backtest or reported trading experiment.

Backtrader Multiple Stocks: Complete Guide to Multi-Asset Trading Strategies

How Backtrader Handles Multiple Stocks at Once​

Backtrader orders feed delivery by timestamps, but it does not create missing observations or guarantee every feed has a new bar at every strategy step. Different holidays, listing dates or missing records can leave a feed's latest value older than the decision timestamp. Check each feed's length and datetime before using it for a cross-asset signal.

Define a freshness rule explicitly: for example, a daily close comparison might require both instruments to have the intended session date. Decide whether to skip a decision or use a documented prior observation when data is absent. See Backtrader's data synchronization explanation.

How to Add Multiple Stocks to Your Backtrader Strategy​

To test a trading strategy on more than one stock, you add multiple data feeds to Cerebro. It's like adding items to a list — repeat the same steps for each stock you want to include.

Create a data feed for each company and add it to Cerebro. The legacy Yahoo download example below demonstrates registration only; current endpoint access is not verified. For reproducibility, use saved vendor data through a compatible CSV or Pandas feed and record adjustment/session conventions. The fragment does not register a strategy or call run().

import backtrader as bt
from datetime import datetime

cerebro = bt.Cerebro()

# Add first stock (Apple)
data0 = bt.feeds.YahooFinanceData(
dataname='AAPL',
fromdate=datetime(2020, 1, 1),
todate=datetime(2024, 1, 1)
)
cerebro.adddata(data0, name='AAPL')

# Add second stock (Microsoft)
data1 = bt.feeds.YahooFinanceData(
dataname='MSFT',
fromdate=datetime(2020, 1, 1),
todate=datetime(2024, 1, 1)
)
cerebro.adddata(data1, name='MSFT')

# Add third stock (Google)
data2 = bt.feeds.YahooFinanceData(
dataname='GOOGL',
fromdate=datetime(2020, 1, 1),
todate=datetime(2024, 1, 1)
)
cerebro.adddata(data2, name='GOOGL')

Once they're added, you access each stock's data inside your strategy using a simple ordering:

  • The first one is self.data0 (or just self.data).
  • The second becomes self.data1.
  • The third is self.data2, and so on.

Giving each feed a clear name when you add it makes your code much easier to read when you're writing strategy logic later.

How to Work with Multiple Stocks in Your Trading Strategy​

When you're building a strategy, you often want to trade more than one stock at a time. Backtrader makes this straightforward. In your strategy's __init__ and next methods, you pull data from each stock individually. That lets you build separate indicators for each one or create rules that compare them against each other.

The following class creates three SMAs and reads prices. Its retained VIX comment is incorrect: self.data1 is MSFT in the preceding setup, so self.vix contains MSFT's close, not a volatility index. Do not use that assignment as a VIX filter. A real VIX input requires a separate, correctly identified feed and timestamp checks. This class places no orders.

class MultiStockStrategy(bt.Strategy):
def __init__(self):
# Create indicators for each stock
self.sma_aapl = bt.indicators.SMA(self.data0.close, period=20)
self.sma_msft = bt.indicators.SMA(self.data1.close, period=20)
self.sma_googl = bt.indicators.SMA(self.data2.close, period=20)

# VIX filter on second data feed
self.vix = self.data1.close

def next(self):
# Access current prices for all stocks
aapl_price = self.data0.close[0]
msft_price = self.data1.close[0]
googl_price = self.data2.close[0]

But what if you have a whole list of stocks? Writing manual lines for each one gets messy fast. A cleaner way is to loop through all the data you've loaded using self.datas. This is helpful for managing a portfolio with dozens of assets.

The loop lets you check each stock's current situation and make decisions based on your rules. To further improve your strategy development, you might check out Pine Script Wizard vs Pineify to see which platform can help simplify your coding and backtesting process, especially when translating ideas between different environments.

def next(self):
for i, d in enumerate(self.datas):
pos = self.getposition(d).size
current_price = d.close[0]

if not pos and self.should_buy(d):
self.buy(data=d)

This loop is a method fragment: should_buy() must be implemented. It omits timestamp checks, pending-order tracking, exits and sizing. Those requirements remain per instrument even when iteration is concise.

How Do You Manage a Portfolio with Multiple Stocks?​

When you're using Backtrader with several stocks, a key question comes up: how do you manage your portfolio? It's not just about picking winners — it's about how you divide your money between them. Here are a few practical approaches.

1. Equal Weighting Split your starting capital evenly across all stocks. Over time, as some stocks grow and others shrink, their weights change. You periodically rebalance — sell a bit of the winners and buy more of the laggards — to get back to equal. It's a neutral starting point.

2. Risk Parity Risk parity targets similar contributions to estimated portfolio risk. Inverse-volatility weights are a simpler approximation; equal risk contributions generally also depend on correlations. Estimates change and do not guarantee lower losses.

3. Signal-Based Rotation Your strategy generates a score for each stock — like a momentum rank. You rank them and put most of your money into the top few. This is common with momentum strategies that constantly shift capital to the strongest current signals.

4. Correlation-Based Pairs A pairs strategy tests a specified spread, hedge ratio and convergence hypothesis. High historical correlation alone does not establish a mean-reverting spread. Model short borrow availability, financing costs and the possibility of continued divergence.


A Modern Twist: Separating the "What" from the "How"​

You can calculate factors and target weights in a separate research process, then supply timestamped decisions to Backtrader. Every factor must use only information available at its decision time. Full-sample normalization, revised fundamentals or today's index membership can otherwise leak future information into the backtest.

Keep the universe point-in-time, including delisted instruments where relevant, and distinguish the signal timestamp from the earliest possible execution. Separating research and execution makes those assumptions inspectable, but does not automatically improve speed or correctness.

Pineify provides a separate workflow for creating TradingView Pine indicators, strategies and screeners. A Pine strategy tests orders for its chart instrument; it is not a replacement for a coordinated Backtrader multi-asset portfolio simulation or an automatic broker connection.

Pineify Website

Keeping Tabs on Your Trades Across Different Stocks​

When you're juggling trades in more than one stock, things get messy fast. You need a clear system to track what's ordered and what's still open. Backtrader gives you a few handy ways to stay organized.

Use a Dictionary Like Your Trading Sticky Notes Use a Python dictionary with each stock's data feed as a key to store orders and info. Think of it as a dedicated checklist for each stock.

def __init__(self):
self.orders = dict() # Track orders per data feed
self.holding = dict() # Track holding periods per data

def next(self):
for i, d in enumerate(self.datas):
if not self.orders.get(d, None): # No active orders
if self.signal_buy(d):
self.orders[d] = self.buy(data=d)

The dictionary fragment needs signal_buy() and a notify_order() implementation. Clear a stored reference only when that order reaches a terminal state; retain it through partial fills. For bracket orders, track all related order references rather than one value per feed. The unused holding dictionary does not calculate holding periods by itself.

Always Specify the Stock Every time you place an order, be explicit about the data feed. This tells Backtrader exactly which stock you're buying or selling.

self.buy(data=self.data0) # Buy AAPL
self.sell(data=self.data1) # Sell MSFT
self.close(data=self.data2) # Close GOOGL position

Set Up Bracket Orders Per Stock A bracket groups an entry with contingent stop and limit exits. Track their activation and cancellation separately for each feed. Gaps, slippage, rejected orders and adapter support can prevent the intended exit price; a bracket does not guarantee a maximum loss. See the bracket order reference.

How to Size Your Trades When Trading Multiple Stocks​

Figuring out how much to invest in each trade gets trickier with several stocks. You're not just thinking about one trade — you're thinking about your whole portfolio. The big questions: How much do I put into each position, and how do I keep total risk in check?

Here are a few straightforward ways to handle it.

Fixed Size for Every Stock The simplest approach. Decide on a set number of shares for every trade. But it doesn't account for different stock prices. Buying 100 shares of a $10 stock is very different dollar-wise than 100 shares of a $200 stock.

cerebro.addsizer(bt.sizers.FixedSize, stake=100)

Allocate a Percentage of Cash Instead of a fixed share count, allocate a fixed percentage of your available cash to each new position. For a new position, the sizer bases its quantity on available cash and price. This is not a target percentage of total portfolio equity and does not automatically rebalance existing holdings. Pending orders can also compete for the same cash; reserve a buffer for fees and gaps. See the sizers reference.

cerebro.addsizer(bt.sizers.PercentSizer, percents=10)

Build a Custom Sizer A custom sizer can implement explicit instrument-specific rules. The illustrative class below only returns fixed share counts; it ignores cash, commission, price, current position and isbuy. It is not risk-based sizing and can request unaffordable or unintended quantities.

class MultiStockSizer(bt.Sizer):
def _getsizing(self, comminfo, cash, data, isbuy):
if data._name == 'AAPL':
return 100
elif data._name == 'MSFT':
return 75
else:
return 50

Register the custom sizer explicitly if you intend to use it. Before adding portfolio logic, check the requested quantities against buying power and the desired position change; fixed share counts do not imply equal dollar or risk exposure.

Getting Your Multi-Stock Strategy Right​

Jumping into multi-stock strategies can be powerful, but small details cause big headaches. A few simple practices make your results more trustworthy. It's like packing for a hike — a little organization upfront prevents chaos down the trail.

Check these properties in the trade log:

  • Line up your timelines: Double-check that all your stock data covers the same dates. If one dataset ends earlier, your strategy might try to trade on missing data for other stocks, leading to weird errors.

  • Name your data streams: Give them clear names like AAPL and MSFT. It sounds simple, but when you're deep in debugging, you'll thank yourself for not having to guess which feed is which.

  • Keep orders straight: Use a simple dictionary to track orders for several stocks. Tag each order with the stock it belongs to. This prevents mix-ups where you think you're selling one stock but accidentally close another position.

  • Check your position before trading: Always check your current position for that specific stock before buying or selling. The strategy might say "buy," but if you're already holding a bunch, you might not want to.

  • Watch correlation: If all your stocks zoom up and crash down at the same time, you're not diversifying. You're concentrating risk in a different way. Historical correlations can change during stressed markets; separate ticker symbols do not ensure independent risk.

  • Start small, then expand: Don't test on 50 stocks right away. Begin with 2 or 3. Get the logic working, understand the system, then slowly add more.

  • Watch the whole portfolio: Focus on total portfolio value, not whether one stock is up 5%. A single stock might be doing great, but if everything else is tanking, your overall portfolio is still losing money. For optimizing your technical setup, check out the Best EMA for 5-Min Chart on TradingView for entry timing in fast-moving multi-stock environments.

Working with Multiple Stocks in Backtrader: Common Hurdles and How to Clear Them​

Backtesting with a full portfolio comes with its own headaches. These are common growing pains, and there are straightforward ways to handle them.

Data Misalignment​

You might pull data for ten stocks only to find their dates don't match. One stock trades on the NYSE, another has a different holiday schedule. Timestamp ordering does not make every observation current. Log each feed timestamp at the decision point and apply your freshness policy before comparing prices.

Memory Issues​

Loading years of historical data for dozens of stocks can slow your computer down. If your script is crawling, use a shorter date range for development and debugging. Once your logic is solid, scale up. For massive portfolios, look into data streaming to feed data in chunks.

Tracking Gets Tricky​

Managing one stock is simple. Managing twenty positions at once? Use Backtrader's self.datas iterator to loop through your assets systematically. Track each stock's state in a dictionary keyed by the data's name.

Slow Backtest​

More stocks and complex calculations mean longer backtest times. Profile the actual bottleneck before moving calculations outside the engine. Precomputed factors must remain causal. Compare results on a small reference dataset after any performance change; no speed improvement is assumed here.

A small portfolio validation exercise​

Start with two saved feeds and a deliberately missing date in one of them. Log feed lengths, timestamps, prices, signals and pending orders on each decision. Confirm the strategy skips or handles the missing observation according to its stated rule.

Next, submit candidate orders whose combined cost exceeds available cash and inspect the broker's rejection/margin notifications. Test a partial fill or a staged order-state sequence if your intended adapter supports it. Finally, reconcile cash plus marked positions with reported equity, including commissions and open positions at the final bar.

Record dataset versions, universe selection, portfolio constraints and execution assumptions before expanding the asset list. There is no verified 10-, 50- or 100-stock performance benchmark presented here.

Frequently Asked Questions​

What exactly counts as a multi-stock strategy in Backtrader?

A multi-stock strategy in Backtrader loads multiple stock data feeds into the Cerebro engine and trades them simultaneously within a single strategy. You can access each stock's price data, create individual indicators, and manage separate positions. This enables portfolio-level backtesting, pairs trading, and diversified algorithmic trading experiments.

How do I actually add multiple stocks to a Backtrader strategy?

Create data feeds using bt.feeds.YahooFinanceData (or your preferred data source) for each stock, then call cerebro.adddata() for each one. The first feed becomes self.data0, the second self.data1, and so on. Using a meaningful name parameter keeps your code organized and readable.

What's the difference between equal weighting and risk parity?

Equal weighting splits your capital evenly across all stocks and rebalances periodically. Risk parity targets comparable contributions to estimated portfolio risk, which generally depends on covariance as well as individual volatility. Inverse-volatility weighting is an approximation, not the general definition.

Can I go long on some stocks and short others in the same Backtrader strategy?

The simulator can represent long and short positions per feed. A sell can reduce an existing long or open/increase a short depending on size and current holdings. Actual short execution also requires broker permission, borrow availability and appropriate financing assumptions.

How do I track orders across multiple stocks in Backtrader?

Use a Python dictionary keyed by each data feed. Store order references with self.orders[d] = self.buy(data=d) and check for active orders using self.orders.get(d, None). Handle order notifications and clear terminal references; a dictionary alone does not update itself or prevent duplicate submissions.

Is there a limit to how many stocks I can add to a Backtrader strategy?

Memory, bar counts, indicator complexity and execution settings determine practical scale. Measure runtime and memory for your exact configuration before expanding; no universal stock-count capacity is established here.