Skip to content

Score news headlines

Score sentiment and impact for each headline and rank what to read first.

A trader sees hundreds of headlines a day and has time to read a few. This guide scores every headline for sentiment and whether it's likely to move the stock today, in one batch call, then ranks them by what to read first.

  • Scale
  • Yes/No
  • Sort
Try
  1. Score each headline, 1 very bearish to 5 very bullishScale
    • HLVR4.19
    • VLTX1.33
    • QUIL3.22
    • TSSL3.00
  2. Does it hold news that moves the stock today?Yes/No
    • HLVRyes 89%
    • VLTXyes 90%
    • QUILno 3%
    • TSSLno under 1%
    The number is the chance of yes.183 ms
  3. Rank what to read firstSort
    1. VLTX
    2. HLVR
    3. QUIL
    4. TSSL
    110 ms
  4. Your codeReading list sent

    VLTX goes first. 2 of 4 headlines hold no new information.

Scale and Yes/No ran in one batch of 8 questions: 183 ms. Sort took 110 ms. Made-up companies, not financial advice.

Get the code

Set SAGE_API_KEY (get a free key), then run it. Python: pip install requests. TypeScript: Node 22+. Save as recipe.mts and run npx tsx recipe.mts.

A null answer is not an error. Sage2 isn't sure, and each step below says what to do with it. Limits, errors and retries: Limits and errors.

import os
import requests

def sage(path, body):
    r = requests.post(
        "https://sage.levanto.ai" + path,
        headers={"Authorization": f"Bearer {os.environ['SAGE_API_KEY']}"},
        json=body,
        timeout=30,
    )
    r.raise_for_status()
    return r.json()

headlines = [
    "Halvorsen Freight (HLVR) raises full-year guidance after record third-quarter bookings.",
    "Regulator rejects Veltrix Bio's (VLTX) lead drug; the company cuts 20% of staff.",
    "Quill Robotics (QUIL) CEO says the company is 'cautiously excited about being excited' in a new interview.",
    "Tessel Energy (TSSL) shares unchanged as the company confirms its previously announced dividend.",
]

QUESTIONS = [
    {
        "id": "sentiment",
        "kind": "scale",
        "instructions": "How does this headline read for the named company's stock?",
        "levels": [
            {"level": 1, "description": "Very bearish"},
            {"level": 2, "description": "Bearish"},
            {"level": 3, "description": "Neutral"},
            {"level": 4, "description": "Bullish"},
            {"level": 5, "description": "Very bullish"},
        ],
    },
    {
        "id": "material",
        "kind": "yesno",
        "instructions": "Does this headline contain new information likely to move the stock today?",
    },
]

scores = sage("/decide/batch", {"requests": [{"content": h, "questions": QUESTIONS} for h in headlines]})

def answers(out, group=0):
    # Each answer is a full decision. Failed ones (ok is false) are left out: treat a missing id as "not sure".
    return {a["result"]["id"]: a["result"]["result"] for a in out["results"][group]["answers"] if a["ok"]}

def reading(i):
    a = answers(scores, i)  # a missing answer reads as not sure: None
    return a.get("sentiment", {}).get("expectation"), a.get("material", {}).get("answer")

ranked = sage("/decide", {
    "content": {"kind": "list", "value": [{"id": str(i), "content": h} for i, h in enumerate(headlines)]},
    "question": {
        "id": "read_first",
        "kind": "sort",
        "instructions": "Sort by how much a trader needs to read this now, most important first.",
    },
})["result"]["sorted"]

for i in map(int, ranked):
    sentiment, moves = reading(i)
    score = "  ?  " if sentiment is None else f"{sentiment:.1f}/5"
    flag = {"yes": "moves", "no": "     "}.get(moves, "  ?  ")
    print(score, flag, headlines[i])

