Explore more topics
NexusTrade guide

Build and test a bull call spread

Define the two call legs, inspect the debit and width, and test a vertical options spread in NexusTrade.

As of 2026-10-13

Jump to a section

TL;DR

Build a bull call spread in four steps: describe it to Aurora, check the saved rules, backtest it, follow a trade. Start from the worked example below: copy its rules, then change the ticker, dates or exits and test your version. On SPY in 2024 it returned 26.49% against 25.30% for SPY, and its maximum drawdown was 14.19%.

How to build a bull call spread

Animation: the bull call spread strategy as built in the AI chat (Price(SPY) > SMA(SPY,50,Day); $1,000 dollars allocation; at or below SMA50; maxDte 14) is saved as 3 rules. Its legs (buy 1 call at ATM resolving to $472, sell 1 call at +5% resolving to $495; nearest expiry 30 to 45 days out, Feb 2, 2024) appear on a strike line. Then the expiration payoff of the recorded bull call spread (Buy the SPY $472 call, sell the $495 call; both expire Feb 2, 2024) draws as the SPY price sweeps from $455 to $512: max profit $1,500.99; max loss $799.01; breakeven $479.99; per contract before commissions. Then the same position's profit or loss is drawn at each session close from Jan 2, 2024 to Jan 19, 2024, ending at +$40.89 after commissions.
The algorithmic strategy held this position from Jan 2 to Jan 19, 2024 in its backtest and made $40.89 after commissions.
Step 1 of 4

1. Describe it to Aurora

Open the prepared request. Aurora receives the exact rules shown here and checks them without saving a portfolio. Review its preview before choosing whether to save or test your own version.

What you should see

A preview of the rules and their validation result. Any invalid inputs are identified before you decide what to save.

Step 2 of 4

2. Check the saved rules

Start with the actual rules used in this example. Days to expiration means the days remaining until the option expires. The complete editor inputs and SDK functions are available below.

Underlying
SPY
Entry
Open when SPY’s price is above its 50-day simple moving average and no matching option position is open.
Purchased call
Buy the call with a strike nearest SPY’s price. Choose the nearest available expiry 30 to 45 days away; use one contract per strategy unit.
Sold call
Sell the call targeting a strike 5% above SPY’s price. Use the same expiry range and one contract per strategy unit.
Exit
Close all matching option legs when SPY falls to its 50-day average or below, or when 14 or fewer days remain until expiration.
Allocation
Use a $1,000 allocation; only whole spread units that fit the resolved cost may open.
Starting capital
$10,000.00
What you should see

The entry decides when to open. The leg selectors choose contracts. The allocation sets size, and the exit decides when to close. Keep these parts together in your draft.

Completed 2024 backtest13.6K9.7K2024-01-022024-12-31
Options strategy 2024-12-31, 21:00:00 UTC
12,648.7
Full-capital SPY buy and hold 2024-12-31, 21:00:00 UTC
12,530.29
Options strategyFull-capital SPY buy and holdHistorical simulation
Completed 2024 historical simulation. The plotted observations and comparison series are preserved.
Step 3 of 4

3. Backtest it

Compare the two recorded curves and the result below. SPY invests the full starting balance; this options candidate uses its saved allocation. Their fees and invested capital differ.

Candidate vs full-capital SPY
26.49% vs 25.30%
What you should see

The requested period was 2024-01-01 through 2024-12-31. The candidate’s largest reported decline was 14.19%, with $46.80 in recorded commissions. Quote coverage is not certified for strategy comparison; inspect the retained quote checks before comparing these strategies.

Step 4 of 4

4. Follow one trade

Buying to open purchases an option; selling to open writes it. Closing reverses that position. Multiply each premium below by 100 shares per contract and its quantity, then subtract the recorded fees to follow the cash flow.

Call · $472 strike
Jan 2, 2024 14:31 UTC: buy to open 1 at $8.6544 per share; $0.65 fee. Jan 19, 2024 14:31 UTC: sell to close 1 at $8.6825 per share; $0.65 fee.
Call · $495 strike
Jan 2, 2024 14:31 UTC: sell to open 1 at $0.6643 per share; $0.65 fee. Jan 19, 2024 14:31 UTC: buy to close 1 at $0.2575 per share; $0.65 fee.
What you should see

The first position opened on January 2 and closed on January 19, 2024, at 14 DTE. The time-exit close evaluation emitted 2 orders. These are simulated Filled orders, not broker executions. The first position's P&L was $40.89 after recorded commissions, calculated from all its opening and closing premiums and quantities.

