Skip to content

Build option recipes from a supplied chain

The Python builders select contracts from an OptionChain you supply and return a signed composite recipe. They do not fetch a chain, calculate missing deltas, certify complete data coverage or establish that the recipe can execute.

Import these legacy composite builders explicitly from atom.library.structures. They are distinct from the typed Python bridge below and TradeScript's named structure blocks; a similarly named family does not make a Composite executable.

Supply the decision date and selection inputs

Pass today=date(...) for pure construction. Missing decision dates now refuse with ATOM-OPTION-CLOCK; the old fallback to the earliest expiry is removed. The dispatch fallback uses the bar's UTC date. A naive timestamp means UTC; an offset-aware timestamp is converted to UTC before taking its date. A declared market timezone cannot change that instant or date. For example, 2026-01-02 00:30 UTC selects January 2, even though New York is still January 1. Previously this fallback required a market timezone and could choose another day.

dte is a nonnegative integer number of calendar days from that date, default 45. Only expiries on or after the decision date and within dte - 7 through dte + 7, inclusive, are considered. Candidates are searched by distance to target DTE, then earlier expiry, until a complete requested geometry exists. An expiry dated today is eligible for construction; this is not evidence that its trading cutoff has not passed.

Delta recipes read supplied finite deltas in the appropriate call/put range. Calls default to 0.16, puts to -0.16; condors use short_delta=0.16 for the call and its negative for the put. Selection tolerance is inclusive 0.05. Missing, nonfinite, wrong-sign or out-of-range deltas cannot supply a match. Invalid selector values refuse with ATOM-OPTION-GEOMETRY. Ties use delta distance, expiry, strike, then contract symbol (the supplied stable ID). near_atm similarly orders by decimal strike distance, expiry, strike and symbol. For spot 100.2 and equal-expiry strikes 100.1/100.3, it selects 100.1 first; binary floating-point subtraction cannot break that mathematical tie. Neither ordering depends on chain input order. Contract IDs must be nonempty and unique, and every leg must belong to the chain's stated underlying. Mixed-underlying or duplicate-ID chains refuse with ATOM-OPTION-GEOMETRY rather than producing an ambiguous recipe.

Choose the geometry and direction

All single-expiry recipes require shared expiry; each leg uses its stated right. ATM means the eligible anchor nearest the chain's finite positive spot, with ties resolved by strike and symbol. Searches can use a farther anchor when the closest one cannot supply a complete geometry. Widths are positive finite strike-price points, not percentages, ticks or currency premiums. Widths have no nearest-strike approximation or implicit tolerance.

Builder Default direction Signed legs for that default
straddle(chain, dte=45, today=...) short Sell call and put at the same strike nearest ATM
strangle(chain, call_delta=0.16, put_delta=-0.16, ...) short Sell selected call and put; put strike must be below call strike
iron_condor(chain, short_delta=0.16, wing_width=5, ...) short Sell inner call and put; buy call exactly wing_width above and put exactly that width below; inner put below inner call
calendar_spread(chain, dte_front=30, dte_back=60, ...) long Sell front call, buy back call at the same strike; front expiry strictly before back expiry
call_vertical(chain, width=..., ...) long Buy lower call, sell call exactly width higher
put_vertical(chain, width=..., ...) long Buy higher put, sell put exactly width lower

Every builder accepts direction='long' or direction='short'. Changing the direction reverses every signed leg; it does not change selection. Calendars search eligible front/back expiry pairs in front candidate order, then back candidate order. They use calls only. Verticals require an explicit width and choose the buy-side anchor nearest ATM for the long recipe.

No complete supplied geometry within these limits raises ATOM-OPTION-NO-MATCH. Supply the missing exact contracts or deliberately change the requested inputs; Atom will not silently widen the search. A custom structure provider must also implement the separate runtime-checkable VerticalStructureProvider protocol to support the vertical wrappers. A legacy provider without both vertical methods refuses with ATOM-OPTION-GEOMETRY.

Worked pure-construction example

from datetime import date
from atom.helpers.options import Option, OptionChain
from atom.library.structures import call_vertical

expiry = date(2026, 2, 15)
chain = OptionChain(
    underlying="XYZ", spot=100,
    options=[
        Option(symbol="XYZ_C100", underlying="XYZ", strike=100,
               expiry=expiry, right="call"),
        Option(symbol="XYZ_C105", underlying="XYZ", strike=105,
               expiry=expiry, right="call"),
    ],
)
recipe = call_vertical(chain, width=5, dte=45, today=date(2026, 1, 1))

This constructs one unit of buy XYZ_C100 / sell XYZ_C105. It needs no delta because strike geometry selects the legs. With direction='short', the signs become sell XYZ_C100 / buy XYZ_C105. Requesting width 6 refuses because the supplied chain lacks that exact pair.

