> youcanbuildthings.com
tutorials books topics about

Multi Strategy Trading Bot Python: Risk Parity Allocator

by J Cook · 9 min read·

Summary:

  1. A multi strategy trading bot Python project lives or dies on the allocator. Six bots running on the same broker without coordination eat fees, drift in correlation, and conflict-trade themselves into a worse net P&L than any single strategy alone.
  2. Four mechanics: risk-parity capital sizing, rolling 90-day correlation drop rule, per-strategy drawdown circuit breaker (15% half / 25% zero), intent netting before the broker.
  3. Worked example: $98K total capital sized as Pairs $30K @ 6% vol, Cash-and-carry $21K @ 4% vol, Earnings $15K @ 12% vol, Trend $11K @ 16% vol, News $11K @ 14% vol, Flow $10K @ 14% vol.
  4. Below $25K total capital, run single broker (Alpaca + ccxt). Above $25K, split across Alpaca, IBKR, and ccxt by asset class.

Most retail builders ship one bot, then a second, then never write the layer that runs both at once. They run two bots independently on the same Alpaca account, and the first time the trend bot wants long 100 SPY while the news bot wants short 60 SPY, the broker double-trades, the spread eats fees, and the builder learns the hard way why the meta-strategy matters. Skip that lesson. The allocator goes in before any of the strategies touch live capital.

A hub-and-spoke architecture diagram. Center node: ALLOCATOR running risk-parity engine. Six surrounding strategy bubbles with explicit allocations and volatilities: Pairs $30K @ 6% vol, Cash-and-carry $21K @ 4% vol, Earnings $15K @ 12% vol, Trend $11K @ 16% vol, News $11K @ 14% vol, Flow $10K @ 14% vol. Below the allocator, a multi-broker routing layer connects to Alpaca, IBKR, and ccxt with a $25K threshold annotation. Right panel: unified P&L dashboard. Sidebar: risk parity sizes by volatility, not excitement. Footer: the boring bot gets the bigger check.

Why does a multi-strategy bot need an allocator?

Because six bots on the same broker without coordination is six bots trying to take opposite sides of the same trade. Strategy A says long 100 SPY at 10:31. Strategy B says short 60 SPY at 10:31. Both fill. You have a long-100-short-60 net long 40 position with full commission paid on both legs. The same exposure is one buy 40 SPY order with one set of fees. Half the cost.

The allocator does four things the broker cannot: risk-parity capital sizing, correlation-drop drift detection, per-strategy drawdown circuit breakers, and intent netting before the broker sees anything. Without these, you have six bots competing for the same fills, not a portfolio.

A user on r/algotrading said it more directly than I can (thread):

“you really want most of your logic to be deterministic… llms are good for structuring unstructured data into predefined schemas… use llms to structure news and other unstructured things, use them to help you fine tune the end to end algo but don’t use them as decision logic”

— u/n-7ity, r/algotrading “what do you think about this agent set up” thread (90 upvotes)

The allocator is the deterministic layer. It does not ask Claude how to size positions. It runs arithmetic on volatility and correlation, every bar, with no LLM in the loop.

How does risk-parity capital sizing actually work?

Each strategy gets capital inversely proportional to its 60-day daily-return volatility. The math:

def risk_parity_weights(strategy_vols: dict[str, float]) -> dict[str, float]:
    """strategy_vols: 60-day daily-return volatility per strategy.
    Returns capital weight as a fraction of total."""
    inv_vols = {s: 1.0 / v for s, v in strategy_vols.items()}
    total = sum(inv_vols.values())
    return {s: iv / total for s, iv in inv_vols.items()}

Worked example. Total strategy capital: $98K. The 60-day daily-return volatilities (annualized):

StrategyAnnualized volInverse volWeightNotional
Pairs6%16.730%$30,000
Cash-and-carry4%25.021%$21,000
Earnings12%8.315%$15,000
Trend16%6.311%$11,000
News14%7.111%$11,000
Flow14%7.110%$10,000

The cash-and-carry bot gets the most capital ($21K) because it has the lowest volatility per dollar. The flow bot gets the least ($10K) because each dollar of flow exposure swings hardest. Each contributes roughly the same daily-P&L variance to the portfolio.

