etrade-pull
Pull a live E*TRADE account — positions, weights, cost basis, unrealized P&L, or full transaction history — via a bundled OAuth 1.0a client. Use whenever real account numbers are needed rather than a
SKILL.mdskills/etrade-pull/SKILL.md
GitHubPull live E*TRADE data
Setup. The scripts sit beside this file. Run them from a working folder that holds your
.env(E*TRADE consumer key/secret) and adata/directory; install deps withpip install -r requirements.txtwhere one is included. Part of a set:etrade-pull,order-check,portfolio-review,realized-pnl,rebalance-plan,price-history,trade-journal,intrinsic-value,sec-financials,buffett-checklist.
Scripts: fetch_portfolio.py → data/portfolio.csv; fetch_transactions.py →
data/transactions.csv; etrade.py is the client to import.
source .venv/bin/activate
ETRADE_ENV=prod python fetch_portfolio.py
ETRADE_ENV=prod python fetch_transactions.py --start 2023-01-01
Called from portfolio-review, which is the entry point for anything that
starts "what do they hold."
Rules
ETRADE_ENV=prodon every single call. Unset means sandbox, and the sandbox returns four canned accounts with zero securities trades, dated 2013. It does not error and it does not look empty — it looks like a real account belonging to someone who never trades. The OAuth URLs are hardcoded to prod either way, so a successful login proves nothing about which environment you're reading.Never print or echo the contents of
.envor.tokens. They are the account. A traceback that includes the consumer secret has published it.Re-pull rather than reuse a CSV from a previous session. Prices move; weights computed off yesterday's file are wrong in a way nobody notices.
The auth gotcha
OAuth 1.0a with an out-of-band verifier: there is no redirect, the user reads a short code off E*TRADE's page and pastes it back.
Tokens go inactive after ~2 hours idle and expire at midnight US Eastern, every
night. etrade.get() catches the resulting 401 and re-runs the handshake once,
which is enough for a long-running script to cross the boundary.
But the handshake needs a human, and input() hangs in a non-interactive shell —
which is where an agent usually is. Split it across two invocations instead:
python auth_cli.py start # prints the authorize URL — give it to the user
python auth_cli.py finish <code> # exchange the code they paste back
If a pull hangs with no output, this is why. Kill it and use auth_cli.py.
What the API will not give you
- ~3 years of transactions. Older dates return error 2003. A complete archive needs statement CSV exports from the web UI.
- A request with no date range silently returns only the last ~month.
fetch_transactions.pyalways sends an explicit window for this reason. - Field shapes and the sandbox's exact limitations are documented in
SCHEMA.md.
Writing: orders can be placed, and the API's limits shape the design
etrade.py exposes post() / put(); the transmitter is place_order.py, owned
by order-check. Four tool facts:
- ⚠️ There is NO OCO / bracket / one-cancels-other field.
priceTypecovers MARKET / LIMIT / STOP / STOP_LIMIT / trailing / hidden;orderTermcovers GOOD_UNTIL_CANCEL / GOOD_FOR_DAY / GOOD_TILL_DATE / IMMEDIATE_OR_CANCEL / FILL_OR_KILL; the only linkage isconditionType(CONTINGENT_GTE/CONTINGENT_LTE) against another symbol's price. OCO is a Power-E*TRADE UI construct. Any plan prescribing "an OCO bracket" describes something the API cannot do — say so rather than trying. - A write is NEVER auto-retried on a 401.
get()re-auths and replays;_send()deliberately raises instead. Replaying an order body after a token refresh can transmit the same order twice, and a duplicate stop is a duplicate exit. - A preview creates no order, so
POST /orders/previewis a free, safe, end-to-end validation against the live account and returns thepreviewIdthatplacerequires. Use preview as the integration test; never use the sandbox for orders — it holds no positions, so nothing about a sell is exercised. clientOrderIdis capped at 20 alphanumeric characters. Longer or punctuated ids fail with an error that reads like an auth problem.
Endpoints: orders/preview, orders/place, orders/{id}/change/preview,
orders/{id}/change/place, orders/cancel (a PUT taking {"CancelOrderRequest": {"orderId": N}}).
/orders is also the only SIMULTANEOUS price record you get
Instrument[].averageExecutionPrice on an EXECUTED order is a real fill, and
executedTime is stamped per leg. When several legs execute in the same minute,
those fills are simultaneous prices for every name involved — which is what makes
a "should I have bought the other one instead" comparison honest, with no
close-vs-intraday fudging. Nothing else in this API gives you that.
⚠️ A cash-identity reconciliation is NOT a fill price
If a fill hasn't posted yet, the tempting move is to solve for it: proceeds +
prior cash − cost = the residual the broker reports. Don't. Real instance:
that arithmetic produced ~$380.00 against an actual fill of $377.955 — off
by $2.045/share — and it failed in the flattering direction, recording a sell as
better than the quote when it was worse.
Solving an identity for its one unknown silently absorbs every unmodelled term —
fees, unsettled credits — into precisely the number you wanted. It cannot come
out inconsistent, so it never warns you. Read the fill off /orders, or write
"unknown." Never off an identity, and never off the limit price: a limit fills at
or better than its limit.