Skip to content

Composite positions in Python

A composite groups multiple instrument legs into one position view. Use this advanced Python surface only when the execution environment supports the instruments and grouped operations.

pair("TCS", "INFY", ratio=2.0) describes a two-leg relationship. Buying one composite unit means one unit of TCS and two short units of INFY; selling reverses it.

basket(["TCS", "INFY"], [1.0, -1.0]) uses signed quantity multipliers, not money-allocation weights. Equal quantities do not mean equal notional exposure.

A handle provides buy, sell, close, is_flat and pnl. Repeated opening calls on an already-open handle are no-ops. Execution and partial-fill handling remain runtime concerns; grouping does not guarantee simultaneous broker fills.

Option structures require a supplied chain and additional lifecycle support. They are not enabled simply by mentioning a structure name. See composite reference for available Python functions. These functions do not make deferred TradeScript pair/options syntax executable.

Translate one composite unit into legs

Handle and action Desired leg quantities
pair("TCS", "INFY", ratio=2).buy(3) Long 3 TCS, short 6 INFY.
The same pair's .sell(3) Short 3 TCS, long 6 INFY.
basket(["TCS", "INFY"], [1, -2]).buy(3) Long 3 TCS, short 6 INFY.

The pair ratio and basket multipliers are quantities, not hedge estimates calculated from live prices. A two-to-one quantity relationship can have unequal money exposure. A basket needs at least one instrument and the same number of symbols and multipliers.

Manage the grouped lifecycle

A handle identifies a group. Default group identifiers are deterministic; use an explicit group_id when distinct strategies or ideas need separate groups. Create the handle as a descriptor, and call its actions inside strategy functions with valid execution context.

handle.is_flat checks the group, .close() requests exits for its actual legs, and .pnl reads grouped P&L. Repeated open calls while the group is already open are no-ops; use the actual grouped operation supported by your runtime rather than manually assuming atomic fills.

Partial leg execution can leave exposure different from the intended relationship. Instrument/shorting support, order limits and group tracking all matter. The option constructors in the reference additionally require a chain and supported expiry/strike contracts; they are advanced capability-dependent functions, not a beginner default.

Run a complete pair and basket

This Python strategy declares every required instrument, creates two independent groups, submits one entry for each and closes both after its exit condition. The thresholds are arithmetic teaching values, not a recommended market signal.

"""One-shot composite lifecycle tutorial, not an investment strategy."""
from atom.library.composite import basket, pair

symbols = ["TCS", "INFY", "RELIANCE", "HDFCBANK"]
spread = pair("TCS", "INFY", ratio=2.0, group_id="tutorial:pair")
hedge = basket(["RELIANCE", "HDFCBANK"], [1.0, -1.0], group_id="tutorial:basket")


def on_start():
    return {}, {"opened": False}


def on_tick():
    price = close["TCS"][0]
    if not state.opened and price >= 100:
        spread.buy(3)
        hedge.buy(4)
        state.opened = True
    elif price < 100:
        spread.close()
        hedge.close()

The same maintained example is in examples/python/critical_pair_basket.py in the repository.

symbols selects explicit multi-symbol access: on_tick() runs once for the incoming panel, and close["TCS"][0] reads TCS deliberately. Do not replace symbols with a flat universe here: that would broadcast the same function per member, potentially submitting duplicate grouped entries before fills. Supply aligned observations for all four symbols and an execution environment that supports the short legs. Missing leg prices are not a hedge estimate.

When TCS first closes at or above 100, spread.buy(3) requests long 3 TCS and short 6 INFY. hedge.buy(4) requests long 4 RELIANCE and short 4 HDFCBANK. The pair ratio 2 multiplies units, while the basket's signed weights multiply its four units. Neither recipe splits a money budget. They use distinct group IDs so closing one cannot select the other's legs.

The one-shot state.opened flag prevents another entry request while prices remain above the threshold and prevents re-entry after exit. It records an attempt, not confirmed fills. The tutorial does not retry a rejection; inspect rejected orders and each group's actual holdings before interpreting a run. Once TCS is below 100, each handle's close() requests opposite quantities for its actual open legs. Calling it on an already-flat group does nothing. Do not use a bare single-symbol close() to mean close both groups.

For a small synthetic check, supply aligned panels priced 100, 101, 99, 99 for all four symbols. After the first entry has filled, the expected groups are {TCS: 3, INFY: -6} and {RELIANCE: 4, HDFCBANK: -4}. The below-100 signal requests their exits; leave another bar for next-open execution. Both groups finish flat in the local event backtest. These quantities and closure are regression-tested against the maintained sample, not inferred from a syntax check. This is not proof of simultaneous broker fills or live recovery.

The constructors default to one composite unit if no action quantity is given; the example supplies quantities explicitly. risk_pct sizing on these handles is unsupported and raises; use explicit units. Python composite support does not enable the parked TradeScript pair/basket sizing forms. For a money-weighted portfolio instead of unit recipes, use target portfolios.