Weather Bot — Episode 13: The Fee Change, the Ghost Trade, and Where Things Stand Now
Two problems hit at the same time. One was announced publicly. The other I only found because a position showed up in my Polymarket wallet that my bot didn't know about.
What This Post Covers
A fee structure change that forced me to rethink how my bot places orders, a ghost trade that revealed three separate bugs in my position tracking, and a price analysis that accidentally confirmed my 48-hour buy limit was landing on the optimal entry window. Plus where the numbers actually stand right now.
The Fee Change
On March 30, Polymarket expanded taker fees to weather markets. Previously only crypto and sports had fees. Now every category except geopolitical events gets charged.
The fee is dynamic — highest around 50% probability ($0.50 price), dropping toward zero at the extremes. For weather markets where I'm buying at $0.15-$0.25, the effective rate is about 1.00%.
But the fee only applies to taker orders. There are two types on Polymarket:
Maker orders (GTC/limit): You post a price and wait for someone to trade against you. Zero fees. You actually receive a daily USDC rebate for providing liquidity.
Taker orders (FOK/market): You trade immediately against existing orders. You pay the fee.
My main buy mechanism was already GTC — post a limit order at my target price and wait. That's maker. Zero fees, plus rebates.
But I had three code paths that used FOK (taker) orders: bucket swaps when the forecast changes, last-minute D-0 catchup buys, and METAR NO trades. I pulled the logs to see how those had been performing.
Combined success rate across all FOK paths: 0 out of 18 attempts. Seventeen bucket swap attempts, zero fills. One D-0 catchup attempt, zero fills. METAR NO was already disabled since Episode 8. Not a single FOK order had ever actually worked. The weather market order books are too thin for market orders at our price levels.
So I commented out all three. Every FOK function disabled. The bot now has exactly one buy mechanism: GTC maker orders. Taker fee exposure: 0%. Plus I qualify for maker rebates.
The Ghost Trade
A few days after the fee change, I was checking my Polymarket wallet manually — something I still do every morning — and noticed two Chicago positions that shouldn't exist.
Chicago April 1:
40-41°F — 14.9 shares @ $0.111
42-43°F — 5.7 shares @ $0.205
Neither appeared in positions.json. My bot wasn't tracking them, wasn't monitoring them, and wouldn't manage them at resolution. Ghost positions.
I went through the logs to figure out what happened:
Mar 31 04:38 — Bot posts GTC: Chicago 40-41°F,
18.02 shares @ $0.111
Mar 31 09:22 — Bot posts GTC: Chicago 42-43°F,
9.76 shares @ $0.205
Mar 31 (various) — Both partially fill on Polymarket
Apr 1 05:56 — Forecast shifts 2 buckets →
Bot cancels the 42-43°F order
Here's what went wrong. When the bot cancelled the 42-43°F order, 5.7 shares had already filled. The cancel API removed the remaining unfilled portion, but those 5.7 shares were sitting in my wallet. My bot never recorded them.
Same thing with 40-41°F — it partially filled (14.9 out of 18.02 shares) but my fill detection only checked for MATCHED status, meaning fully complete. A LIVE order with partial fills wasn't being tracked.
Three Bugs in the Tracking
Digging into this uncovered three separate issues.
The first was almost embarrassing: my add_position function never saved the bucket name. Every single position — all 121 of them — had bucket: null in the JSON. The bot worked around this for display purposes, but it made data analysis harder than it needed to be.
The second was the actual ghost problem. When cancel_pending_order ran, it just sent the cancel request without checking if shares had already filled. The fix: before every cancel, call get_order to check size_matched. If shares filled, save them as a position first, then cancel the remainder.
if size_matched > 0:
add_position(opp, actual_bet,
actual_shares=size_matched)
log.info(
f" Partial fill saved: {city}"
f" {size_matched}sh"
)
if status == "MATCHED":
remove_pending(order_id)
return True
# then proceed with cancel
The third was subtler. My code assumed MATCHED meant "all shares filled." In reality, MATCHED means "order is done" — which includes "partially filled then cancelled." I needed to compare size_matched against the original order size to know if it was a full fill or a partial.
The Resolution: +$12.08
I manually added both Chicago ghost positions to positions.json and waited for resolution.
April 1 actual high in Chicago: 42.8°F. Winning bucket: 40-41°F.
40-41°F @ $0.111 × 14.9 shares → WIN
Payout: $14.90, Cost: $1.65
Profit: +$13.25
42-43°F @ $0.205 × 5.7 shares → LOSS
Payout: $0, Cost: $1.17
Profit: -$1.17
Net: +$12.08
If I hadn't spotted those ghost positions during my morning wallet check, I'd have lost $1.17 on the loser (Polymarket takes the shares at resolution) and missed $13.25 in winnings sitting unclaimed. The manual check saved me $12.08 on a single trade.
An Accidental Discovery: The Price Floor
While investigating the ghost trade, I ran a broader analysis on winning bucket prices across all 758 resolved markets. I wanted to know: when is the winning bucket cheapest?
| Hours Before Peak | Avg Price | Available ≤$0.25 |
|---|---|---|
| 54-60h | $0.261 | 55% |
| 48-54h | $0.284 | 48% |
| 42-48h | $0.282 | 46% |
| 36-42h | $0.287 | 44% |
| 24-30h | $0.309 | 39% |
| 12-18h | $0.350 | 31% |
| 6-12h | $0.373 | 29% |
The price floor sits at 42-48 hours ($0.282). Before that, prices are slightly higher due to wider spreads and less liquidity. After that, they climb steadily as the forecast becomes more certain and more people pile in.
My 48-hour buy limit from Episode 10 lands almost exactly on this floor. I set it based on forecast accuracy data, not market prices. But it turns out the two are correlated — the sweet spot where models become reliable is also when the market hasn't yet priced in certainty. A nice accident.
Where Things Stand
| Metric | Value |
|---|---|
| Balance | $199 (recovering from $188 low) |
| Cities | 24 active, 2 disabled |
| Buy mechanism | GTC only, zero taker fees |
| Trades (since 3/28) | 30 |
| Wins | 4 (13%) |
| Average win | $4.96 |
| Average loss | $0.91 |
| Win/loss ratio | 5.5:1 |
| Breakeven win rate | ~15.4% |
| Current win rate | 13% |
I'm at 13% win rate. Breakeven is 15.4%. That's close — close enough that a small improvement in forecast accuracy or entry timing could push it over. The win/loss ratio of 5.5:1 means the math is on my side structurally. I just need to win slightly more often.
The 10 new cities from Episode 12 are adding more opportunities without requiring more capital. Each trade is still $1. The strategy is right at the edge, and the edge is where things get interesting.
Key Takeaways
- Polymarket taker fees don't matter if you use GTC maker orders. Zero fees plus rebates. All my FOK paths had 0/18 success rate anyway — good riddance.
- Check your wallet manually. Every morning. The ghost trade bug was invisible to my bot but plainly visible on the Polymarket website. That morning check saved $12.
- The winning bucket price floor is at 42-48 hours before peak. My 48h buy limit accidentally lands on the optimal entry window.
- 13% win rate vs 15.4% breakeven. Close, not there yet. But 5.5:1 win/loss ratio means a small accuracy improvement changes everything.
What's Coming
The bot's running. The data's accumulating. The strategy is still shifting — there are ensemble forecasting experiments I haven't written about yet, and the new cities are generating patterns I'm only starting to understand. This isn't the end of the series. It's just where things are today.
← Previous: Episode 12: From 15 Cities to 26 Next: Episode 14: When 163 Trades Rewrote the Strategy — The v2.9 Overhaul
More updates on the way. If you're working on something similar or found a smarter way to do it, drop it in the comments — the more we share, the faster we all move.
Disclaimer: This blog documents my personal learning journey. Nothing here is financial advice.
Comments
Post a Comment