Deploy Strategies to a Live Environment

6 min read

Core idea

A backtest tells you a strategy can work. A live deployment proves it does. Closing the gap requires three additions to the trading app: real-time risk metrics computed continuously on a background thread, portfolio-target order primitives that translate "the portfolio should look like X" into actual orders against current positions, and a strategy loop that periodically — monthly, intraday, or event-driven — recomputes the target and dispatches the deltas. The topic demonstrates the pattern with three deployable strategies: a monthly factor portfolio, an options combo (straddle and iron condor), and an intraday multi-asset mean-reversion strategy using the crack spread.

Author's framing: Professional algorithmic traders spend most of their time analyzing and explaining deviations between backtest performance and live performance. The infrastructure for measuring those deviations is as important as the strategies themselves.

Why it matters

Real-time risk indicators close the validation loop

A backtest can be wrong. A strategy that earns a 1.8 Sharpe in simulation may earn 0.4 in production for reasons no backtest captures — execution timing, market-impact slippage, regime shifts. The empyrical-reloaded library provides forty-plus risk and performance metrics. Wired into the trading app on a background thread that polls portfolio PnL every five seconds, you get a live equity curve, a live max-drawdown, a live Sharpe ratio, a live Conditional Value at Risk — the same metrics the backtest reported, now computed from real fills against real prices. When the live Sharpe diverges meaningfully from the backtest Sharpe, you know within hours, not months.

Portfolio-target orders are the abstraction strategies actually want

Strategy code does not want to think in shares. It wants to say "AAPL should be 10% of the portfolio" or "MSFT should be three contracts." The primitives order_value, order_target_quantity, order_target_value, order_percent, and order_target_percent translate dollar amounts and percentages into share counts by combining the position dictionary (from the previous topic) with the live NetLiquidation value and the current market price. The _target_ variants compute the delta — if the position already exists, only the difference is traded. This is the same abstraction Zipline exposes in backtests, ported to live execution.

Monthly factor rebalancing is the simplest production pattern

Run after market close on the last trading day of the month. Pull a year of daily bars for ~20,000 equities. Compute the momentum factor through a Zipline Pipeline (already built in topic 5). Identify the top-N longs and bottom-N shorts. Compute equal weights. For each target, call app.order_target_percent(contract, market, side * weight). The strategy is trivially short — most of the work is in the pipeline and the data. The deployment infrastructure does the rest.

Options combos require multi-leg contract construction

A straddle is "long one call + long one put at the same strike, same expiry." An iron condor is "short put + long lower-strike put + short call + long higher-strike call." IB models these as a BAG contract — a Contract with secType=BAG and a comboLegs list referencing each leg's conId (resolved through reqContractDetails). One placeOrder submits all legs as a single execution; fills arrive grouped in the TWS orders pane. The pattern abstracts naturally: any multi-leg derivative position is a list of combo_leg(contract, ratio, action) calls wrapped in a spread([...]) BAG.

Intraday mean-reversion is a continuous-loop pattern

Rank one asset against its history. Rank another asset (or synthetic spread) against its history. Compute the z-score of the rank-spread over a rolling window. Trade when the z-score exceeds a threshold; exit when it crosses zero. The crack-spread strategy implements this against a refiner stock (PSX) and a 1:2:3 oil-products spread (HO + 2·RB − 3·CL). The infinite while True loop fetching one-minute bars and recomputing the signal is the production shape of every intraday relative-value strategy.

Key takeaways

Mental model

Mental model

Practical application

