DEV Community

Blockchain Rust Engineer
Blockchain Rust Engineer

Posted on

My Polymarket Bot Has a Volatility Filter Now. Here Is Why I Built It Last.

I built the execution engine first. Then the order management. Then the inventory controls. Then the WebSocket integration. Then the Rust rewrite. Then the dashboard. Then the risk manager.The volatility filter came after all of that.Which is embarrassing because the volatility filter is the thing that decides whether any of those other systems should be running at all.Here is what happened. A few weeks ago I had a session where the bot ran perfectly by every technical measure I track. No errors. No missed fills. No crashes. PM2 stayed green all night. I woke up to a loss.I pulled the logs from MongoDB and mapped every fill against price movement during that session. The bot had been trading BTC 5 minute markets during a four hour flat range. Almost zero movement. Markets kept opening and closing with thin volume. Fills were coming in one sided. Inventory was accumulating on the buy side with no clean exit.The strategy needs volatility to work. Market making on a prediction market only generates edge when probability is moving fast enough that both sides of the book attract real counterparties. In a dead range probability barely moves. The book looks fine from the outside. The fills just never balance.The bot had no way to know this. It kept trading because that is what it was built to do.The fix was a volatility check before every market entry. Here is the core logic:

function isVolatileEnough(candles: Candle[]): boolean {
  const recent = candles.slice(-5)
  const high = Math.max(...recent.map(c => c.high))
  const low = Math.min(...recent.map(c => c.low))
  const range = (high - low) / low

  return range > VOLATILITY_THRESHOLD // 0.003 for BTC 5m
}
Enter fullscreen mode Exit fullscreen mode

If movement across the last 5 candles is below 0.3 percent the market gets skipped. The bot waits for the next one and checks again.
Getting the threshold right took about a week of backtesting against three months of trade logs. Too sensitive and it skips too many good markets. Too loose and it still enters dead conditions. At 0.003 it filters out roughly 30 percent of markets I was previously trading. P&L improved in the first week.
The second thing I added was a session drawdown limit:

if (sessionPnL < -MAX_SESSION_LOSS) {
  logger.warn('Session drawdown limit hit. Stopping.')
  await cancelAllOrders()
  process.exit(0)
}
Enter fullscreen mode Exit fullscreen mode

If cumulative session loss crosses the limit the bot cancels everything and stops. No grinding through bad conditions trying to recover with the same strategy that is clearly not working.
Both changes reduced total trade count by about 30 percent. That felt wrong at first. More trades had always meant more opportunity. What I learned is that more trades in bad conditions is just more ways to lose. The bot does not need to be active every minute. It needs to be active in the right minutes.
The numbers after two weeks with the filter running:

Total trades down 31 percent
Fill rate up 18 percent
Weekly P&L up across every session
Zero sessions with one sided inventory blowout

I should have built this before the WebSocket integration. Before the Rust rewrite. Before half the things I spent weeks on. None of those optimizations matter when the bot is trading in conditions where the strategy has no edge.
Build the filter that decides when to trade before you optimize how fast you trade.
Full strategy explanation at github.com/casatrick

Top comments (0)