Four steps take a bull call spread from an idea to a tested strategy. Each step uses the SPY worked example.

The rules that define it

Show the table · 8 rows, 2 columns
InputSaved example
UnderlyingSPY
EntryPrice > SMA(50, Day) AND matching OptionPositionCount = 0
Purchased callATM; nearest expiration 30 to 45 days away; ratio 1
Sold call5% above ATM; same expiration selector; ratio 1
Premium allocation$1,000 dollars; whole contracts must fit the resolved debit
CloseWhole matching position when price <= SMA50, or at 14 or fewer DTE
Order executionMarket; actual fills and fees are not shown by an authoring preview
Product readback3 saved rules; inactive paper example; zero positions; automated approval disabled

Contract legs

Inspect contract legs

Both calls must use the same underlying, expiration and quantity. Select the lower strike for the purchased call and the higher strike for the sold call in the options builder.

Scroll sideways to compare all columns. Select a heading to sort.

Lower strikeBuy to openCall
Higher strikeSell to openCall

Payoff inputs

Inspect payoff inputs

For one share of underlying, maximum expiration loss is the net debit. Maximum expiration gain is the strike width minus that debit. The breakeven is the lower strike plus the debit. Scale by each contract's actual multiplier and quantity; fees reduce the payoff.

These formulas describe expiration payoff. Quotes, early exercise, assignment and closing costs affect the result before expiration. Do not treat a payoff diagram as a backtest.

Expiration payoff with round numbers

Show the payoff example

With a lower strike of $100, a higher strike of $110 and a $3 net debit per underlying share, expiration breakeven is $103. Maximum loss is $3 and maximum gain is $7 per share before fees. For a contract multiplier of 100, one spread has a $300 debit and a $700 maximum expiration gain before fees. These are hypothetical payoff inputs, not market quotes or a backtest result.

Save it as a draft

Show how to save the draft

Use the prepared Aurora request in the walkthrough above. Check the resolved rules before saving: match the flat-position entry condition and both close rules with the saved table. The returned preview is the object to inspect.

Prompt
Create a chat portfolio named Public SEO example - Bull call spread with $10,000 initial value. Do not deploy it or run a backtest. Underlying SPY. Open only when Price(SPY) > SMA(SPY,50,Day) and the number of matching long call vertical option positions on SPY equals zero. Use a $1,000 dollars allocation. Buy one ATM call with ratio 1, nearest expiration between 30 and 45 DTE. Add one short call 5% above ATM, ratio 1, using the same expiration and underlying, as a vertical spread. Add a close-all matching-spread rule when SPY price is at or below SMA50. Add a separate close-all matching-spread rule with maxDte 14. Create an undeployed research draft in this conversation and show its saved rules, selectors and returned identifier. Keep automated trading off.

Set the fields in the rule editor

Set the fields in the rule editor

A saved Aurora chat draft and a native paper portfolio have different inspection routes. Open Portfolio, choose Drafts, and preview your chat draft or open it in Aurora. The native strategies editor belongs to a paper or live portfolio; a chat draft ID is not a native editor ID. Keep an authoring example inactive while inspecting it.

In your inactive paper portfolio, choose Strategies and add or edit a rule. Select the Open Options Position action, then Bull Call Spread. Set Underlying Asset to SPY. The preset supplies ATM and 5% above ATM call legs; verify Min DTE 30, Max DTE 45, nearest expiration and each ratio 1. Under Allocation choose Type Dollars and Amount 1000.

Add the price/SMA50 and zero-position conditions to the opening rule. Add both Close Options Position rules separately. Saving an opening preset alone does not add these conditions or exits. The close rules match SPY and the configured spread type, and close all matching positions. Keep this example in its own draft; adding other matching SPY positions changes what those exits can close. Review the saved rules before any test.

Build the strategy

Inspect build the strategy

In the saved portfolio strategy editor, choose the Bull Call Spread preset, and define the underlying, expiry selection and strike selection for both legs. Inspect the generated contracts and allocation before saving the strategy.

Check that the saved spread still has one purchased lower-strike call and one sold higher-strike call with matching expiration and quantity. After the test, inspect the selected contracts and entry premiums against the configured selectors. If either leg lacks a quote or the run cannot resolve an expiration, fix the data or selector issue before interpreting the payoff. Backtest the saved configuration and review missing quotes, fills and expiry behavior. A live broker must support multi-leg options and the account must have the required trading approval.

