Edge Autopsy邊際解剖

8 lines · 0 passed · $3,300 capital · 1 stale · 2026-08-27which one?

Futures · engineering English

What actually breaks when you run a bot on a prop firm evaluation account

1,384 words · 約 6 分鐘

Market data died for 7 days because the trading account blew up. A UI toggle silently dropped our API stop parameters. A retired systemd unit kept trading.

We have been running an automated futures strategy on a prop firm evaluation account since 2026-07. This is a list of things that actually broke, all of them first hand, none of them taken from a rules page or a forum thread.

The strategy results are elsewhere. On the same account we decomposed the execution gap and found it split 82% timing against 18% cost, taking the win rate from 60% in the model down to 51% live: Your backtest win rate is 60%. Live is 51%.

The line itself is still unproven, at t=0.79 with n=53. None of what follows changes that. It is the part of the work that nobody writes down.

Market data permission is tied to having a live account

On 2026-07-27 the evaluation account blew up. On 2026-07-29 the market data recorder went dark.

What we observed while debugging:

CheckResult
API loginsucceeded, token returned
Contract searchreturned an empty array
Account listevery account canTrade=false

Our mental model had been that a market data subscription is an account level entitlement, so a dead trading account should not affect it. That model was wrong. With no active tradeable account, the data permission goes away, and the API tells you this by returning nothing rather than by returning an error.

The recorder stayed dark for 7 days. It recovered within minutes of us buying a new evaluation account, with no code change at all.

That was the whole fix. A new account, and the data came back.

This is the cleanest example we have of the general failure mode on these platforms: the documentation describes the endpoints, not the entitlements. You find out what your subscription is actually attached to on the day it detaches.

A web UI toggle can silently drop your API parameters

The platform’s web interface has an Auto OCO Brackets setting. With it switched off, stop loss bracket parameters sent through the API are silently ignored.

Nothing fails. The order request returns success. The protective order simply does not exist.

We found out by reconciling positions, not by reading an error.

Our incident log marks this one as an old trap, meaning we hit it more than once. It belongs to the same family as everything in 檢查因為錯誤的理由而通過 (in Chinese): the check passes, and it passes for the wrong reason. A success response is a statement about the request, not about the state of the account.

The lesson we now apply: after placing a bracketed entry, read the open orders back and assert that the protective order is there. Do not trust the acknowledgement.

Automated trading is allowed, and we were wrong about this at first

We initially believed the common claim that bot trading on prop firm evaluations sits in a grey area, tolerated but not sanctioned. On 2026-08-13 we checked it properly and withdrew that belief.

A lot of what is written about this online is old. We repeated it before we checked it, which is exactly the mistake this site is about, so it goes in the list.

Eight integration traps in the API

All eight cost us real debugging time. The endpoints referenced are documented in the ProjectX Gateway documentation (checked 2026-08-27).

#TrapWhy it hurts
1Searching contracts by product symbol returns both the full size and the micro contractYou can silently subscribe to the wrong instrument. Data keeps arriving, so nothing looks broken
2Hardcoding the contract month means the subscription dies silently at rollThe failed subscription raises nothing. Files keep being written because other instruments still stream
3Fill timestamps carry five fractional second digitsdatetime.fromisoformat on Python 3.10 raises ValueError on them
4The entry fill has a null profit field, only the exit fill carries a valueYou need this to pair entries with exits
5Commissions arrive in two separate fieldsCounting one of them understated our costs by about $7.5 over three days
6Session tokens expire after 24 hoursOur recorder restarts and re-authenticates every 20 hours, inside the 24 hour window
7The realtime hub may need a direct websocket attempt with negotiation skipped, falling back to normal negotiationBoth paths have to exist in the client
8Account queries return simulated: true for evaluation accountsUseful, and it means your logs can prove which kind of account produced a number

Trap 3 has a documented cause. The Python standard library notes that before version 3.11, fromisoformat only supported the formats that isoformat() itself emits (datetime documentation, checked 2026-08-27), which means three or six fractional digits. Five digits is valid ISO 8601 and was not accepted.

Traps 1 and 2 are the expensive pair, because both of them fail by continuing to work. A subscription to the wrong contract looks exactly like a subscription to the right one until you reconcile the data against something else. We lost six trading days to trap 2 on a different instrument, described in 我以為它過了,多五天資料後沒過 (in Chinese).

The zombie bot

On 2026-08-11 two of our bots traded the same session. 2 orders went out that we did not intend, and that day’s data was contaminated.

One line of systemctl state, one contaminated trading day.

The cause was a systemd unit left over from July, still enabled with automatic restart. We had moved on to a new deployment and stopped thinking about the old one.

Our debugging order was: rule out the scheduler, rule out the monitoring script, rule out the watchdog, and only then discover that a retired unit was quietly alive.

The manual is explicit about the part we got wrong: systemctl disable “does not implicitly stop the units that are being disabled” (systemctl man page, checked 2026-08-27). Enabled, disabled, running and stopped are two independent pairs, and we had been treating them as one.

We now disable the unit, rename the file, and point the old path at a null device, so the process cannot come back by accident. Three steps instead of one, because the first two are each individually reversible by a stray deploy script, and the failure they prevent cost us a trading day and 2 unintended orders.

When you deploy a new version, the old version’s resident services do not disappear just because you stopped using them.

The same shape appears in 一個算損益給人看的函式,讓賣出持倉這件事沒有發生 (in Chinese): something you believed was inert turned out to be on the critical path.

What we are not claiming

FAQ

Is any of this specific to one prop firm? The eight API traps are specific to this gateway. The two structural lessons are not: entitlements are undocumented until they fail, and a success response is not a state check.

Would a different language have avoided trap 3? Yes, and that is the point. It was a standard library parsing limitation, not an API bug. The API emitted valid ISO 8601 that our runtime declined to read.

Why publish integration details at all? Because they are engineering, not strategy. Anyone can copy our deployment practices and still have no idea what our entry conditions are. That boundary is described on the methodology page (in Chinese).

How much of this would monitoring have caught? The zombie bot, yes, with a check for how many processes hold the trading lock. The market data outage, no. Our recorder was reporting healthy because it was running fine, it simply had nothing to record.