Recompute weights every 60 trading days. The volatilities drift; the allocations drift with them. A strategy whose volatility doubles in the last 60 days gets its capital halved automatically. The allocator quietly de-risks the portfolio without you noticing.

The counter-intuitive result: the boring bot gets the bigger check. Risk parity inverts the rookie instinct of overweighting the strategy with the highest backtest return. Equal-risk-contribution is the math hedge funds use; it keeps a multi-strategy book from being secretly dominated by one volatile sleeve.

When should the allocator drop a strategy?

When two strategies have a 90-day rolling correlation above 0.70 for 30 consecutive days. The diversification benefit between them is gone. Running both is just extra brokerage fees and operational risk for the same exposure. Drop the lower-Sharpe member of the pair.

def correlation_drop(returns_df, threshold: float = 0.70, persistence_days: int = 30):
    """Returns the list of (strategy_a, strategy_b) pairs whose 90-day rolling
    correlation has stayed above `threshold` for at least `persistence_days`."""
    rolling_corr = returns_df.rolling(90).corr()
    flagged = []
    # iterate unique strategy pairs; check sustained breach
    for a, b in unique_pairs(returns_df.columns):
        breach_run = consecutive_days_above(rolling_corr[a][b], threshold)
        if breach_run >= persistence_days:
            flagged.append((a, b))
    return flagged

The 0.70 threshold is editorial. A more conservative value (0.50) drops strategies aggressively and keeps the portfolio aggressively diversified at the cost of fewer active strategies. A more permissive value (0.85) keeps strategies in longer at the cost of letting hidden correlation creep in. 0.70 is the threshold above which two equity strategies start behaving like one, and below which the diversification benefit is still meaningful. The 30-day persistence requirement filters out short-term correlation spikes that mean-revert.

When the rule fires, the dropped strategy’s capital weight goes to zero on the next risk-parity recomputation. Existing positions ride out their normal exit logic; no new entries are taken. The strategy goes on the bench. A bench strategy can rotate back in once correlations normalize.

How does the drawdown circuit breaker work?

Per-strategy month-to-date drawdown enforcement. Two thresholds, two actions.

def drawdown_circuit(strategy) -> float:
    """Multiplier applied on top of the risk-parity weight."""
    mtd_pct = strategy.mtd_pnl / strategy.month_start_equity
    if mtd_pct <= -0.25:
        if strategy.rolling_90d_sharpe < 0.5:
            return 0.0
        return 1.0
    if mtd_pct <= -0.15:
        return 0.5
    return 1.0

Down 15% MTD: half size. The strategy’s capital weight is halved on the next allocation cycle. Existing positions ride out; new entries take half the previous notional. The strategy stays in the portfolio with reduced influence while it works through the drawdown.

Down 25% MTD: zero size. The strategy’s capital weight is zeroed. New entries are blocked. The strategy stays at zero until its rolling 90-day Sharpe recovers above 0.5.

A primitive version of this rule shows up in retail tutorials as “if the bot is down 10 percent, it stops and writes a lock file you have to manually delete.” That is too brittle. Lock files require human intervention, which means the strategy stays dead for hours or days while you are busy. The allocator’s version is automated: half-size on the first threshold, zero on the second, automatic recovery when the rolling Sharpe argues the strategy is working again.

How does intent netting prevent double-trading?

Before any orders go to the broker, the allocator gathers each strategy’s intents for the next bar, sums them per symbol, and submits the net only:

from collections import defaultdict

def net_intents(intents):
    """intents: list of (strategy, symbol, qty, side) tuples.
    Returns the netted order list."""
    net_qty = defaultdict(int)
    for i in intents:
        sign = 1 if i.side == "buy" else -1
        net_qty[i.symbol] += sign * i.qty
    orders = []
    for symbol, qty in net_qty.items():
        if qty > 0:
            orders.append(("buy", symbol, qty))
        elif qty < 0:
            orders.append(("sell", symbol, abs(qty)))
    return orders

The allocator records the original intents for per-strategy P&L attribution but only submits the net. The broker sees one order per symbol per bar, not six. Each strategy’s P&L is computed from the marked-to-market value of its share of the position, weighted by its intent contribution.

