On the 20th of August my trading system bought a company I had never heard of, with real money, because two APIs disagreed about what four letters meant. It survived on luck. Here is what I got wrong, and the plumbing I would build first if I started again. Up front, because it changes how you should read this: none of what follows made me money. The strategy side is the hard part and mine is not solved. What is transferable is the data layer, which turns out to be most of the work and almost none of the writing about this subject. Nothing here is advice. The bug Two systems. A screener that finds candidates, and a broker that executes them. The screener speaks Yahoo symbols. The broker has its own instrument list. Yahoo said LB. My code turned that into the broker's format, LB_US_EQ, and placed the order. Yahoo's LB is LandBridge, around $89. The broker's LB_US_EQ is Bath & Body Works, around $19 — the name it used before it renamed itself to BBWI. Same four letters. Different companies. The ticker had been reassigned, and the broker's instrument list still carried the old one. It survived only because $19 was instantly below the $83 stop loss, so the exit loop dumped it on the next pass. Reverse the numbers — a wrong instrument trading above its stop — and I would have held a company I never chose, indefinitely, with every subsequent analysis describing a different share. The fix is one line of principle: A symbol is a display label, not a primary key. Concretely, when both sources know the company's name, they have to agree before an order goes anywhere: for suffix in _T212_SUFFIXES: cand = f"{symbol}{suffix}" if cand not in index: continue if not _names_agree(yahoo_name, index.get(cand)): print(f" [ticker] {symbol} -> {cand} REFUSED: " f"{yahoo_name!r} is not {index.get(cand)!r}") return None return cand return None Name matching is fuzzy — "Bath & Body Works Inc" against "Bath and Body Works" — so it compares token sets rather than strings, and it fails closed. If it cannot establish that the two are the same company, it does not trade. Suffixes, and the 551 listings I was silently dropping Brokers encode the exchange into the ticker. Mine writes US equities as NVDA_US_EQ. My first version handled two suffixes: ("_US_EQ", "_EQ") That is wrong, and wrong in the worst way: quietly. Every Canadian and most European listings were being dropped as "not tradable" while sitting in the instrument list the whole time. HUT is carried as HUT_CA_EQ. That is 552 Canadian listings alone, discarded with no error, no warning and no log line — the universe just looked smaller than it was. The real list: _T212_SUFFIXES = ("_US_EQ", "_EQ", "_CA_EQ", "_DE_EQ", "_BE_EQ", "_AT_EQ", "_PT_EQ", "_BB_EQ") There is no way to derive that. You read it off the broker's own instrument dump, and you re-read it when it changes. Currency is not in the ticker either This is the one that would have cost me the most. The broker's London listings all end in the same suffix, l_EQ. Here is what that single suffix actually contains, counted from my own instrument book this morning: currency instruments GBX 2,410 USD 1,299 GBP 548 EUR 212 CHF 2 One suffix. Five currencies. Including US dollars, on the London suffix. And note the top two rows: GBX is pence, GBP is pounds. They differ by exactly 100×, and there are 2,410 of one and 548 of the other sitting behind the same four characters. So you cannot infer currency from the suffix, from the exchange, or from the shape of the symbol. You look it up, once a day, from the broker's instrument endpoint, and you store it next to the ticker: def currency_of(ticker: str) -> str | None: return state.currency_of(ticker) Everything downstream — position sizing, stop distances, the £ figures in a report — reads that field rather than guessing. A 100× error in position sizing does not look like a bug. It looks like a decision. What the free data endpoints actually give you Three sources, and their real limits rather than their marketing. Yahoo's chart endpoint query1.finance.yahoo.com/v8/finance/chart is keyless and still works. One request returns daily open/high/low/close/volume plus timestamps, which is enough for moving averages, RSI and candle patterns. What it does not carry: earnings dates, or market cap. That sounds minor and is not. My US flow avoids opening a position just before an earnings announcement — a blackout window. The chart endpoint cannot support that, so for the markets where it is my only source, positions cannot be labelled as entries at all. That side of the system is a watchlist, and it says so in the email rather than pretending otherwise. Two more things worth knowing before you build on it: The region parameter on the predefined screener endpoint is accepted and then ignored. Asking for GB gainers returns NYSE and Nasdaq names. It does not error. You just get the wrong country. The v7/quote and quoteSummary endpoints, which would let you bulk-look-up a whole universe, now require a crumb. It is scraped, not consumed. It is unversioned and unsupported, and one day it will change shape without telling anyone. Alpha Vantage, as a keyed second opinion I added this precisely because of the previous paragraph. The free key's limits, taken from the API's own refusal text rather than a docs page: Please consider spreading out your free API requests more sparingly (1 request per second) ... the free key rate limit (25 requests per day) One per second, twenty-five per day. That single fact determines the entire design. A scan prices dozens of symbols, so this can never be a drop-in second source — it would exhaust the day's quota before the first report and then return nothing, silently, for every later call. So it is used in exactly two places: as a fallback when the primary returns nothing, and to corroborate the two index symbols that set the market regime — the read every other decision is judged against. One trap worth the price of admission: Alpha Vantage signals throttling with HTTP 200 and a sentence of prose, in an Information key. If you parse only for the data key, "please slow down" arrives looking exactly like "this symbol has no history". A wrong answer wearing a right answer's clothes. My first version then latched on the first refusal and disabled the second source for the whole run — which turned "the two sources agree" into "there was no second source", with no way to tell those apart from the outside. The per-second limit is transient. Wait, retry once, and only give up on a second refusal. The broker API Three decisions here that I would keep in any system that can spend money. Auth is HTTP Basic, and the key knows the account. Key as username, secret as password. More useful: the first eight characters of the key are the account number. So you can assert that the credential in the vault belongs to the account you believe you are trading: # a swapped secret is refused, not silently run That check costs nothing and catches the single worst class of configuration error. There is no idempotency key, so writes never retry. Reads retry with backoff and respect the x-ratelimit-* headers. Writes do not retry at all, ever, because a retried POST is a second order. An API without idempotency is an API where your retry logic is a duplicate-order generator. The demo host and the live host are named separately, at every call site. There is no default that can reach the live host by accident. The shape I would build first If I were starting again, in this order: Sync the broker's instrument list into a local table, daily. Ticker, name, currency, size cap, trading hours. This is your identity source. Not the screener, not the price feed — the venue that will actually fill the order. Resolve every external symbol through that table, and refuse when the names disagree. Log the refusal loudly. A refusal is information; a silent drop is the 551 listings. Store currency next to the price, always. Never infer it. Never assume a price is in the unit you expect. Assume every free endpoint lies about its limits and will change shape. Read the refusals, not the docs. Cache aggressively. Only then write the strategy. I did those in roughly the reverse order, which is why I have a post about buying the wrong company rather than a post about returns. The honest ending None of this made money. The plumbing above is solid, tested and boring, and that is the point of it — but a correct pipeline that executes a mediocre idea just executes a mediocre idea faster and more reliably. What I would say to anyone starting: the interesting problem is not the broker API. It is that the same share has a different name in every system that touches it, and none of those systems will tell you when they disagree. You have to check. Mine did not, and it bought the wrong company.