제목이 아닌 서류를 통한 주별 수입: 무료 API 키에 하나의 스크립트

작성자

카테고리:

← 피드로
DEV Community · Mikhail Makeev · 2026-09-17 개발(SW)

Every earnings season I end up with the same three questions about my watchlist. Which of these names report this week? What did the ones that already reported actually say, as opposed to what a headline writer decided they said? And what macro release lands in the same window, because a Fed decision on the same afternoon changes how any print gets read.

I run AlphAI, so I have a biased answer to where that data lives. What I didn’t have was a script that answered all three on the free tier, so I dogfooded it the way a new user would: a fresh account and a fresh key, with no help from the inside. I built the script on a Saturday and ran it against the live API. I kept everything it returned. This is what happened, including the parts I didn’t like and the parts that are still not right.

The repo is makeev/alphai-earnings-week. Everything below comes from two runs: Saturday 12 September 2026, and Thursday the 17th, the morning after the Fed.

The budget

The free tier is one key, 20 requests per minute, 100 per day, no card. That’s the constraint, so the design started there.

The earnings endpoint, GET /api/symbols/{ticker}/earnings/, returns two things in one response: the company-confirmed date of the next report, and the list of reads AlphAI has published for past filings. A read is a structured analysis of the company’s own results filing, the 8-K a US company files with the SEC when it releases numbers, or the 6-K a foreign filer uses. It carries a verdict, the key metrics as printed in the filing, the guidance, plus a list of what the filing left out. Because both things arrive together, a watchlist of 25 names costs 25 requests. The macro calendar is one more. 26 out of 100. Once a release in the window has printed, one more request pulls the macro stories the feed scored 7 or higher since it, so a card built after the Fed costs 27.

The per-minute limit turned out to matter more than the daily one. I’ll come back to it.

The script

Two dependencies: alphai-sdk and httpx. The SDK covers the earnings call. The calendar endpoint isn’t wrapped yet, so that one is a plain request. I left it that way in the repo because you’ll hit the same edge with any SDK that lags its API.

from alphai import Client

with Client() as client:  # reads ALPHAI_API_KEY
    hist = client.symbols.earnings("ORCL")
    print(hist.next_report_date)  # a date or None, never an estimate
    read = hist.reports[0] if hist.reports else None
    if read and read.analysis:
        a = read.analysis
        print(read.fiscal_period, a.verdict, a.headline)
        for m in a.key_metrics[:3]:
            print(m.name, m.value, m.prior_year, m.yoy_change)

Enter fullscreen mode Exit fullscreen mode

The calendar call:

import httpx

r = httpx.get(
    "https://api.alphai.io/api/calendar/",
    params={"from_date": "2026-09-12", "to_date": "2026-09-19"},
    headers={"Authorization": f"Bearer {key}"},
)
for ev in r.json()["events"]:
    print(ev["scheduled_at"], ev["title"], ev["importance"], ev["has_sep"])

Enter fullscreen mode Exit fullscreen mode

The window is half-open, to_date exclusive. The news endpoints on the same API treat their date window as inclusive. That’s my API being inconsistent with itself, and I’ll come back to it.

The rest of week.py is formatting, and there’s more of it than I’d like. It writes a markdown card and a JSON sidecar that logs every HTTP response with a timestamp, the status, Retry-After and X-RateLimit-Remaining. It also carries two pacing modes for the measurements below. The whole file is about 430 lines. The part that talks to the API is the two snippets above. The sidecar is where the numbers in this post come from.

What came back

Macro first. The calendar for the week of 12 September held three rows: advance retail sales on Wednesday morning, the FOMC decision Wednesday at 18:00 UTC with a Summary of Economic Projections and a press conference half an hour later, and weekly claims on Thursday. Lennar reports on that same Wednesday. That’s the collision I wanted the card to surface without me remembering it.

Twelve of the 25 names had a confirmed next report date. Micron on 30 September, Costco on the 24th, Nike and Accenture on 1 October, Nvidia on 17 November. The other 13 had none. The API returns only a date the company itself has confirmed, and I’ll defend that choice: the alternative most vendors offer is an estimate, and the estimate is usually last quarter’s filing date plus 91 days. But a choice is not coverage, and 13 blanks out of 25 is what the choice costs. More on that below.

Two names had fresh reads. Oracle filed its Q1 FY27 results on 10 September and the read came back strong: total revenue up 30% year over year and cloud infrastructure up 121%, with next-quarter guidance of 30 to 34% growth. Adobe filed the same day, also strong. The mega caps all had reads from their late-July and August filings: Nvidia, Apple, Microsoft, Amazon, Meta. Meta’s was the only mixed in the set.

Sixteen names had no read at all, and most of that is the calendar. Reads exist only for filings since 28 July 2026, so anyone who reported before that has nothing until the next print: FedEx, Micron and Nike in June, Alphabet and Tesla a week before coverage began. Berkshire is a different case, and not a flattering one. It’s in the list of open problems below.

What a read looks like