Create and backtest with Python

Use the Python SDK instead of Aurora

Python SDK 1.42.0. Save the runner below as run_bull_call_spread.py.

Install and download

Shell
python3 -m pip install nexustrade==1.42.0
export NEXUSTRADE_API_KEY="YOUR_API_KEY"
curl --fail --output bull_call_spread.py https://nexustrade.io/seo/strategy-examples/bull-call-spread.py

Create, test and inspect

Python
from bull_call_spread import create_bull_call_spread, backtest_bull_call_spread

book = create_bull_call_spread()
completed = backtest_bull_call_spread()
print(completed["result"])

Run

Shell
python3 run_bull_call_spread.py

Python strategy and backtest functions

View complete Python SDK functions

The backtest function submits the 2024 test with SPY as its benchmark, records events and waits for the result. Use a new request key when changing rules, dates or fees.

Python
import nexustrade as nt


def create_bull_call_spread():
    asset = nt.stock_asset("SPY")
    return nt.portfolio(
        "Public SEO example - Bull call spread",
        [
            nt.strategy(
                "Open Bull call spread",
                ((nt.Price(asset) > nt.SMA(asset, 50, "Day")) & (nt.OptionPositionCount("SPY", "call", "long", "vertical") == nt.Value(0))),
                nt.open_option(
                    builder=nt.options_builder(
                        legs=[
                            nt.leg(
                                option_type="call",
                                direction="long",
                                min_days_to_expiration=30,
                                max_days_to_expiration=45,
                                preference="nearest",
                                distance=0,
                                ratio=1
                            ),
                            nt.leg(
                                option_type="call",
                                direction="short",
                                min_days_to_expiration=30,
                                max_days_to_expiration=45,
                                preference="nearest",
                                distance=5,
                                ratio=1
                            )
                        ],
                        underlying_symbol="SPY",
                        spread_type="vertical"
                    ),
                    allocation={'type': 'dollars', 'amount': 1000}
                )
            ),
            nt.strategy(
                "Close when trend breaks",
                (nt.Price(asset) <= nt.SMA(asset, 50, "Day")),
                nt.close_option(
                    underlyings=['SPY'],
                    spread_type="vertical",
                    close_scope="spread",
                    quantity={'type': 'all'}
                )
            ),
            nt.strategy(
                "Close at 14 DTE",
                (nt.Value(1) > nt.Value(0)),
                nt.close_option(
                    underlyings=['SPY'],
                    spread_type="vertical",
                    close_scope="spread",
                    quantity={'type': 'all'},
                    triggers=[nt.dte_trigger(max_dte=14)]
                )
            ),
        ],
        initial_value=10000,
        alerts_enabled=False,
    )


def backtest_bull_call_spread(client=None):
    client = client or nt.NexusTradeClient.from_environment()
    operation = client.create_backtest(
        nt.backtest(
            create_bull_call_spread(),
            start_date="2024-01-01", end_date="2024-12-31",
            interval="Day", baseline_symbol="SPY",
            initial_value=10000, generate_events=True,
            dividend_policy="cash",
            fee_config={"Stock": {"type": "percent", "amount": 0.1},
                        "Cryptocurrency": {"type": "percent", "amount": 0.7},
                        "Option": {"type": "dollars", "amount": 0.65}},
        ),
        idempotency_key="explore-bull-call-spread-2024-spy-v1",
    )
    return client.wait_for_backtest(operation["id"])


# Build locally: book = create_bull_call_spread()
# Run and inspect: completed = backtest_bull_call_spread(); print(completed["result"])

Create and backtest with TypeScript

Use the TypeScript SDK instead of Aurora

TypeScript SDK 1.42.0. Save the runner below as run-bull-call-spread.ts.

Install and download

Shell
npm install nexustrade@1.42.0
npm install --save-dev tsx
export NEXUSTRADE_API_KEY="YOUR_API_KEY"
curl --fail --output bull-call-spread.ts https://nexustrade.io/seo/strategy-examples/bull-call-spread.ts

Create, test and inspect

TypeScript
import { createBullCallSpread, backtestBullCallSpread } from "./bull-call-spread";

async function main() {
  const book = createBullCallSpread();
  const completed = await backtestBullCallSpread();
  console.log(completed.result);
}

main().catch((error: unknown) => {
  console.error(error);
  process.exitCode = 1;
});

Run

