Rowan Das
← Back to the main page

Morning brief

What moved overnight

A short read on the overnight session, written each weekday by a Llama model that only sees the headlines my pipeline retrieves. The small numbers link to the headline behind each claim.

Sources

Every headline the model was given, numbered the way it cites them.

    How this brief was made

    This page is built with retrieval-augmented generation, or RAG. A language model on its own has no idea what happened in markets this morning, and if you ask it anyway it will make up a plausible answer. RAG fixes that by searching for the relevant news first, then handing only that news to the model with instructions to write from it and cite it.

    The split matters. Code handles the facts: fetching prices, parsing feeds, storing and searching headlines. The model handles the part code can't do well, which is reading the sources and writing the note. Both models run through Ollama on the same machine as the code, so no text goes to an outside AI service.

    1. Step 1

      Pull prices and headlines

      Python asks yfinance for the last two closes on six instruments: the S&P 500, the Nasdaq, the 10-year yield, USD/JPY, crude and gold. It then reads Google News RSS feeds for eight searches. RSS arrives as XML, so the code parses each item, drops anything older than 16 hours, strips the HTML out of the snippet and removes stories that showed up under more than one search.

      Each story gets an ID made from a hash of its link. The same article always gets the same ID, which is how it avoids being stored twice on later mornings.

      # headlines.py "id": hashlib.sha1(link.encode()).hexdigest()[:16]

    2. Step 2

      Turn each headline into a vector

      An embedding model, nomic-embed-text, reads each headline and its snippet and returns a list of 768 numbers. Those numbers work like coordinates: headlines that mean similar things land close together, even when they share no words. "Brent climbs on Iran fears" and "Oil prices rise" end up neighbours. That is what lets the next steps search by meaning instead of keywords.

      Headlines and search questions get different prefixes before embedding, because that is how this model was trained to tell them apart.

      # llm.py: Ollama serves the model on this machine POST localhost:11434/api/embed {"model": "nomic-embed-text", "input": ["search_document: Oil prices rise as ..."]}

    3. Step 3

      Store them in a vector database

      The vectors go into Chroma, a vector database saved as files next to the code. Each entry keeps its vector, its text, and the title, source, link and publish time. Headlines stay for 14 days before they are deleted.

      This is the memory of the system. The language model never changes and never learns from these runs. What grows is the database, so each morning can draw on a story from two days ago that today's 16-hour window would have missed.

    4. Step 4

      Search the database for each move

      For every instrument the code writes a plain-English question in the words a headline would use, embeds it the same way, and asks Chroma for the four closest headlines from the last three days. Closeness is cosine similarity: 1 means the same meaning, 0 means unrelated.

      A vector search always returns its nearest results, even when nothing is actually near. So anything below a similarity cutoff is thrown away, and a quiet day gets no sources instead of the wrong ones. These were this morning's searches:

    5. Step 5

      Build the prompt

      The retrieved headlines are given one number each and written into the prompt under the six moves, with a note on which numbers matched which move. The rules come first: use only these sources, cite them by number, and say there was no clear catalyst when nothing explains a move.

      # an excerpt of the prompt the model reads - Crude Oil: +4.09% to 95.93 (possibly related sources: [17], [18]) [17] Sep 24 10:12 PM ET | CNBC | Oil prices rise as investors seek ...

    6. Step 6

      Write, then check the citations

      A Llama model running on Ollama writes the brief at a low temperature, which keeps it close to the sources instead of improvising. The context window is raised so the whole prompt fits, since text past the limit would be cut off without any error.

      Afterwards the code reads every citation in the text and flags any number that doesn't match a source. That catches invented citations. It can't prove a source supports its sentence, so the sources are listed above for you to check.

    Step 1 of 6: raw text
    Fig. 1Headlines in embedding space768 dimensions, drawn in 2

    Each dot is a headline. Scroll through the steps to watch them get embedded, stored and searched.

    I built and tested the pipeline on my laptop with Llama 3.1 8B. Each weekday at 6:30 AM Eastern, a GitHub Actions job installs Ollama on a fresh machine, downloads the models, runs the same Python and commits the new brief and database to this site's repository, which redeploys the page.