Explore more topics
NexusTrade guide

Build and backtest an algorithmic covered call

Buy 100 SPY shares when stock holdings are empty, then write one higher-strike call during a bullish trend. Retain the stock when the call closes; inspect stock coverage and dividends separately.

As of 2026-10-13

Jump to a section

TL;DR

Build a covered call 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 11.81% against 25.30% for SPY, and its maximum drawdown was 4.71%.

How to build a covered call

Animation: the covered call strategy as built in the AI chat (Price(SPY) > SMA(SPY,50,Day); Buy 100 shares; Price(SPY) <= SMA(SPY,50,Day); 14 DTE) is saved as 4 rules. Its legs (sell 1 call at +5% resolving to $552; nearest expiry 30 to 45 days out, Jun 28, 2024) appear on a strike line. Then the expiration payoff of the recorded covered call (Sell the SPY $552 call expiring Jun 28, 2024 against 100 shares bought at $472.65) draws as the SPY price sweeps from $449 to $576: max profit $7,955.33; max loss $47,244.67 if SPY falls to $0; breakeven $472.45; per contract before commissions. Then the recorded session-close value of the 100 shares plus the short call is drawn from May 29, 2024 to Jun 14, 2024, when the call was bought back for a result of -$35.72 on the call after commissions.
The algorithmic strategy sold this call on May 29, 2024 and bought it back on Jun 14, losing $35.72 on the call 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.

Entry
The stock rule buys 100 SPY shares when the stock holding is empty. Separately, open the call when SPY is above its 50-day average, the stock holding is positive and no matching call position is open.
Position sizing
One strategy unit; leg ratios apply. Buying power, collateral and liquidity can restrict orders.
Signal 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.
Starting capital
$100,000.00
Leg 1
Sell 1 call contract per unit, targeting a strike 5% above SPY’s price. Choose the nearest available expiry 30 to 45 days away.
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.

Covered call: completed 2024 backtest129.9K99K2024-01-022024-12-31
Covered call 2024-12-31, 21:00:00 UTC
111,808.96
Full-capital SPY buy and hold 2024-12-31, 21:00:00 UTC
125,302.93
Covered callFull-capital SPY buy and holdHistorical simulation
This exact saved candidate completed its fixed 2024 Day simulation on 2026-10-10. Starting capital was $100000.00. The request enabled detailed events and cash dividends. The curve below contains all 519 exported history points. The SPY comparison invests the full starting balance, reinvests dividends and applies no strategy commission. This strategy requests one spread unit with its declared fees. Use the cash, position value and collateral history to explore how each deploys capital.
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
11.81% 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 4.71%, with $57.66 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 · $552 strike
May 29, 2024 13:31 UTC: sell to open 1 at $0.2033 per share; $0.65 fee. Jun 14, 2024 13:31 UTC: buy to close 1 at $0.5475 per share; $0.65 fee.
What you should see

These Filled orders show one captured position from the run. Its recorded opening and closing cash flows sum to $-35.72 after commissions. Inspect every leg and ratio; the engine's closed-trade count can count option legs rather than whole spread round trips.

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

The rules that define it

  1. Entry

    Price(SPY) > SMA(SPY,50,Day) AND positive SPY stock position; stock rule buys 100 shares when stock value = 0; the call-open rule separately requires zero matching option positions

  2. Position sizing

    One strategy unit; leg ratios apply. Buying power, collateral and liquidity can restrict orders.

  3. Signal exit

    Price(SPY) <= SMA(SPY,50,Day); close whole matching spread at 14 or fewer DTE

  4. Starting capital

    $100000.00

  5. Spread type

    custom

  6. Execution

    Market; $0.65 commission per option contract, default 0.5 half-spread fraction plus applicable liquidity impact.

Show the table · 6 rows, 2 columns
RuleSaved candidate
EntryPrice(SPY) > SMA(SPY,50,Day) AND positive SPY stock position; stock rule buys 100 shares when stock value = 0; the call-open rule separately requires zero matching option positions
Position sizingOne strategy unit; leg ratios apply. Buying power, collateral and liquidity can restrict orders.
Signal exitPrice(SPY) <= SMA(SPY,50,Day); close whole matching spread at 14 or fewer DTE
Starting capital$100000.00
Spread typecustom
ExecutionMarket; $0.65 commission per option contract, default 0.5 half-spread fraction plus applicable liquidity impact.

