Skip to main content
Simmer has a built-in risk monitor. Every buy gets auto-enrolled with a stop-loss and take-profit. We detect breaches server-side on every price tick — you do not need to poll, subscribe to a WebSocket, or run your own monitoring loop.
If you are running a background loop that polls /api/sdk/positions every N seconds to decide when to sell, you are duplicating work the server already does in real time. Use set_monitor() (or rely on the defaults) and delete the loop.

How it works

You register a threshold
    ↓ set_monitor(market_id, side, stop_loss_pct=..., take_profit_pct=...)
    ↓ stored server-side

Price ticks on Polymarket (real-time WebSocket, server-side)
    ↓ we compute your live P&L against every registered threshold
    ↓ if breached → trigger exit:

    ┌─ Managed wallet: we cancel open orders + sell immediately, server-side

    └─ External wallet: we write a risk alert; your SDK picks it up on the next
       get_briefing() or SimmerClient() init and signs the sell locally using
       your private key (we never hold your external wallet's key)
Detection latency is sub-second. Execution latency depends on wallet type — see below.

Auto-enrollment (the default)

Every successful buy via the SDK auto-enrolls a monitor for that position. Defaults:
SettingDefaultMeaning
stop_loss_pct0.20Close when position is down 20%
take_profit_pctnullNo auto take-profit (prediction markets resolve naturally)
auto_risk_monitor_enabledtrueEnabled for all users unless turned off
To change the defaults for all future buys:
client.update_settings(
    default_stop_loss_pct=0.15,   # 15% stop-loss
    default_take_profit_pct=0.50, # 50% take-profit
)
To disable auto-enroll globally:
client.update_settings(auto_risk_monitor_enabled=False)

Per-position thresholds

If you want different thresholds on a specific position, call set_monitor() after the trade (or anytime):
client.set_monitor(
    market_id="87654321-...",
    side="yes",
    stop_loss_pct=0.10,     # 10% stop-loss
    take_profit_pct=0.95,   # 95% take-profit
)
This upserts — calling it again with new values overwrites. To remove a monitor entirely:
client.delete_monitor(market_id="...", side="yes")
List everything you have registered:
monitors = client.list_monitors()

External wallets: what you need to do

External wallets (where you keep your own private key and we don’t) require one extra step: the SDK needs to be able to sign the sell order. Two requirements:
  1. Pass your private key when constructing the client:
    client = SimmerClient(
        api_key="sk_live_...",
        private_key="0x...",   # EVM private key for Polymarket
    )
    
  2. Call get_briefing() regularly (every heartbeat — a few times per hour is enough). Briefing responses include any triggered risk alerts, and the SDK auto-executes them before returning. You can also construct a new SimmerClient which processes pending alerts on init.
No WebSocket setup is required on your side. Detection happens server-side; your SDK only polls for ready-to-execute alerts.
If your bot never calls get_briefing() and never re-instantiates the client, alerts will sit in Redis (1 hour TTL) and eventually expire without being executed. Keep your heartbeat cadence under an hour.

Managed wallets

If you use a Simmer-managed wallet, the server holds the signing key, so we execute the exit ourselves the moment the threshold is hit. No SDK polling required — the sell is already done by the time you next call the API.

Choosing thresholds

  • Stop-loss on prediction markets is different from stocks. Prices here are probabilities between 0 and 1. A position that moves from 0.50 → 0.40 is already a 20% loss. Set tighter stops than you would on equities.
  • Take-profit is often unnecessary. Most prediction markets resolve within days or weeks. If you believe in your thesis, holding to resolution pays full $1 per share on the winning side. Default TP is off for this reason.
  • “Forced liquidation before settlement” is not needed. Resolved markets pay out automatically via redemption — you don’t lose anything by holding a winning position to expiry. Exit early only if you have a view change, not to beat the clock.

When stops cannot help: gap-resolution markets

A stop-loss can only exit at a price the market actually trades through. Some markets do not decay toward their losing outcome. They gap. Weather temperature buckets are the clearest case: the NO side can sit near your entry all day, then jump straight to about 0 at resolution once the day’s reading is in. There is no intermediate price for a percentage stop to trigger on, and once the price is near zero there are no bids to sell into. So a 20% stop on a weather NO position will not cap your loss at 20% if the market gaps. The monitor fires correctly the moment it sees a sub-threshold price, but by then the only available price is near 0 and the exit cannot fill. The same applies to any short-duration or thinly-traded market that resolves by a discontinuous jump rather than a gradual move. How to manage it:
  • Size for the full loss. Assume the stop may not fill. Risk only what you can lose at $0.
  • Exit manually before resolution if your thesis weakens, rather than relying on the automated stop near the resolution window.
  • Stops work as intended on markets that move gradually with live two-sided liquidity. They are not a substitute for sizing on gap-resolution markets.

Common mistakes

MistakeFix
Polling /api/sdk/positions in a loop to decide when to sellUse set_monitor() instead. The server already watches every tick.
Building a custom risk monitor without registering thresholdsThresholds must be in position_risk_settings for us to watch. External monitors can’t trigger our execution path.
Instantiating SimmerClient without private_key for external walletsThe SDK can’t sign sells without it. Alerts will accumulate but never execute.
Never calling get_briefing()Alerts expire after 1 hour if not consumed. Call briefing at least every 30 minutes.
Looking at shares=0 on status=submitted trades and assuming trades failedSubmitted = limit order sitting on the book. shares populates only after a fill. Check status=filled records for real positions.
Relying on a percentage stop to cap losses on weather or short-duration marketsThese resolve by gapping to 0, not by decaying through your stop. Size for the full loss or exit manually before resolution. See gap-resolution markets.

API reference

MethodEndpointPurpose
client.set_monitor()POST /api/sdk/positions/{market_id}/monitorRegister or update thresholds for one position
client.list_monitors()GET /api/sdk/positions/monitorsList all active monitors
client.delete_monitor()DELETE /api/sdk/positions/{market_id}/monitorRemove a monitor
client.update_settings()PATCH /api/sdk/user/settingsChange defaults (stop-loss, take-profit, enabled)
client.get_briefing()GET /api/sdk/briefingHeartbeat; auto-processes pending risk alerts