← All posts
Productbotinternals

What our bot actually does on a tick

A tour of the eleven things that happen between “new bar arrived” and “order submitted” — useful if you’re building your own bot, debugging ours, or just curious.

Alok Desai··7 min read

People ask us “what does the bot do?” expecting either a one-liner (it trades stocks!) or a wall of math. The honest answer is in between: a sequence of about eleven steps that run on every fresh bar, each individually simple, composing into something that's actually hard to break in production. Here's the tour.

The trigger

The bot doesn't poll the market on a clock. It subscribes to a bar stream from the broker (Alpaca, IBKR) and the market data API pushes a finalized bar at the end of each interval — 1m, 5m, 1h, or 1d depending on your config. That's the “tick.” Everything in this post happens in the ~200ms between bar-arrival and the next one.

1. Bar validation

Brokers occasionally send malformed bars: zero volume, NaN prices, timestamps from yesterday because of a backfill. The bot rejects those at the door and emits an erroraudit event so the user sees it on the Live tab. Trades aren't made on bad data.

2. Feature computation

The bar gets appended to a rolling window (default ~900 bars) and ~15 features are computed: log returns over multiple horizons, realized volatility over 5/20/60 bars, autocorrelation, dispersion, gap statistics, EMA/SMA crossovers, ATR. These are the inputs to the HMM.

3. Regime classification

The features go through the HMM forward pass. Output: a probability distribution over the K hidden states (typically 7 for daily models, K determined by BIC during training). The bot also tracks a stability counter — how many bars the same regime has been the argmax — and a flicker counter for how often regime swapped in the last N bars.

4. Regime confirmation gate

A new regime label isn't acted on immediately. The bot requires:

  • Confidence ≥ minimum (default 0.60 — meaning the argmax regime has at least 60% probability mass).
  • Stability ≥ minimum (default 3 bars in the same regime).
  • Flicker count ≤ threshold (the regime hasn't been ping-ponging).

Until those gates clear, the bot uses the previous confirmed regime — even if the live argmax has switched. This prevents whipsaws on noisy bars.

5. Strategy selection

Each regime maps to a strategy class: defensive (small or no exposure), fully-invested (max long), trend-follow, breakout, etc. The map is part of the model configuration. Selecting a strategy is a dictionary lookup; the heavy lifting was in fitting the regime → strategy mapping during research, not at runtime.

6. Symbol screening

For multi-symbol bots, every active symbol is screened through the strategy: trend filter, liquidity filter, volatility budget. Symbols that fail are skipped this tick. Symbols that pass go to step 7.

7. Position-size calculation

For each symbol that passed screening, the bot computes a target dollar exposure based on:

  • Account equity (live, from the broker, refreshed each tick).
  • Regime's allocation cap (e.g. DEEP_BEAR allows max 60% gross exposure; TOP_BULL allows 100%).
  • Per-symbol position cap (e.g. max 5% of equity in any single name).
  • ATR-scaled risk budget (riskier symbols get smaller positions, even within the same allocation cap).

8. Risk-cap pre-check

Before submitting, the proposed order is checked against the live state of every risk cap:

  • Daily drawdown halt — if the account is down ≥ X% today, new entries are refused and existing positions tightened.
  • Weekly DD halt, peak-to-trough DD halt.
  • Total leverage limit — gross long + short can't exceed configured max.
  • Per-asset-class caps — crypto exposure ceiling, etc.
  • Trades-per-day cap — if you've already done N trades, no new entries until tomorrow.

If any cap fails, the order is dropped with a circuit_breaker audit event and a clear reason.

9. Duplicate-entry guard

The bot won't add to a position it already holds, even if the strategy says to. Stop-loss + take-profit handle exits; re-entries require the existing position to close first. This sounds obvious but it took us debugging a position- ladder bug in week one to convince ourselves it's the right default.

10. Order submission

Survivors get submitted to the broker (limit order at the bar close ± buffer for slippage tolerance). The bot waits up to 5 seconds for an ACK; if the broker rejects, an order_rejected audit event captures the reason (insufficient buying power, market closed, asset not tradable, etc.) and the order is dropped.

11. State snapshot

After all that, the bot writes its updated state to disk: open positions, today's P&L, the last bar processed, the model's current regime. The snapshot is atomic (write to .tmp+ rename) so a crash mid-write can't corrupt state. On next boot, the bot resumes from this snapshot — no manual recovery, no lost positions.

python
# Roughly the loop body, with non-essential plumbing trimmed.
def on_bar(bar):
    if not validate(bar):
        emit("error", reason="bad_bar")
        return

    features = engineer.compute(rolling_window + bar)
    state_probs = hmm.forward(features)
    regime = pick_regime(state_probs, stability, flicker)

    if not regime.is_confirmed:
        return  # nothing to do — wait for confirmation

    strategy = orchestrator.regime_to_strategy[regime.id]

    for symbol in active_symbols:
        if not strategy.screens(symbol):
            continue

        size = sizer.compute(account, regime, symbol)
        if not risk_manager.allows(size, account):
            emit("circuit_breaker", reason=risk_manager.last_breach)
            continue

        if positions.has(symbol):
            continue  # duplicate-entry guard

        order = build_order(symbol, size)
        ack = broker.submit(order)
        emit("order_submitted" if ack.ok else "order_rejected", ...)

    snapshot.save()

What we don't do

A few things that look like they should be in the loop but aren't:

  • Retrain the model.That happens on a schedule (hourly check, daily retrain, configurable), not every tick. Retraining is expensive and the regime structure doesn't change minute-to-minute.
  • Re-fetch market data we already have. The bar stream is incremental; we keep the rolling window in memory and just append.
  • Consult the news / sentiment / Twitter. Adding extra signal sources is its own research project. Right now the bot is honest about being a regime classifier with risk-aware sizing — nothing more.

That's the loop. Eleven steps, ~200ms per bar, all observable on the dashboard's Live tab as audit events as they happen. If you're curious what your specific bot is doing right now, that's the place to watch.

Try the bot

Run a paper bot in 5 minutes. Free tier, your laptop, no card required.

Start free →