How it works

  1. Scale

    Score every headline

    Scale docs

    Two questions, asked about every headline:

    • sentiment is a Scale question with five levels, from very bearish to very bullish.
    • material is a Yes/No: is there news here that moves the stock today?

    /decide/batch takes one group per headline in requests. A whole feed is one call.

    Runs with the helper from Get the code.

    headlines = [
        "Halvorsen Freight (HLVR) raises full-year guidance after record third-quarter bookings.",
        "Regulator rejects Veltrix Bio's (VLTX) lead drug; the company cuts 20% of staff.",
        "Quill Robotics (QUIL) CEO says the company is 'cautiously excited about being excited' in a new interview.",
        "Tessel Energy (TSSL) shares unchanged as the company confirms its previously announced dividend.",
    ]
    
    QUESTIONS = [
        {
            "id": "sentiment",
            "kind": "scale",
            "instructions": "How does this headline read for the named company's stock?",
            "levels": [
                {"level": 1, "description": "Very bearish"},
                {"level": 2, "description": "Bearish"},
                {"level": 3, "description": "Neutral"},
                {"level": 4, "description": "Bullish"},
                {"level": 5, "description": "Very bullish"},
            ],
        },
        {
            "id": "material",
            "kind": "yesno",
            "instructions": "Does this headline contain new information likely to move the stock today?",
        },
    ]
    
    scores = sage("/decide/batch", {"requests": [{"content": h, "questions": QUESTIONS} for h in headlines]})
    Example Sage2 output
    {
      "results": [
        {
          "answers": [
            {
              "ok": true,
              "result": {
                "id": "sentiment",
                "kind": "scale",
                "result": {
                  "expectation": 4.1947,
                  "probabilities": [
                    {
                      "level": 1,
                      "probability": 0.0005
                    },
                    {
                      "level": 2,
                      "probability": 0.0006
                    },
                    {
                      "level": 3,
                      "probability": 0.0057
                    },
                    {
                      "level": 4,
                      "probability": 0.7899
                    },
                    {
                      "level": 5,
                      "probability": 0.2033
                    }
                  ],
                  "confidence": 0.6593
                },
                "meta": {
                  "model": "levanto-sage-2"
                }
              },
              "error": null
            },
            {
              "ok": true,
              "result": {
                "id": "material",
                "kind": "yesno",
                "result": {
                  "answer": "yes",
                  "probability": 0.8871
                },
                "meta": {
                  "model": "levanto-sage-2"
                }
              },
              "error": null
            }
          ]
        },
        {
          "answers": [
            {
              "ok": true,
              "result": {
                "id": "sentiment",
                "kind": "scale",
                "result": {
                  "expectation": 1.332,
                  "probabilities": [
                    {
                      "level": 1,
                      "probability": 0.6767
                    },
                    {
                      "level": 2,
                      "probability": 0.3175
                    },
                    {
                      "level": 3,
                      "probability": 0.0038
                    },
                    {
                      "level": 4,
                      "probability": 0.001
                    },
                    {
                      "level": 5,
                      "probability": 0.001
                    }
                  ],
                  "confidence": 0.5878
                },
                "meta": {
                  "model": "levanto-sage-2"
                }
              },
              "error": null
            },
            {
              "ok": true,
              "result": {
                "id": "material",
                "kind": "yesno",
                "result": {
                  "answer": "yes",
                  "probability": 0.905
                },
                "meta": {
                  "model": "levanto-sage-2"
                }
              },
              "error": null
            }
          ]
        },
        "… 2 more, one per request"
      ],
      "meta": {
        "model": "levanto-sage-2",
        "request_count": 4,
        "question_count": 8,
        "latency_ms": 183.2,
        "usage": {
          "billed_input_tokens": 682,
          "image_count": 0,
          "image_tokens": 0,
          "input_tokens": 682,
          "output_tokens": 0
        }
      }
    }
  2. Yes/No

    Read the score and the yes/no

    Yes/No docs

    Results come back in the order you sent them. A Scale answer has expectation, the probability-weighted level. A value of 4.2 is between bullish and very bullish. A Yes/No answer has answer: yes, no, or null when Sage2 isn't sure.

    Runs with the helper from Get the code.

    def answers(out, group=0):
        # Each answer is a full decision. Failed ones (ok is false) are left out: treat a missing id as "not sure".
        return {a["result"]["id"]: a["result"]["result"] for a in out["results"][group]["answers"] if a["ok"]}
    
    def reading(i):
        a = answers(scores, i)  # a missing answer reads as not sure: None
        return a.get("sentiment", {}).get("expectation"), a.get("material", {}).get("answer")
  3. Sort

    Rank what to read first

    Sort docs

    Send the headlines as a list, each with an id, and ask a Sort question. Here the id is the headline's position in the feed, as a string. sorted lists the ids, most important first.

    Runs with the helper from Get the code.

    ranked = sage("/decide", {
        "content": {"kind": "list", "value": [{"id": str(i), "content": h} for i, h in enumerate(headlines)]},
        "question": {
            "id": "read_first",
            "kind": "sort",
            "instructions": "Sort by how much a trader needs to read this now, most important first.",
        },
    })["result"]["sorted"]
    Example Sage2 output
    {
      "id": "read_first",
      "kind": "sort",
      "result": {
        "sorted": [
          "1",
          "0",
          "2",
          "3"
        ],
        "confidence": 0.5624
      },
      "meta": {
        "model": "levanto-sage-2",
        "latency_ms": 109.5,
        "question_count": 1,
        "usage": {
          "billed_input_tokens": 1186,
          "image_count": 0,
          "image_tokens": 0,
          "input_tokens": 1186,
          "output_tokens": 0
        }
      }
    }
  4. Your code

    Print the reading list

    Walk the ranked ids and show each headline with its score. A headline Sage2 isn't sure about gets a ?. A person reads those.

    Runs with the helper from Get the code.

    for i in map(int, ranked):
        sentiment, moves = reading(i)
        score = "  ?  " if sentiment is None else f"{sentiment:.1f}/5"
        flag = {"yes": "moves", "no": "     "}.get(moves, "  ?  ")
        print(score, flag, headlines[i])

Made-up companies. Not financial advice.

Before you ship

  1. Use your own ids in the Sort list, such as tickers or article ids. sorted then maps directly to your data.
  2. Filter before you rank. On a busy day, sort only the headlines marked material.
  3. This is a reading list. The scores decide what a person reads first. People make the trades.
  4. Batching saves round trips, not tokens. Each headline is read once per question, so four headlines with two questions are about 700 tokens. Each call answers in about 0.1 s. See Batch.

Sage / Levanto

Start free.

Sign in or create an account.

Continue with Google
or with email

We’ll email you a 6-digit code.