The claim in the title is “from the filings, not the headlines”, so here is one read as the API returned it, trimmed to the fields the card uses. It’s Lennar’s, from the Thursday run, the filing that landed on FOMC day.

{
  "fiscal_period": "Third Quarter 2026",
  "verdict": "weak",
  "verdict_reason": "Net earnings attributable to Lennar declined to $284 million from $591 million, total revenues declined to $8.0 billion from $8.8 billion, new orders decreased 9%, home sales gross margin declined to 15.8% from 17.5%, and the company reduced its full-year 2026 delivery target to approximately 80,000 to 81,000 homes from 82,000 to 83,000 homes.",
  "key_metrics": [
    {"name": "Total revenues (In thousands)", "value": "$ 8,046,119", "numeric": 8046119.0, "unit": "USD", "scale": "thousands", "prior_year": "8,810,278"},
    {"name": "Homebuilding revenues (In thousands)", "value": "$ 7,759,497", "numeric": 7759497.0, "unit": "USD", "scale": "thousands", "yoy_change": "decreased 6%"},
    {"name": "Revenues from home sales", "value": "$7.7 billion", "numeric": 7.7, "unit": "USD", "scale": "billions", "yoy_change": "decreased 6%"}
  ],
  "guidance": {
    "period": "Fourth quarter of 2026",
    "gross_margin": "15.5% - 16.0%",
    "other": ["New Orders: 19,500 - 20,500", "Deliveries: 22,000 - 23,000", "Full-year 2026 deliveries: approximately 80,000 to 81,000 homes"]
  },
  "missing_items": [
    "Operating cash flow was not reported.",
    "Free cash flow was not reported.",
    "Fourth-quarter revenue guidance was not reported."
  ],
  "numbers_verified_from_document": true
}

Enter fullscreen mode Exit fullscreen mode

Three things in there do the work for me. The verdict comes with its reason, in the filing’s own numbers. The metrics come as printed, with the scale the filing stated next to them. And missing_items tells me what Lennar chose not to say, which on a quarter like this one is half the story. The card turns that into a row:

| ticker | next report | last read                               | verdict |
| LEN    |             | Third Quarter 2026 · sec_form8k · 09-16 | weak    |
| ORCL   |             | Q1 FY27 · sec_form8k · 09-10            | strong  |
| MU     | 2026-09-30  | no read yet                             |         |
| FDX    | 2026-10-28  | no read yet                             |         |

Enter fullscreen mode Exit fullscreen mode

The full card, with the macro rows and the metrics per name, is out/week-2026-09-17.md in the repo.

What broke, and got fixed

The metric values are strings. On Saturday their scale wasn’t in the payload. Oracle’s total revenue arrived as "$19,345". The filing’s table is in millions, so that’s $19.3 billion, and the read’s headline said $19.3 billion in plain words. The metric row didn’t. Adobe’s revenue arrived as "$6.76 billion", with a prior-year value of "$5,988 million" in the same row. Microsoft’s was "$90.0 billion" against a prior year of "$76,441". Meta carried the scale as text inside the value: "$60,801 (In millions)". Every one of these is faithful to how the company printed the number. None of them is safe to compare across companies without a parser, and a parser that guesses the scale is how you end up with an Oracle a thousand times smaller than Adobe. I filed it, and it shipped the same afternoon: every metric now carries numeric, unit and scale next to the string, filled only from what the filing itself says. Oracle’s row reads scale: "millions" because the filing’s own table header says so. Where nothing in the filing says it, scale stays null. A null you can see beats a multiplier guessed on your behalf. alphai-sdk 0.6.2 carries the three fields as typed attributes, and the card prints the string and the number side by side, with the number blank on a null scale.

The SDK fell over on a documented response. GET /api/symbols/{ticker}/earnings/latest/ answers 204 with no body when there’s no read yet. That’s in the spec. The Python SDK, version 0.6.0, raised InvalidResponseError on it, because success without JSON wasn’t a case it knew. The TypeScript SDK was worse in a quieter way: it resolved to undefined under a type that promised an object, so the README’s own example would throw on latest.uid. Both are fixed now. alphai-sdk 0.6.1 returns None, and the TypeScript package returns null from 0.5.1. Finding that by running the example instead of reading the docs is the whole reason I built this before writing about it.

BRK.B doesn’t exist, BRK-B does. The 404 message is good: it tells you to look the name up with /api/symbols/?search=. The SDK couldn’t send that parameter. It can now.

Everything in this section was fixed within a day, which says more about the size of the fixes than about the product being finished. The next section is the part that isn’t.

What is still not right

I’d rather you read these here than find them after signing up.

Coverage is young. Reads exist for filings since 28 July 2026, about 1,800 of them as of this week. Nothing earlier. A name that reported in June or in the first three weeks of July has no read until its next print. That’s 16 blanks out of my 25 on Saturday and 15 on Thursday. If your watchlist is small caps that report off-cycle, expect more blanks than reads for a while.

