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

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.
A preview of the rules and their validation result. Any invalid inputs are identified before you decide what to save.
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
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.
- 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
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%
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.
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.
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
| Input | Saved example |
|---|---|
| Underlying | SPY |
| Entry | Price > SMA(50, Day) AND matching OptionPositionCount = 0 |
| Purchased call | ATM; nearest expiration 30 to 45 days away; ratio 1 |
| Sold call | 5% above ATM; same expiration selector; ratio 1 |
| Premium allocation | $1,000 dollars; whole contracts must fit the resolved debit |
| Close | Whole matching position when price <= SMA50, or at 14 or fewer DTE |
| Order execution | Market; actual fills and fees are not shown by an authoring preview |
| Product readback | 3 saved rules; inactive paper example; zero positions; automated approval disabled |
No matching rows. Clear the filter to see all records.
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 strike | Buy to open | Call |
| Higher strike | Sell to open | Call |
No matching rows. Clear the filter to see all records.
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.
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
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
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
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.
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
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
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
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.
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
- 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
- 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.
| Status | COMPLETE |
| Pricing contract version | 8 |
| Requested period | 2024-01-01 through 2024-12-31 |
| Modeled return | 26.49% |
| Candidate maximum drawdown | 14.19% |
| Full-capital SPY buy and hold | 25.30% |
| Recorded fees | $46.80; $0.65 per option contract |
| Option slippage contract | 0.5 of half-spread |
| Observed / synthetic half-spread calls | 78 / 74 |
| Unknown-timing quote accesses | 10146 |
| Rejected fill groups / marks | 0 / 0 |
| Closed trades | 36 |
| First held-option snapshot | 2024-01-02T21:00:00.000Z |
| Missing-current-quote checks | 0 |
| First recorded option holdings | SPY240202C00472000: quantity +1, call strike $472, expiry 2024-02-02; SPY240202C00495000: quantity -1, call strike $495, expiry 2024-02-02 |
No matching rows. Clear the filter to see all records.
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
2024-01-02 14:31:00
ContractSPY240202C00472000
ActionBuy to open
Contracts1
Premium per share$8.6544
Fee$0.65
2024-01-02 14:31:00
ContractSPY240202C00495000
ActionSell to open
Contracts1
Premium per share$0.6643
Fee$0.65
2024-01-19 14:31:00
ContractSPY240202C00495000
ActionBuy to close
Contracts1
Premium per share$0.2575
Fee$0.65
2024-01-19 14:31:00
ContractSPY240202C00472000
ActionSell to close
Contracts1
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:00 | SPY240202C00472000 | Buy to open | 1 | $8.6544 | $0.65 |
| 2024-01-02 14:31:00 | SPY240202C00495000 | Sell to open | 1 | $0.6643 | $0.65 |
| 2024-01-19 14:31:00 | SPY240202C00495000 | Buy to close | 1 | $0.2575 | $0.65 |
| 2024-01-19 14:31:00 | SPY240202C00472000 | Sell to close | 1 | $8.6825 | $0.65 |
No matching rows. Clear the filter to see all records.
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.43 | 11; trailingMedian5 |
| SPY240202C00495000 | $495.00 | $0.66 | $0.68 | $0.67 | 207; trailingMedian5 |
No matching rows. Clear the filter to see all records.
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 |
No matching rows. Clear the filter to see all records.
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.
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.
No comments yet.