Netting is not a decision; it is arithmetic. The allocator does not ask Claude “should I net these intents.” It nets them, deterministically, every bar.

When does multi-broker routing become worth it?

At $25,000 of total trading capital. Below that, run Alpaca + ccxt. Skip the futures legs of the trend bot (use SPY and QQQ ETFs only); skip Interactive Brokers entirely. The operational overhead of three brokers’ worth of API keys, margin accounts, statement reconciliations, and tax-lot tracking is not worth it under $25K, because the marginal capital-efficiency gain from accessing futures and forex is small relative to the time cost.

Above $25K, split by asset class:

  • Alpaca for equities and options (US-only, retail-friendly, free for equities).
  • Interactive Brokers for futures and forex. IBKR Lite is $0 per share for US-resident equities; IBKR Pro Tiered is $0.0005-$0.0035 per share. Use the per-share schedule, not basis-point conversions you find online.
  • ccxt for crypto. Binance and Bybit primarily; OKX or Hyperliquid optional.

The $25K threshold is a soft floor. The point is: do not split brokers because you can. Split brokers because the marginal capital efficiency from the additional asset classes exceeds the marginal operational complexity.

What broke

Three failure modes hit when you first run an allocator.

The correlation monitor lags by 30 days. A new regime starts Monday; two strategies become correlated by Wednesday; the allocator does not flag the pair until 30 days later. Real-time correlation prediction is a much harder problem; the allocator is honest about being lagged.

The drawdown circuit breaker fires on “a bad streak.” A strategy down 25% in a month is not a bad streak. It is broken in the current regime, and funding it bleeds the portfolio. The 0.5-Sharpe-recovery condition is the criterion for letting it back in. Do not lower it.

Intent netting eats a P&L attribution. Two strategies submit opposing trades on the same bar; the allocator nets them; the bar’s recorded P&L is hard to split between the two strategies. Weight each strategy’s marked-to-market position by its intent contribution, or your per-strategy Sharpe is noise.

What should you actually do?

  • If you run two or more strategies without an allocator → ship intent netting first. Highest-impact piece; expect a noticeable drop in commission cost in week one.
  • If you size by equal notional or by recent return → swap to risk parity this weekend. Five lines of code, more durable portfolio Sharpe.
  • If a strategy has been losing for two months and you keep funding it → wire the drawdown circuit breaker. The 25% MTD zero-out plus 0.5 rolling Sharpe recovery automates the painful decision.
  • If you cross $25K total capital → start the multi-broker split. The asset-class diversification is the real return; operational cost stops being prohibitive at that scale.
  • If you have not measured rolling 90-day correlations between your strategies → start now. The biggest hidden risk is two strategies you assumed were independent quietly converging.

bottom_line

  • The allocator is the strategy. The six bots are commodified; the allocator is where the retail edge actually lives.
  • Risk parity, not equal notional, not return-weighted. The math hedge funds use is the math you should use; the boring bot gets the bigger check.
  • Claude is not on the runtime hot path of the allocator. Netting, sizing, correlation, drawdown circuit breakers are arithmetic. If you find yourself prompting an LLM to “decide” any of these, the architecture is wrong.

Frequently Asked Questions

What is risk-parity sizing in a multi-strategy trading bot?+

Risk parity allocates capital inversely proportional to each strategy's recent volatility, so all strategies contribute roughly equal portfolio risk. The lowest-volatility strategy gets the most capital. The highest-volatility strategy gets the least. Equal-notional sizing across strategies is the rookie alternative; it lets the high-volatility strategy dominate the portfolio's risk profile.

When should the allocator drop a strategy from the portfolio?+

When two strategies have a 90-day rolling correlation above 0.70 for 30 consecutive days, drop the lower-Sharpe member. The diversification benefit is gone; running both is just extra brokerage fees and operational risk for the same exposure.

How does intent netting prevent strategies from double-trading?+

Before any orders go to the broker, the allocator gathers each strategy's intents per symbol per bar, sums them, and submits the net only. Strategy A's long 100 SPY plus Strategy B's short 60 SPY becomes one buy 40 SPY order at the broker. One set of fees instead of two.