Contract selectors and ratios

Inspect contract selectors and ratios

Strike distances are percentages of the underlying price, not fixed market strikes or delta targets. Positive distance means out of the money: a higher strike for calls and a lower strike for puts. Negative distance means in the money; zero targets at the money (ATM). Days to expiration (DTE) measures the time remaining until a contract expires. Nearest expiration must lie within each leg's DTE range. Inspect the resolved contracts rather than assuming the authored offsets produce exact strike widths.

All option legs use the same requested expiry window. Verify actual shared expiration and the resulting quantities in the contract-resolution and fill records.

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

1shortcall5%130 to 45 DTE

Save it as a draft

Show how to save the draft

Ask Aurora to build the rules above, review the preview, then save the draft while keeping automated trading off. A chat draft is inspected in Portfolio > Drafts or Aurora; the native Strategies editor belongs to a paper or live portfolio.

In an inactive native portfolio, use Open Options Position, choose the matching structure preset and verify each leg above. Under Allocation, set Type Contracts and Amount 1. Add the entry conditions, including zero matching option positions. Add both Close Options Position rules separately, matching SPY and the spread type with close scope spread and quantity all. A preset adds contract legs; it does not supply the complete entry or exit algorithm.

Before the call rule, add a Buy rule for 100 SPY shares when PositionValue(SPY) equals zero. The call opening also needs PositionValue(SPY) greater than zero and the bullish trend condition. Keep the example inactive and verify the saved share rule and call-coverage checks together.

Create and backtest with Python

Inspect create and backtest with python

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

Install and download

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

Create, test and inspect

Python
from covered_call import create_covered_call, backtest_covered_call

book = create_covered_call()
completed = backtest_covered_call()
print(completed["result"])

Run

