I built a small web app for myself that places and schedules E*TRADE orders — quick one-tap tickets, a portfolio view, and a few order types the web platform doesn't have. It's a personal tool, not a product. But wiring it up surfaced a pile of things that aren't in any doc. Here are the parts worth writing down.
1. The quote endpoint hides the ticker where you least expect it
The first thing that broke: every position showed “No quote available,” even though the quotes were clearly coming back.
GET /v1/market/quote/{symbols} returns an array of quote objects. The obvious assumption is that each object has a symbol field, or that the market data sits at the top level. Both are wrong:
- The ticker is not at
quote.symbolorquote.All.symbol— those are empty. It's nested atquote.Product.symbol(next toProduct.securityType). - The market data isn't top-level either. It's under an
Allobject, with field names that don't match the obvious ones:- bid / ask →
quote.All.bid/quote.All.ask - last trade →
quote.All.lastTrade(notlast) - volume →
quote.All.totalVolume(notvolume) - extended-hours last →
quote.All.ExtendedHourQuoteDetail.lastPrice
- bid / ask →
So a quotes-by-symbol map keyed on quote.symbol produces a map of empty keys, and every lookup misses. The fix is a one-liner once you know it — read the ticker from Product.symbol and the data from All — but you will not guess it from the field names.
2. optionchains silently ignores the expiration you asked for
This one is worse, because it fails quietly and returns plausible-looking wrong data.
If you request a specific expiration's option chain with a combined expiryDate=YYYY-MM-DD parameter, the endpoint ignores it and hands you the nearest expiration's strikes and prices instead. No error. You ask for the June 18 chain and get next Friday's, and unless you check, you'll show a wrong (near-dated) bid/ask for an option the user actually holds.
The expiration has to be passed as three separate integer params:
GET /v1/market/optionchains?symbol=TXN&expiryYear=2026&expiryMonth=06&expiryDay=18
And then verify: read each returned leg's expirationDate and confirm it matches what you requested before you trust the prices.
3. After hours, E*TRADE goes dark — so I read from Schwab
E*TRADE's quote endpoint gets flaky outside regular hours. More than once I'd request a quote at, say, 3am and get back an empty array — no bid, no ask, nothing — while the stock obviously had a perfectly good extended-hours market.
I already had a separate process running that authenticates to Schwab and keeps a fresh token. So the order ticket now asks that source first and only falls back to E*TRADE when it can't produce a real bid/ask. The fallback is the key part: if the preferred source returns anything short of a usable two-sided quote, drop through to the broker. The payoff showed up immediately — at a moment when E*TRADE returned [] for a name, the Schwab path returned a live bid/ask within a penny of the last print. For an order ticket, where the whole point is to price against the current market, “fresher of two sources” beats “whatever one broker feels like returning.”
4. Chase and Escape: orders that re-price themselves to the live market
The order type I'm happiest with. The idea: fill a large quantity in small clips, sending the next clip only after the previous one fully fills, and re-quoting each clip to the current market so it actually executes as the price moves.
My first cut got this subtly wrong, and it's an instructive bug. I captured the limit price once, when you hit the button, and reused it for every clip. So if you were covering a short and the price climbed after clip one filled, clip two went out at the original (now-too-low) price and just sat there. It “chased” in the sense of repeating, not in the sense of following. The fix is obvious in hindsight: re-fetch the quote inside the loop, before each clip, and price to the current touch.
But “price to the current touch” has two opposite meanings, and that's the interesting part. Which side of the book you take depends on whether you want to get filled now or hold out for a better price — and those are different order types:
- Chase = cross the spread. Aggressive. You take liquidity to guarantee a fill, and you follow the market wherever it goes.
- Buying or covering → pay the ask
- Selling or shorting → hit the bid
- Escape = back away. Passive. You post on your own side of the book and let price come to you, chasing price improvement instead of fills.
- Buying or covering → post at the bid
- Selling or shorting → offer at the ask
So the same control is “Chase” or “Escape” depending on which side you've pinned relative to your action. Pin the aggressive side and it crosses up to keep filling; pin the passive side and it steps away looking for a better price. The button label flips between the two words on its own based on action + side, which turned out to be the clearest way to make sure I knew which behavior I was about to get.
A couple of engineering notes that mattered more than I expected:
- Make Stop responsive. Each clip polls the order's status until it fills. The naive loop checks the stop flag only at the top of each iteration — after a sleep and a status round-trip — so hitting Stop felt dead for several seconds while the current poll finished. Re-check the flag immediately after the sleep, before the (slow, and possibly hanging) status request, and bound that request with a timeout. Now Stop bites within a second.
- Poll fast, then back off. A fixed poll interval is the wrong shape. A marketable clip fills almost instantly, so start polling fast (a few hundred ms) to catch it, then back off toward ~1.5s so a resting order doesn't hammer the status endpoint. The per-clip floor is still two unavoidable round-trips — placing the order and confirming the fill — but you shouldn't add a second and a half of your own latency on top.
- Stop should pause, not abandon. When you stop mid-clip, the safe thing is to cancel the working order at the broker (so nothing's left live), remember how much already filled, and offer Resume to continue the remaining quantity — rather than leaving an orphan order on the book.
5. Where per-symbol margin requirements actually live
To compute a maintenance figure I needed each symbol's house requirement (30% for a typical name, but 50–100% for volatile or leveraged ones). E*TRADE's trading API doesn't expose it. It only lives on their margin-requirements web tool.
That tool, though, is backed by a plain JSON endpoint that takes a list of symbols and returns long/short requirements per symbol — and an empty account id returns the firm-wide requirements, no authentication needed. Batch your symbols, parse the per-side requirement (it comes typed as either a percentage or dollars-per-share), and cache the result for an hour since firm requirements change at most daily. It's the kind of thing that's “undocumented” only because nobody wrote it down, not because it's hidden.
6. Small details that are easy to get wrong
A few UI/logic things that are obvious in retrospect and wrong by default:
- “Held shares” has a side. When you're selling to close, the relevant holding is your long lots; when you're covering, it's your short lots. Summing the absolute quantity across both (so a name held both long and short shows the combined total) is just a bug. Sell counts longs, cover counts shorts — and showing the other side as a note (“you are short 540 shares”) doubles as a wrong-side warning.
- Enter should not fire an order. With no preview step, letting Enter/Return submit the ticket is one fat-fingered keypress away from a live order. Enter now just blurs the focused field — which, on mobile web, is the only way to dismiss the keyboard (there's no “hide keyboard” API; you remove focus and the OS retracts it).
- One tap to flip open/close. A small toggle next to the ticket title swaps the action between its opening and closing counterpart — buy↔cover, sell↔short — and the label tells you which you're on. Tiny, but it removes a whole category of “wait, is this opening or closing a position” mistakes.
Takeaways
Three things I'm taking with me:
- Broker APIs lie about their shapes. Read a real response, not the docs. The ticker was in the one place I'd never have looked, and the option-chain endpoint failed by returning the wrong-but-plausible answer.
- Market microstructure belongs in your tooling. “Re-price to the market” is two different orders — chase and escape — and conflating them gives you an order type that doesn't do what its name says.
- Check a metric against ground truth before you believe its units. “Margin used” looked right, read large, and was off by my entire short book. The broker's own debit balance was the check that caught it.
None of this is exotic. It's just the gap between “the API returned 200” and “the number on the screen is the number I think it is” — which, for anything touching real orders and real money, is the only gap that matters.