This example deliberately stops at construction. The intended execution verb is recipe.open(qty), preserving the recipe's signed direction; .sell() would reverse it. Currently built-in recipes' .open(), .buy(), .sell() and .close() explicitly refuse with ATOM-OPTION-TERMS before submission. The generic equity group path cannot preserve option multipliers, per-leg terms, executable bid/ask quotes or recipe lifetimes, so it is not a valid execution fallback. This also corrects the previous builders' ability to submit option IDs as untyped equity legs. Do not catch that refusal and submit the same legs as equities instead.

Point-width geometry and delta/ATM distances preserve the supplied decimal values independently of the host's numeric precision settings. For a 100.2/100.21 call chain, width 0.01 constructs that exact pair; another component lowering Decimal precision cannot remove it or admit an approximate pair. Positive OTM placement on the typed path below cannot round back to ATM.

Typed host handoff

Python strategies can also use the additive OptionDecision interface. This is separate from the older Composite methods above: it sends a qualified intent to an adopting host, not an equity order or a simulated option fill.

from atom.library.option_intents import OptionDecision, OptionDistance, OptionStructure

SPREAD = OptionStructure(
    name="spread", kind="call_vertical",
    width=OptionDistance(unit="points", value=5),
)

def on_tick():
    decision = OptionDecision("XYZ", (SPREAD,), "recording@1")
    view = decision.position("spread")
    if view.is_flat:
        decision.enter("spread", 2, "buy")
    elif view.profit is not None and view.profit >= 0.5:
        decision.close("spread")

The example requires an injected v1 host with the matching recording@1 policy, an explicit strategy identity and a dated UTC decision/market clock. Naive decision timestamps mean UTC; supplied host views still require aware, matching instants. It is not runnable with Atom's default equity simulator. recording@1 names the offline test policy, not a production admission or collateral policy.

With a covered XYZ chain containing 100/105 calls, two BUY verticals emit buy two 100 calls and sell two 105 calls. A multiplier of 100 stays attached as a contract term; it does not turn two contracts into 200 contracts. Named families require compatible multiplier, currency, exercise and settlement. Calendar expiries remain distinct. Unsupported adjusted or nonlinear terms refuse. Missing model identity refuses delta selection; a declared model name is a host assertion, not independent verification of the Greeks.

Placement accepts ATM, exact strike, delta magnitude, points OTM, percentage points OTM (value=5 means 5%), or listed-strike steps. Widths in points need the exact strike difference; strikes walks the actual listing grid. Expiry defaults to 45 calendar days within an inclusive seven-day window. The snapshot must cover the entire requested interval, even if a plausible contract exists. Selection is deterministic and tries complete geometry, not approximate wings. Typed named recipes also try the next ranked geometry when an anchor or wing cannot accept the exact authored count. With one five-point call vertical and 100/105/110 listings, a three-contract lot step at 100 makes 105/110 the eligible pair if those legs accept one. Counts and lot steps are never rounded; body ratios apply once, so one butterfly needs two body contracts. No eligible recipe raises ATOM-OPTION-NO-MATCH. This does not change the older Composite builder contract. At a shared strike, the first lot-eligible contract ID wins; a rejected ID cannot hide another eligible listing or change the underlying listed-strike grid. The supplied snapshot declares the spot currency; selected strike currency must match it. Same-day openings refuse in this version rather than infer a cutoff. Custom legs allow explicit sides and ratios with open; their risk qualification belongs to the host. Each typed resolution shares a 100,000-unit logical-work budget across preprocessing, selector scans, candidate attempts and expiry retries. Custom recursion also has a 10,000-node ceiling. An ATOM-OPTION-SEARCH-LIMIT refusal is not a no-match result: narrow the requested expiry/placement or supplied snapshot. Cached candidates are local to that one resolution, never reused at a later decision. These bounds do not certify a machine-independent wall-clock or memory limit.

The position view belongs to the exact strategy and recipe at the decision instant. Missing profit/loss/DTE is unavailable, not a fabricated zero. Check for None before a Python comparison, as the example does. Profit and loss are nonnegative fractions supplied by the host under the pinned policy. Closing reverses the held contract IDs, sides and counts with their lifetime identity; it never rebuilds a spread from today's chain. Pending close permission can suppress another close. This does not implement an order-lifetime ledger.

Each intent has a stable ID. The host must acknowledge and deduplicate repeats, including after restart, and reject conflicting payloads under one ID. Atom suppresses repeats only within a callback. A rejected or malformed acknowledgement is an essential error; earlier accepted intents are not rolled back. Quote fills, capital, reconciliation, exercise and production persistence remain outside Atom.

Integration work still pending

OptionChain has no full dated snapshot, coverage or contract-terms contract. Construction does not prove freshness, coverage, dated multipliers and terms, collateral admission, executable bid/ask prices, lifetime handling or expiry settlement. The typed interface above requires a host to supply these views; it does not upgrade an old OptionChain automatically. Production option execution remains a separate integration. Named TradeScript structures use the same typed decision interface, not the older Composite actions. A returned composite handle alone is not evidence of runnable option trading or financial acceptance.