Shell
npx tsx run-bull-call-spread.ts

TypeScript strategy and backtest functions

View complete TypeScript SDK functions

The same rules, dates and fees are used in both SDKs. The backtest function keeps its request key across retries.

TypeScript
import * as nt from "nexustrade";

export function createBullCallSpread() {
  const asset = nt.stockAsset("SPY");
  return nt.portfolio(
    "Public SEO example - Bull call spread",
    [
      nt.strategy(
          "Open Bull call spread",
          nt.and(
              nt.gt(nt.Price(asset), nt.SMA(asset, 50, "Day")),
              nt.eq(nt.OptionPositionCount("SPY", "call", "long", "vertical"), nt.Value(0))
          ),
          nt.openOption({ builder: nt.optionsBuilder({ legs: [nt.leg({ optionType: "call", direction: "long", minDaysToExpiration: 30, maxDaysToExpiration: 45, preference: "nearest", distance: 0, ratio: 1 }), nt.leg({ optionType: "call", direction: "short", minDaysToExpiration: 30, maxDaysToExpiration: 45, preference: "nearest", distance: 5, ratio: 1 })], underlyingSymbol: "SPY", spreadType: "vertical" }), allocation: {"type": "dollars", "amount": 1000} })
      ),
      nt.strategy(
          "Close when trend breaks",
          nt.lte(nt.Price(asset), nt.SMA(asset, 50, "Day")),
          nt.closeOption({ underlyings: ["SPY"], spreadType: "vertical", closeScope: "spread", quantity: {"type": "all"} })
      ),
      nt.strategy(
          "Close at 14 DTE",
          nt.gt(nt.Value(1), nt.Value(0)),
          nt.closeOption({ underlyings: ["SPY"], spreadType: "vertical", closeScope: "spread", quantity: {"type": "all"}, triggers: [nt.dteTrigger({ maxDte: 14 })] })
      ),
    ],
    { initialValue: 10000, alertsEnabled: false }
  );
}

export async function backtestBullCallSpread(client = new nt.NexusTradeClient()) {
  const operation = await client.createBacktest(
    nt.backtest(createBullCallSpread(), {
      startDate: "2024-01-01", endDate: "2024-12-31",
      interval: "Day", baselineSymbol: "SPY",
      initialValue: 10000, generateEvents: true,
      dividendPolicy: "cash",
      feeConfig: {
        Stock: { type: "percent", amount: 0.1 },
        Cryptocurrency: { type: "percent", amount: 0.7 },
        Option: { type: "dollars", amount: 0.65 },
      },
    }),
    { idempotencyKey: "explore-bull-call-spread-2024-spy-v1" }
  );
  if (typeof operation.id !== "string") throw new Error("Missing backtest ID");
  return client.waitForBacktest(operation.id);
}

// Build locally: const book = createBullCallSpread();
// Run and inspect: const completed = await backtestBullCallSpread(); console.log(completed.result);

Start from this example

The worked example below is a starting point for you to copy. Open its exact rules in Aurora, then change the ticker, dates, legs or exits and backtest your own version.

Worked example to copy: SPY, 2024

Completed 2024 backtest13.6K9.7K2024-01-022024-12-31
Options strategy 2024-12-31, 21:00:00 UTC
12,648.7
Full-capital SPY buy and hold 2024-12-31, 21:00:00 UTC
12,530.29
Options strategyFull-capital SPY buy and holdHistorical simulation
Return
26.49%
SPY buy and hold
25.30%
Max drawdown
14.19%
Closed option legs
36
Commissions
$46.80; $0.65 per option contract
Show the table · 15 rows, 2 columns

Scroll sideways to compare all columns. Select a heading to sort.

StatusCOMPLETE
Pricing contract version8
Requested period2024-01-01 through 2024-12-31
Modeled return26.49%
Candidate maximum drawdown14.19%
Full-capital SPY buy and hold25.30%
Recorded fees$46.80; $0.65 per option contract
Option slippage contract0.5 of half-spread
Observed / synthetic half-spread calls78 / 74
Unknown-timing quote accesses10146
Rejected fill groups / marks0 / 0
Closed trades36
First held-option snapshot2024-01-02T21:00:00.000Z
Missing-current-quote checks0
First recorded option holdingsSPY240202C00472000: quantity +1, call strike $472, expiry 2024-02-02; SPY240202C00495000: quantity -1, call strike $495, expiry 2024-02-02

