Explore more topics
NexusTrade guide

Backtest an options trading strategy

Run a saved options draft with fixed dates, capital and fees. Explore the full curve, selected contracts and detailed trade events.

As of 2026-10-10

Jump to a section

TL;DR

Save the complete options rules before testing, then fix dates, capital and fees and inspect the curve, selected contracts and filled orders. The worked SPY long-call simulation returned 31.16% in 2024 with 13.27% maximum drawdown. SPY returned 25.30%, but invested the full balance with reinvested dividends and no strategy commission; the candidate used a $1,000 opening premium budget. Keep changes separate and freeze your chosen rules before testing unseen dates.

Inspect the completed SPY long-call test

Portfolio value (USD)

Completed 2024 backtest$14.2K$9.6KLargest decline in plotted observations: 13.24%, 2024-07-16, 20:00:00 UTC to 2024-09-06, 20:00:00 UTC2024-01-022024-12-31
Options strategy 2024-12-31, 21:00:00 UTC
$13,116.20
Full-capital SPY buy and hold 2024-12-31, 21:00:00 UTC
$12,530.29

Options strategy: 13.24% largest decline in plotted observations. 2024-07-16, 20:00:00 UTC ($13,235.24) to 2024-09-06, 20:00:00 UTC ($11,483.00).

Options strategyFull-capital SPY buy and holdHistorical simulation
Strategy return
31.16%
Maximum drawdown
13.27%
SPY return
25.30%

This saved long call completed a January 1 through December 31, 2024 backtest with $10,000 starting capital and interval Day. The result includes the full historical curve and the captured trade walkthrough near the top.

The SPY comparison invests the full starting balance, reinvests dividends and applies no strategy commission. This candidate uses a $1,000 option premium budget per opening and the fees shown here. Explore cash and position values to see how each deploys capital.

Use the pricing counters to explore observed and synthetic half-spread calls, including contract selection and sizing. Follow the detailed events below to connect entry conditions, selected contracts, quantities and fills.

Connect the result to a completed trade

  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-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 · 2 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-19 14:31:00SPY240202C00472000Sell to close1$8.6825$0.65

A separate detailed run completed on 2026-10-10 with the same saved candidate, fixed dates and fees. It reproduced the lightweight result retained in the linked source record. The captured opening condition had SPY at $472.16, SMA50 at $461.57 and matching-position count 0. Both entry checks were true.

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

Compare the selected-candidate bid, ask and mid with the Filled-order premium. Follow the contract symbols and timestamps to connect selection, sizing and execution.

Calculate what this recorded trade earned

The displayed premiums changed from $8.6544 to $8.6825. For 1 contract with multiplier 100, ($8.6825 − $8.6544) × 100 − $0.65 − $0.65 = $1.51 after the two recorded commissions. This calculation uses the displayed rounded premiums. The contract closed before expiration, so an expiration-payoff formula would answer a different question.

Its opening checks were SPY above SMA50 and zero matching option positions. The complete source preserves those indicator values, selected contract, quote, fill and close. Compare the first trade with the full-year curve before attributing the whole return to this one position.

Freeze the candidate before running it

Save and read back the strategy rules first. Record the selected underlying, strike and expiry rules, premium budget and exits. Choose a fixed period before looking at its result. Changing rules after reviewing returns creates a new candidate; keep the original result alongside the change.

Show the table · 7 rows, 2 columns
InputWorked examples
Dates2024-01-01 through 2024-12-31
Starting value$10,000 simulated capital
IntervalDay
BaselineSPY
Candidate dividendscash
Option commissions$0.65 per contract
Default option slippage0.5 of half-spread; liquidity impact may add cost

Submit the saved draft

In Aurora, ask to backtest your verified saved draft with these fixed inputs. Review the displayed research-token cost before submitting. Request detailed events when you need to inspect signals, contract resolution and orders. Detailed events cost 5 times the usual research tokens and remain available for 3 days after the run; capture the audit while it is retained.

Plan a separate variant and later test

Review the optional next experiment

