Alphanume

Guide

How to Close Short Positions with the TradeZero API

Use Python to cover recorded TradeZero short positions at 2 p.m., reconcile partial fills, check quote freshness, and understand closing-auction limits.

Closing a short means buying shares back. If you shorted 40 shares and bought back 15, the remaining job is to cover 25. The program must keep that count correct even when an order only partly fills or a cancel request takes time.

This guide covers positions opened by the Alphanume Pre-Market Drop Risk starter. It uses the saved entry IDs to establish ownership instead of buying back every short in the account.

Download the Python starter, Windows wrapper, and offline tests. Extract it before running commands. The ZIP includes setup instructions and the complete source.

Start at 2 p.m., with a session-aware clock

The private deployment starts covers at 14:00 ET during a normal session. On early-close days, it starts one hour before the close. The public starter follows those start times. Its schedule comes from the NYSE calendar and uses Eastern local time, including daylight saving.

The model predicts an open-to-close outcome. A 2 p.m. exit is a separate execution rule, so do not label the dataset's close-based hit rate as the return of this workflow.

Use a fresh ask for the cover limit

The ask is the price a seller is currently offering. A buy limit at that ask can trade if the quote remains available, but the quote can move before the order arrives. This starter obtains the quote from Massive and rejects old or missing timestamps.

def cover_price(quote, clock):
    price, stamp = float(quote.get("P") or 0), quote.get("t")
    age = clock.timestamp() - float(stamp or 0) / 1e9
    if not math.isfinite(price) or price <= 0 or not math.isfinite(age) or not 0 <= age <= 60:
        raise ValueError("Missing, stale or future-dated ask; inspect the position manually")
    tick = Decimal("0.01") if price >= 1 else Decimal("0.0001")
    return float(Decimal(str(price)).quantize(tick, rounding=ROUND_UP))

The function reads the quote's ask P and nanosecond timestamp t, checks a maximum age of 60 seconds, and rounds up to the price increment. A delayed market-data plan may fail this check repeatedly. Fix data access rather than deleting the freshness check.

The runner calls GET https://api.massive.com/v2/last/nbbo/{symbol} with MASSIVE_API_KEY. Consult Massive's last-quote reference for the response and plan requirements. The public starter does not substitute a P&L mark for a fresh quote.

Cancel, confirm and cover only the residual

  1. Look up the recorded entry. If it is still working, confirm its cancellation before treating its executed shares as final.
  2. Validate market data before disturbing a working cover.
  3. Read each recorded cover. Cancel working ones and wait for a terminal response.
  4. Subtract executed covers from executed entries. Compare that residual with the broker's current short position.
  5. Save a fresh request ID, then send only the remaining quantity as Buy / Close / Limit / Day.
def cancel_confirm(client, client_id):
    row = client.order(client_id)
    if row.get("orderStatus") not in TERMINAL:
        client.cancel(client_id)
        for _ in range(5):
            row = client.order(client_id)
            if row.get("orderStatus") in TERMINAL:
                break
            client.sleep(1)
    if row.get("orderStatus") not in TERMINAL:
        raise RuntimeError("Cancel unconfirmed. No replacement; inspect the working order")
    return row

This helper never treats “cancel requested” as “cancel complete.” If confirmation does not arrive, the runner stops without submitting a replacement. A position/ledger mismatch also stops: another manual or automated trade may have changed the account. Use a dedicated workflow and avoid trading the same symbols elsewhere.

Run and monitor the exit

# Preview only; no order writes
.\.venv\Scripts\python.exe starter.py exit --expect Live
# Explicit live cover orders, repeated every 30 seconds
.\.venv\Scripts\python.exe starter.py exit --expect Live --submit --watch
# Read back fills and residual positions
.\.venv\Scripts\python.exe starter.py reconcile --expect Live

The starter needs today's saved entry state. No recorded entries means no covers. It stops starting new covers near the close and reports a nonzero exit status when a late residual needs attention. A final working limit can remain in the account. Check TradeZero directly; stopping Python does not cancel an order.

Why this example does not send MOC

The private deployment recorded an HTTP 400 rejection for a closing-auction order on September 24, 2026. The current TradeZero order schema also distinguishes advertised route metadata from the order types accepted by place-order: MarketOnClose and LimitOnClose are not accepted order types there.

The private exit used increasingly marketable limits late in the day. The public starter deliberately omits that automatic price widening, along with the private P&L-mark fallback. It keeps the code easier to inspect and reports residuals for manual attention. Neither approach guarantees a fill.

Common questions

Will this close unrelated shorts?

It follows only today's saved starter entry IDs. A mismatch between that ledger and the live position stops the run. Do not share its symbols with another strategy while it is active.

What happens if a cover is rejected?

The runner records the request ID, stops with an error and leaves the position for inspection. HTTP success alone is not a fill check.

Does a 2 p.m. job mean the shares are covered at 2 p.m.?

No. That is when the exit process starts. Partial fills, market movement, halts and rejected orders can delay or prevent completion.

Next: schedule and monitor the jobs. Return to opening orders or the overview.

API documentation checked September 27, 2026. Route availability and account access can change. Examples describe the included starter; they do not establish trading performance.