Square off intraday positions before close
Chapter 48 warned: never rely on the broker's forced MIS square-off as your primary exit — its price is out of your control. This chapter builds your own square-off routine that runs comfortably ahead of that cutoff.
import datetime
def is_square_off_time(hour=15, minute=10) -> bool:
now = datetime.datetime.now()
return (now.hour, now.minute) >= (hour, minute)
def square_off_all_intraday(kite, order_manager: OrderManager):
positions = [p for p in kite.positions()["net"] if p["quantity"] != 0 and p["product"] == "MIS"]
for pos in positions:
side = "SELL" if pos["quantity"] > 0 else "BUY"
qty = abs(pos["quantity"])
result = order_manager.place(
strategy_code="eod-squareoff",
exchange=pos["exchange"], tradingsymbol=pos["tradingsymbol"],
transaction_type=side, quantity=qty, product="MIS",
order_type="MARKET",
)
logging.info(f"Square-off {pos['tradingsymbol']}: {result}")
Schedule it with margin for retries
def run_square_off_loop(kite, order_manager, cutoff_hour=15, cutoff_minute=10):
while not is_square_off_time(cutoff_hour, cutoff_minute):
time.sleep(5)
square_off_all_intraday(kite, order_manager)
time.sleep(10) # give orders time to fill
remaining = [p for p in kite.positions()["net"] if p["quantity"] != 0 and p["product"] == "MIS"]
if remaining:
logging.error(f"Positions still open after square-off attempt: {remaining}")
send_alert(f"Manual square-off may be needed: {remaining}")
square_off_all_intraday(kite, order_manager) # one retry
Set your cutoff meaningfully earlier than the broker's
If Zerodha's forced cutoff is ~3:20 PM, target 3:05-3:10 PM for your own routine — this buffer absorbs order rejection retries, partial fills needing a second attempt, and any transient API slowness, all before the broker takes it out of your hands.
Also cancel resting entry orders before square-off time
def cancel_pending_entries_before_close(kite, cutoff_minute_before=20):
# Cancel any still-open ENTRY orders (not exits) shortly before square-off time
# to avoid a fresh position opening right as you're trying to close everything out.
open_orders = [o for o in kite.orders() if o["status"] == "OPEN" and o.get("tag", "").startswith("entry")]
for o in open_orders:
kite.cancel_order(variety=o["variety"], order_id=o["order_id"])
A resting limit entry order that fills at 3:12 PM, moments before your square-off sweep, creates a position your routine might miss if it already ran — cancel entries before running the exit sweep, not after.