Confirmed dates are the minority. 13 of my 25 names had one on Thursday. A company-confirmed date exists only once the company has announced it, usually two or three weeks ahead of the print, so at any given moment most of a watchlist has no date on file. That’s the honest side of “no estimates”. If you need an estimate, you’ll need another source for it.

Berkshire was a bug on my side, not a data choice. I called it structural in an earlier draft, then blamed a missing document body. Neither was right. The filing and its press release did reach the pipeline on 11 August. The enrichment step saved the row with no ticker at all, because Berkshire prints its classes as BRK.A and BRK.B and the tag didn’t survive validation. A row with no ticker never shows in the feed and never gets a read. Five earnings filings out of 3,139 since late July ended up that way, four of them share classes, preferred series or renamed issuers. The fix is a fallback to the filing’s own ticker when the model leaves none. Berkshire’s Q2 read is live now, and the fallback went out the same day.

Two window conventions on one host. The calendar’s to_date is exclusive. The news endpoints’ to_date is inclusive. Both are documented, and neither is going to change soon because clients depend on each, so the inconsistency is yours to remember.

The SDK’s retry default is thin. Two retries, with the wait capped at 60 seconds. That default is what turned a mis-stated Retry-After into a crash in the next section. Client(max_retries=...) raises it, and pacing avoids the retry path entirely, but the default is still two. The SDK also still doesn’t wrap the calendar or the macro feed, so two of the three calls in this post are plain HTTP.

The rate limit, measured

This is the part I’d want someone to tell me before I wrote a bot.

I ran the same 26 requests two ways. With a 3.2-second pause between calls, the run took 80 seconds and never saw a 429. At full speed, relying on the SDK to honor Retry-After, it took 96 seconds with 22 rejections. It would have crashed without a fallback I’d written into the script.

Here’s the shape of it. The first 18 requests went through in 1.8 seconds. Two probe calls from the minute before were still on the books, which is why the allowance wasn’t a round 20. The 19th got a 429 with Retry-After: 2. The SDK waited 2 seconds and got another 429, also promising 2 seconds. It waited again and got a third, then gave up. From the response log, the window didn’t reopen until about 30 seconds after the burst. Once it did, the limiter admitted one request every 3.2 seconds, and from then on the SDK’s two retries were enough for each call.

The reason is the algorithm. The minute limit is a sliding window: the previous minute’s count decays linearly across the current one. Retry-After is computed as if the entire overage decays that way, but the requests that caused it sit in the current minute and don’t decay until it rolls over. After a fresh burst the header under-promised, and a client that trusted it twice and quit was following the contract straight into a wall. That one shipped the same day too. In the burst run after the fix the 21st request got Retry-After: 60, and the retry after it went straight through.

The practical rule doesn’t need the theory: space your requests at the rate the limit implies, three seconds and a bit, from the first call. It’s faster than bursting and it never touches the retry path. Rejected requests didn’t consume any of the daily 100, which I checked, so the burst experiment was free. Slower, though.

Thursday’s run added a footnote. The sleep-paced version, which never saw a 429 on Saturday, met one: the 21st request, Retry-After: 1, cleared by the SDK’s first retry a second later. My script fired the calendar call and the first ticker 0.18 seconds apart and only started spacing from the second request, so the first minute held 20 requests in 59.6 seconds, right on the limit. Spacing from the first call fixes it, and that’s in the repo now. The header named the real wait this time, which is what Saturday’s fix was for.

The second run

Thursday morning, with the calendar window starting on Monday so that Wednesday’s releases would show as elapsed. Both did, and the tape column filled in with eight macro stories the feed scored 7 or higher since then, seven of them about the hike, for one request. Lennar’s read arrived, as shown above. Confirmed dates went from 12 to 13: FedEx and Carnival came in, and Lennar’s dropped off once it reported. Twenty-eight responses, of which 27 counted against the day and the 429 didn’t, so the budget read 73 of 100 left.

Run it yourself

git clone https://github.com/makeev/alphai-earnings-week
cd alphai-earnings-week
export ALPHAI_API_KEY=ak_live_...
uv run week.py --watchlist watchlist.txt --days 7

Enter fullscreen mode Exit fullscreen mode

A free key takes about a minute at alphai.io/account/api-keys and doesn’t ask for a card. Edit watchlist.txt, one ticker per line, share classes with a dash. The card lands in out/, the response log next to it. --from 2026-09-14 starts the calendar on a Monday, so a release that already printed shows as elapsed and the tape column fills in. To reproduce the rate-limit numbers, --pace retry --wait-for-minute starts the burst on a clean minute.

What went into the tracker

Five items went in, from a script that was supposed to take an afternoon: the metric scale, the 204 handling in two SDKs, the missing search parameter, the Retry-After arithmetic, plus a date window the general news feed accepted while the dedicated macro feed didn’t. All five shipped the same day. The macro feed takes from_date now, so the tape after a Fed decision is one request. The Berkshire gap went in after the second run and was fixed the same day. The SDK’s calendar wrapper is still open.

원문에서 계속 ↗