How the example did on SPY, 2024-01-01 to 2024-12-31, starting with $10,000.

One trade from the example

One position from the worked example, opened Jan 2, 2024 and closed Jan 19, 2024.

Long $472 call
+$2.81
Short $495 call
+$40.68
Commissions
−$2.60
Net
+$40.89

Explain this options decision with Aurora

Ask Aurora to connect the recorded signal, contract choice, allocation and fills. It receives this page's captured decision and order fields, so you can follow its explanation back to each value.

Inspect the worked example's records

The example trade's fills

  1. 2024-01-02 14:31:00

    Contract

    SPY240202C00472000

    Action

    Buy to open

    Contracts

    1

    Premium per share

    $8.6544

    Fee

    $0.65

  2. 2024-01-02 14:31:00

    Contract

    SPY240202C00495000

    Action

    Sell to open

    Contracts

    1

    Premium per share

    $0.6643

    Fee

    $0.65

  3. 2024-01-19 14:31:00

    Contract

    SPY240202C00495000

    Action

    Buy to close

    Contracts

    1

    Premium per share

    $0.2575

    Fee

    $0.65

  4. 2024-01-19 14:31:00

    Contract

    SPY240202C00472000

    Action

    Sell to close

    Contracts

    1

    Premium per share

    $8.6825

    Fee

    $0.65

Recorded rows in source order; spacing does not represent elapsed time.

Show the table · 4 rows, 6 columns

Scroll sideways to compare all columns. Select a heading to sort.

2024-01-02 14:31:00SPY240202C00472000Buy to open1$8.6544$0.65
2024-01-02 14:31:00SPY240202C00495000Sell to open1$0.6643$0.65
2024-01-19 14:31:00SPY240202C00495000Buy to close1$0.2575$0.65
2024-01-19 14:31:00SPY240202C00472000Sell to close1$8.6825$0.65

Each premium is per share; one contract is 100 shares.

Inspect the selected contracts and sizing

The candidate snapshot shows the bid and ask used in the sizing decision. Every selected leg had the February 2 expiration, 31 DTE at entry and multiplier 100.

The liquidity model kept the target. Allocation allowed 1 unit; the volume limit allowed 2. The requested and sent quantities were both 1. The opening premium also includes modeled liquidity impact beyond the persisted half-spread fraction.

Show the table · 2 rows, 6 columns

Scroll sideways to compare all columns. Select a heading to sort.

SPY240202C00472000$472.00$8.15$8.71$8.4311; trailingMedian5
SPY240202C00495000$495.00$0.66$0.68$0.67207; trailingMedian5

Inspect an adverse period in the published history

The largest peak-to-trough decline in the exported 519-point history was 13.60%: $13199.31 on 2024-07-16 to $11404.51 on 2024-09-10. At the trough the exported position value was $983.00.

The engine reported maximum drawdown of 14.19%. That differs from the decline calculated over the exported history points. Explore the snapshots and events around this period to understand the change in portfolio value.

Show the table · 2 rows, 5 columns

Scroll sideways to compare all columns. Select a heading to sort.

2024-07-16 20:00:00$13199.31$11894.81$1304.50$12001.33
2024-09-10 20:00:00$11404.51$10421.51$983.00$11697.13

Plan a separate variant and later test

Review the optional next experiment

After reviewing the recorded example, use the prepared request above for the original rules. This optional plan covers a separate variant and a later test.

  1. Create one variant, then test unseen dates

    Ask: "Change only the timed close trigger from 14 DTE to 10 DTE. Show the rule diff and compare both candidates on the 2024 development period with every other setting fixed." After choosing, freeze the rules and select another test period you have not inspected. The displayed 2024 results already make 2024 a viewed development period. Keep any later revisions in a new research cycle.

Explore the backtest events

Show what to check in the events

Choose a fixed period and record the displayed research-token cost before submitting an options test. Daily options tests can cost research tokens. Keep the saved rules, date range, starting capital, interval, fees and result receipt together. Inspect selected symbols, expirations, strikes, quote coverage and actual opening/closing events before discussing returns.

If no contract fits the selectors or $1,000 budget, no opening is expected. Missing quotes, an unresolved shared expiration or insufficient buying power require investigation. Explore the resolution and sizing events to understand how the rules were applied.

Continue exploring

Discussion

Sign in or create a free account to join the discussion.

No comments yet.

Aurora · AI research assistant

Try this in Aurora

Edit the request, then open it in Aurora. You choose when to send it.