Close-Only: The Middle State Between Running and Halted

For a long time my bot's safety model had exactly two states: running and halted. Something looks wrong? Halt everything. Something looks fine? Trade normally. It's the obvious design, and it's wrong, because the two states bundle together two very different kinds of risk.

Over a few weeks this summer, three unrelated problems each forced me to confront that bundling. All three ended up with the same answer, which is usually a sign the answer is structural rather than situational.

Halting is not the safe option

Here's the thing that took me embarrassingly long to internalize: a full halt is not conservative. When the bot halts, it stops everything — including the machinery that manages and exits positions it already holds. An open options position is a live obligation. It doesn't pause because your software did. A halted bot with open positions is arguably in a more dangerous state than a running one, because now nothing is watching the exits.

I'd actually written this down earlier in the project — a drill scenario built around the requirement that a halt must never strand an open position. But the deeper implication hadn't propagated into the design: if halting strands positions, then "halt" is the wrong response to the partial failures that leave position management intact. What you usually want is a third state.

The state is: close-only

Close-only means the bot keeps running, keeps monitoring, keeps managing and exiting existing positions — but refuses to open anything new. It splits the risk bundle cleanly. Opening a position is a discretionary act; you can always decline it and be no worse off than before. Closing a position is stewardship of risk you already carry; declining it is itself a risk decision, and usually a bad one.

Two caveats before that framing runs away with itself. First, close-only is conditional, not automatic: it's only a valid state when the stewardship path — position state, order routing, the exit logic itself — has been separately verified. A failure that compromises any of those doesn't earn close-only; it earns a halt and manual handling, because at that point the exit machinery is exactly as suspect as the entry machinery. Second, closing is not inherently safe. A close placed against stale position state, a duplicated order, one leg of a multi-leg exit that fills while the other doesn't — each of those adds risk while wearing the costume of reducing it. Close-only permits validated, risk-reducing management actions; it does not mean every closing action is blessed.

Three failure scenarios drove this home. To be precise about the word: these were mostly design-review findings — hazards caught by adversarial review before they could become production incidents — which is the cheap way to learn this.

The incomplete prerequisite protocol. Before the bot may open anything, a validation protocol has to complete — a battery of checks confirming the world matches the software's assumptions. What should happen when that protocol doesn't complete? My first instinct was to block trading entirely. But the failure of an entry prerequisite does not by itself prove the stewardship path unsafe — obligations created earlier, under a protocol that did complete, still need stewarding, and the dependencies the exit path shares with the protocol have to be evaluated separately. The answer that survived adversarial review (my collaborating reviewer model actually found my first framing of the problem unsound, which forced a redesign) was more conditional than my instinct in either direction: no new opens until the protocol completes, and exit management continues only if those shared dependencies verify clean. An incomplete protocol that implicates the shared dependencies doesn't degrade to close-only; it escalates to halted and a human.

The margin problem. When the account gets into margin trouble, the recovery logic reduces exposure. My first design had recovery restore normal operation once the numbers looked healthy again. The reviewer flagged it, and on reflection I agreed: recovery code that automatically resumes opening positions is recovery code that can oscillate. Margin stress means my sizing model and reality disagreed about something. The disagreement doesn't evaporate when the immediate number recovers — until a human understands why it happened, the bot shouldn't get its opening privileges back. So recovery now degrades to close-only and stays there.

The gateway reconnect. This one was the sneakiest. The broker gateway connection drops and re-establishes routinely, and reconnect logic naturally wants to restore the bot to its prior state. But "restore prior state" is exactly the kind of well-meaning code that silently un-does a safety latch. A reconnect after a margin-triggered degradation would have resumed opens as a side effect of plumbing. We closed that door explicitly: reconnecting restores connectivity, never permissions.

Latch it, and make resumption human

The pattern that generalizes across all three:

  1. Degrade, don't halt — when the stewardship path checks out. Keep the exit machinery alive and stop only the discretionary part. If the failure reaches the exit machinery itself, this rule doesn't apply: halt and go manual.
  2. Latch the degraded state. It must survive restarts, reconnects, and recovery routines. Any code path that could flip it back automatically is a bug, even if that code path is called "recovery."
  3. Resuming opens is a human act. Not because humans are smarter in the moment — the bot reacts faster than I do — but because entering close-only means the system encountered something outside its model. Deciding whether the model was wrong or the world was is, in this system, deliberately reserved for a human: not because automated recovery could never establish safe conditions, but because I chose not to trust it to judge a situation it has just demonstrated it didn't model.

The latch is the part I'd underline. Every one of these scenarios had a moment where some helpful subsystem — a restart script, a reconnect handler, a recovery routine — was perfectly positioned to quietly resume normal operation. Safety states that can be exited as a side effect aren't safety states; they're suggestions.

The asymmetry underneath

There's a clean asymmetry that justifies the whole design. The cost of being wrongly in close-only is opportunity: some trades don't happen. The cost of wrongly resuming opens is new risk taken on by a system that just demonstrated it doesn't fully understand its situation. Those costs aren't comparable, so the transitions shouldn't be symmetric either. Entering close-only should be easy, automatic, and triggered by many things. Leaving it should be hard, manual, and triggered by exactly one thing: a person who looked at what happened and signed off. The asymmetry holds only as long as the management side stays validated — which is the same condition close-only needed in the first place.

For this system, running / close-only / halted turned out to be the minimum viable state machine — I couldn't steward previously created obligations safely with fewer states. Whether that generalizes I can only offer as a hypothesis, but I'd expect the same three-state shape to recur in autonomous systems that accumulate obligations: full stop is for when you can't trust the machinery at all; degraded-but-stewarding is for when you can't trust the judgment but the hands still work — and have been checked. Confusing those two is how helpful recovery code turns into an incident.

Nothing here is trading advice; this is an engineering journal about building software.

Disclaimer: This journal documents a personal software-engineering project. The system described trades a paper (simulated) account. Nothing here is investment advice, a recommendation, or a signal, and no market data or trading performance is provided. Content is about building software.