The live deployment pattern has four phases.

  1. Wire live risk metrics. Add a get_streaming_pnl generator on IBClient that polls get_pnl every N seconds (minimum 3, to respect IB's PnL refresh cadence). Add get_streaming_returns that consumes the PnL stream and maintains a portfolio_returns pandas Series. Spawn a daemon thread targeting get_streaming_returns in IBApp.__init__. Expose cumulative_returns, max_drawdown, volatility, sharpe_ratio, omega_ratio, and cvar as @property accessors that call empyrical functions on self.portfolio_returns.

  2. Build the portfolio-target order primitives. Each pair has a public method and a private helper: order_value / _calculate_order_value_quantity (dollar amount to shares via market price), order_target_quantity / _calculate_order_target_quantity (delta against current position), order_percent / _calculate_order_percent_quantity (percent of NetLiquidation), order_target_value, order_target_percent. The helpers compose: order_target_percent calls order_percent's helper, which calls order_value's helper.

  3. Pick a deployment pattern. Monthly factor (Zipline Pipeline → top-N / bottom-N → equal-weight order_target_percent loop). Options combo (option() factories → resolve_contractcombo_leg × N → spread([legs]) → single send_order). Intraday relative-value (infinite loop: fetch bars → compute signal → branch on signal vs threshold → send_order or order_target_percent(0)).

  4. Schedule appropriately. Monthly strategies run as cron jobs after market close. Intraday strategies run inside the trading process from market open. Options combos are typically event-driven (e.g., triggered by a volatility forecast). The interval parameter governs how often the strategy loop fires; pick it shorter than the signal's decay time but not so short that you fight IB's rate limits.

Monthly factor

The simplest pattern. Load a Zipline data bundle, run a Pipeline with a MomentumFactor (12-month return minus the most recent month, divided by realized volatility), select the top-N longs and bottom-N shorts, compute weight = 1 / top_n / 2 for equal weighting, then iterate:

for row in pd.concat([longs, shorts]).itertuples():
    side = 1 if row.longs else -1
    contract = stock(row.Index.symbol, "SMART", "USD")
    app.order_target_percent(contract, market, side * weight)

The pipeline does the work; order_target_percent makes the rebalance idempotent.

Options combo

A long straddle is two option() contracts (call and put at same strike/expiry), each resolved to a conId via app.resolve_contract, wrapped as combo_leg(contract, ratio=1, action=BUY), then spread([leg_1, leg_2]) produces a BAG contract. A single app.send_order(long_strangle, market(BUY, 1)) submits the entire structure.

A short iron condor adds two legs — short put + long farther-OOTM put + short call + long farther-OOTM call. Same pattern, four legs instead of two. The premium-collection sign means the bid/ask are negative (you receive money to enter).

Intraday mean-reversion

The crack-spread strategy runs an infinite loop:

while True:
    data = app.get_historical_data_for_many(
        request_id=99,
        contracts=[psx, ho, rb, cl],
        duration="1 W",
        bar_size="1 min",
    ).dropna()
    data["crack_spread"] = data.HO + 2 * data.RB - 3 * data.CL
    data["crack_rank"] = data.crack_spread.rolling(60).rank(pct=True)
    data["refiner_rank"] = data.PSX.rolling(60).rank(pct=True)
    data["rank_spread"] = data.refiner_rank - data.crack_rank
    roll = data.rank_spread.rolling(60)
    signal = ((data.rank_spread - roll.mean()) / roll.std())[-1]
    holding = psx.symbol in app.positions
    if signal <= -thresh and not holding:
        app.send_order(psx, market(BUY, 10))
    elif signal >= 0 and holding:
        app.order_target_percent(psx, market, 0)
    # symmetric short branch ...

Rolling rank normalizes the spread and the refiner onto a comparable 0-1 scale; z-score of the rank-difference is the trade signal; mean-reversion to zero is the exit.

Example

You backtest a sector-rotation strategy using monthly factor data and get an annualized Sharpe of 1.5 with a 14% max drawdown. You deploy it live with $100k of capital using order_target_percent for ten long and ten short positions, equal-weighted. The infrastructure is identical: same Zipline pipeline, same factor logic, same monthly rebalance.

In the first month, the live Sharpe is meaningless — one data point — but the live max-drawdown intraday metric registers a 3.1% intraday dip on day five. That matches the backtest distribution. By month four, the live Sharpe has stabilized at 0.9 — meaningfully lower than the backtest's 1.5. The CVaR is also worse. You run an ad-hoc analysis: live execution prices are systematically 6 bps worse than the backtest's assumed mid-price fills. Across forty rebalance trades a month, that's about 250 bps a year of slippage drag — almost exactly the Sharpe deterioration you're seeing.

Now you know what to fix. The strategy works; the execution doesn't. Either rebalance into the close instead of at the open, switch to limit orders sized below the spread, or switch to a less-active universe where market impact is smaller. None of those changes would have been visible without the live-metrics infrastructure. That feedback loop — backtest, deploy, measure, attribute, refine — is the actual job. The strategies are merely the substrate it runs on.

Continue exploring

Tags