Keep one changed variant separate from the original. After the development comparison, freeze a chosen version before selecting a later period that has not influenced its rules.

  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.

Create and backtest with Python

Use the Python SDK route

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

Install and download

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

Create, test and inspect

Python
from long_call import create_long_call, backtest_long_call

book = create_long_call()
completed = backtest_long_call()
print(completed["result"])

Run

Shell
python3 run_long_call.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_long_call():
    asset = nt.stock_asset("SPY")
    return nt.portfolio(
        "Public SEO example - Long call",
        [
            nt.strategy(
                "Open Long call",
                ((nt.Price(asset) > nt.SMA(asset, 50, "Day")) & (nt.OptionPositionCount("SPY", "call", "long", "custom") == 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)],
                        underlying_symbol="SPY",
                        spread_type="custom"
                    ),
                    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="custom",
                    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="custom",
                    close_scope="spread",
                    quantity={'type': 'all'},
                    triggers=[nt.dte_trigger(max_dte=14)]
                )
            ),
        ],
        initial_value=10000,
        alerts_enabled=False,
    )


def backtest_long_call(client=None):
    client = client or nt.NexusTradeClient.from_environment()
    operation = client.create_backtest(
        nt.backtest(
            create_long_call(),
            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-long-call-2024-spy-v1",
    )
    return client.wait_for_backtest(operation["id"])


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

Create and backtest with TypeScript

Use the TypeScript SDK route

TypeScript SDK 1.42.0. Save the runner below as run-long-call.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 long-call.ts https://nexustrade.io/seo/strategy-examples/long-call.ts

Create, test and inspect

TypeScript
import { createLongCall, backtestLongCall } from "./long-call";

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

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

Run

Shell
npx tsx run-long-call.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 createLongCall() {
  const asset = nt.stockAsset("SPY");
  return nt.portfolio(
    "Public SEO example - Long call",
    [
      nt.strategy(
          "Open Long call",
          nt.and(
              nt.gt(nt.Price(asset), nt.SMA(asset, 50, "Day")),
              nt.eq(nt.OptionPositionCount("SPY", "call", "long", "custom"), 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 })], underlyingSymbol: "SPY", spreadType: "custom" }), 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: "custom", closeScope: "spread", quantity: {"type": "all"} })
      ),
      nt.strategy(
          "Close at 14 DTE",
          nt.gt(nt.Value(1), nt.Value(0)),
          nt.closeOption({ underlyings: ["SPY"], spreadType: "custom", closeScope: "spread", quantity: {"type": "all"}, triggers: [nt.dteTrigger({ maxDte: 14 })] })
      ),
    ],
    { initialValue: 10000, alertsEnabled: false }
  );
}

export async function backtestLongCall(client = new nt.NexusTradeClient()) {
  const operation = await client.createBacktest(
    nt.backtest(createLongCall(), {
      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-long-call-2024-spy-v1" }
  );
  if (typeof operation.id !== "string") throw new Error("Missing backtest ID");
  return client.waitForBacktest(operation.id);
}

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

Keep the fee contract and result together

The SDK function submits the saved rules with the dates and fees shown above. Read the saved fee contract after completion to see the platform's option slippage setting. Opening orders may include liquidity impact as well as the configured half-spread cost. Inspect the recorded premiums alongside commissions to explore total trade costs.

In Aurora, ask for the saved test's status. Once it is complete, inspect the full history and trade events. If the run failed, inspect the reported error before submitting another test.

Prompt
For my completed backtest, report the saved rules, date range, interval, starting capital, fee contract, modeled return and maximum drawdown. Inspect the full history, opening and closing orders, selected contracts and option resolution audits. Distinguish Filled orders from signals and repeated order-status events. Do not rerun or change the candidate.

Understand the SPY comparison

The worked examples compare with full-capital fractional SPY buy and hold. That comparator reinvests dividends and applies no strategy commission. The candidate uses a $1,000 option premium budget per opening and its own fees. Different capital deployment, dividends and costs affect the comparison. Explore the cash and position-value history to understand the difference.

Completed strategy examples

Next: inspect the result

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.