Shell
python3 run_covered_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_covered_call():
    asset = nt.stock_asset("SPY")
    return nt.portfolio(
        "Public SEO example - Covered call",
        [
            nt.strategy(
                "Buy 100 shares for call coverage",
                (nt.PositionValue(asset) == nt.Value(0)),
                nt.buy(asset, 100, "number of assets")
            ),
            nt.strategy(
                "Open Covered call",
                (((nt.Price(asset) > nt.SMA(asset, 50, "Day")) & (nt.OptionPositionCount("SPY", "call", "short", "custom") == nt.Value(0))) & (nt.PositionValue(asset) > nt.Value(0))),
                nt.open_option(
                    builder=nt.options_builder(
                        legs=[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="custom"
                    ),
                    allocation={'type': 'contracts', 'amount': 1}
                )
            ),
            nt.strategy(
                "Close when signal invalidates",
                (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=100000,
        supports_fractional_shares=False,
        alerts_enabled=False,
        dividend_policy='cash',
    )


def backtest_covered_call(client=None):
    client = client or nt.NexusTradeClient.from_environment()
    operation = client.create_backtest(
        nt.backtest(
            create_covered_call(),
            start_date="2024-01-01", end_date="2024-12-31",
            interval="Day", baseline_symbol="SPY",
            initial_value=100000, 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-covered-call-2024-spy-v1",
    )
    return client.wait_for_backtest(operation["id"])


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

Create and backtest with TypeScript

Inspect create and backtest with typescript

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

Create, test and inspect

TypeScript
import { createCoveredCall, backtestCoveredCall } from "./covered-call";

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

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

Run

Shell
npx tsx run-covered-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 createCoveredCall() {
  const asset = nt.stockAsset("SPY");
  return nt.portfolio(
    "Public SEO example - Covered call",
    [
      nt.strategy(
          "Buy 100 shares for call coverage",
          nt.eq(nt.PositionValue(asset), nt.Value(0)),
          nt.buy(asset, 100, "number of assets")
      ),
      nt.strategy(
          "Open Covered call",
          nt.and(
              nt.and(
                  nt.gt(nt.Price(asset), nt.SMA(asset, 50, "Day")),
                  nt.eq(nt.OptionPositionCount("SPY", "call", "short", "custom"), nt.Value(0))
              ),
              nt.gt(nt.PositionValue(asset), nt.Value(0))
          ),
          nt.openOption({ builder: nt.optionsBuilder({ legs: [nt.leg({ optionType: "call", direction: "short", minDaysToExpiration: 30, maxDaysToExpiration: 45, preference: "nearest", distance: 5, ratio: 1 })], underlyingSymbol: "SPY", spreadType: "custom" }), allocation: {"type": "contracts", "amount": 1} })
      ),
      nt.strategy(
          "Close when signal invalidates",
          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: 100000, supportsFractionalShares: false, alertsEnabled: false, dividendPolicy: "cash" }
  );
}

export async function backtestCoveredCall(client = new nt.NexusTradeClient()) {
  const operation = await client.createBacktest(
    nt.backtest(createCoveredCall(), {
      startDate: "2024-01-01", endDate: "2024-12-31",
      interval: "Day", baselineSymbol: "SPY",
      initialValue: 100000, 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-covered-call-2024-spy-v1" }
  );
  if (typeof operation.id !== "string") throw new Error("Missing backtest ID");
  return client.waitForBacktest(operation.id);
}

// Build locally: const book = createCoveredCall();
// Run and inspect: const completed = await backtestCoveredCall(); 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

Covered call: completed 2024 backtest129.9K99K2024-01-022024-12-31
Covered call 2024-12-31, 21:00:00 UTC
111,808.96
Full-capital SPY buy and hold 2024-12-31, 21:00:00 UTC
125,302.93
Covered callFull-capital SPY buy and holdHistorical simulation
Return
11.81%
SPY buy and hold
25.30%
Max drawdown
4.71%
Closed option legs
8
Commissions
$57.66
Show the table · 16 rows, 2 columns

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

StatusCOMPLETE
Requested period2024-01-01 through 2024-12-31
Starting capital$100000.00
Modeled return11.81%
Reported maximum drawdown4.71%
Full-capital SPY comparator return25.30%
Recorded commissions$57.66
Engine closed-trade count8
Peak reserved collateral$0.00
Median reserved collateral while holding positions$0.00
Pricing contract version8
Observed / synthetic half-spread calls16 / 26
Unknown-timing quote accesses24080
Missing-current-quote checks0
Rejected fill groups / marks0 / 0
Stale basket / single-option marks0 / 0

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

One trade from the example

One position from the worked example, opened May 29, 2024 and closed Jun 14, 2024.

Short $552 call
−$34.42
Commissions
−$1.30
Net
−$35.72

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-05-29 13:31:00

    Contract

    SPY240628C00552000

    Action

    Sell to open

    Contracts

    1

    Premium per share

    $0.2033

    Fee

    $0.65

  2. 2024-06-14 13:31:00

    Contract

    SPY240628C00552000

    Action

    Buy to close

    Contracts

    1

    Premium per share

    $0.5475

    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-05-29 13:31:00SPY240628C00552000Sell to open1$0.2033$0.65
2024-06-14 13:31:00SPY240628C00552000Buy to close1$0.5475$0.65

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

Inspect the candidate book

Inspect inspect the candidate book

This is the indicative selected-candidate snapshot from the opening size decision. Compare the selected symbols and ratios with the actual Filled orders. Use the candidate bid, ask and mid to follow the sizing calculation, then compare the recorded execution premium.

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

SPY240628C00552000$552.002024-06-2830$0.20$0.22$0.21136; trailingMedian5
JSON
{
  "modelVersion": 2,
  "enforced": true,
  "chosenIndex": 0,
  "path": "targetKept",
  "reason": "target size limit 9 covers 1",
  "requestedQuantity": 1,
  "quantity": 1,
  "sizingChain": [
    {
      "kind": "allocationImplied",
      "bound": 1,
      "quantity": 1,
      "binding": true
    },
    {
      "kind": "buyingPowerCap",
      "bound": 1,
      "quantity": 1,
      "binding": false
    },
    {
      "kind": "coveredCallShareCap",
      "bound": 1,
      "quantity": 1,
      "binding": false
    },
    {
      "kind": "volumeLimit",
      "bound": 9,
      "quantity": 1,
      "binding": false
    },
    {
      "kind": "finalPriceRecap",
      "bound": 1,
      "quantity": 1,
      "binding": false
    }
  ],
  "pricing": {
    "mid": 0.21000000000000002,
    "halfSpread": 0.009999999999999995,
    "participation": 0.027777777777777776,
    "impactCost": 0.16666666666666655
  }
}

Inspect the stock coverage

  1. 2024-01-02T21:00:00.000Z

    Asset

    SPY

    Side

    Buy

    Shares

    100

    Price

    $472.65

    Fee

    $47.26

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

Show the table · 1 row, 6 columns

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

2024-01-02T21:00:00.000ZSPYBuy100$472.65$47.26

These separate Filled stock orders show the share purchase used by this covered-call candidate. A purchased share remains exposed after a call closes. Verify the holding and the resolver's coverage check at each call entry.

Inspect a contract-resolution audit

Inspect inspect a contract-resolution audit

This is the latest retained resolution attempt, which can belong to a different position than the matched trade above. Its outcome, capital and risk codes explain what the resolver accepted or rejected at that decision. Follow the resulting Order events to see its fill status.

JSON
{
  "time": "2024-12-26T21:00:00.000Z",
  "status": "rejected",
  "reason": "Cannot afford even 1 contract: resolved quantity is 0 (max loss ≈ $60073.00 per spread unit vs effective allocation). Increase allocation, use contract-based sizing, or choose a structure with lower capital per contract.",
  "riskAudit": {
    "rulesetVersion": 1,
    "outcome": "rejected",
    "ruleCodes": [
      "RESOLVER_REJECT"
    ],
    "longSharesAvailable": 100,
    "unhedgedShortCallContractsPerSpread": 0,
    "buyingPowerEffective": 53200.957716873825,
    "allocationRequested": 1,
    "allocation": {
      "type": "contracts",
      "amount": 1
    },
    "portfolioValue": 113334.9604024207,
    "underlyingPrice": 601.3400268554688,
    "committedCostBefore": 0
  }
}

Inspect the largest decline in the exported history

Scale: 0 to 53.03K USD · zero baseline

Cash

  • 2024-07-16T20:00:00.000Z$52850.50
  • 2024-08-05T13:30:00.000Z$53026.40

Scale: 0 to 56.49K USD · zero baseline

Position value

  • 2024-07-16T20:00:00.000Z$56486.00
  • 2024-08-05T13:30:00.000Z$51164.00

Scale: 0 · zero baseline

Reserved collateral

  • 2024-07-16T20:00:00.000Z$0.00
  • 2024-08-05T13:30:00.000Z$0.00
Show the table · 2 rows, 4 columns

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

2024-07-16T20:00:00.000Z$52850.50$56486.00$0.00
2024-08-05T13:30:00.000Z$53026.40$51164.00$0.00

The exported grid's largest peak-to-trough decline was 4.71%, from $109336.50 at 2024-07-16T20:00:00.000Z to $104190.41 at 2024-08-05T13:30:00.000Z. The engine reported maximum drawdown of 4.71%. Compare the history snapshots with the reported statistic and inspect the events around this period.

Create, test and iterate in Aurora

Start this worked example in the NexusTrade app. Review the filled-in request, open it in Aurora, then send it when ready. Continue in the same conversation through these steps.

  1. Create the exact strategy

    Ask: "Review Covered call. Entry: Price(SPY) > SMA(SPY,50,Day) AND positive SPY stock position; stock rule buys 100 shares when stock value = 0 AND zero matching option positions. Exit: Price(SPY) <= SMA(SPY,50,Day); close whole matching spread at 14 or fewer DTE. Show the resolved rules and selectors before saving." The request includes this example's complete draft.

  2. Run the 2024 development comparison

    Ask: "Backtest the saved rules from January 1 through December 31, 2024, daily, starting with $100,000. Keep the displayed fees and capture detailed events." Compare the completed curve with SPY buy and hold.

  3. Explain what happened

    Ask: "Explain one entry signal and its Filled orders using the recorded indicator values, comparisons, allocation, prices, quantity and fees. Then follow the largest drawdown through trades, cash and holdings."

  4. 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 this strategy further

Inspect explore this strategy further

The stock rule buys 100 SPY shares only when the SPY stock position is empty. The call rule waits for positive stock value and a bullish trend, then requests one short call. The execution model must still enforce actual share coverage. Closing the call retains stock exposure. Stock commissions and cash dividends also affect this result; inspect the stock order, held shares and any exercise outcome.

Try another date range or change one rule, then compare the new backtest with this saved 2024 result. Inspect which trades and periods explain the difference.

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.