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
- Score each headline, 1 very bearish to 5 very bullishScale
- HLVR4.19
- VLTX1.33
- QUIL3.22
- TSSL3.00
- Does it hold news that moves the stock today?Yes/No
- HLVRyes 89%
- VLTXyes 90%
- QUILno 3%
- TSSLno under 1%
- Rank what to read firstSort
- VLTX
- HLVR
- QUIL
- TSSL
- 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
Two questions, asked about every headline:
sentimentis a Scale question with fivelevels, from very bearish to very bullish.materialis a Yes/No: is there news here that moves the stock today?
/decide/batchtakes one group per headline inrequests. 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 } } }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 hasanswer:yes,no, ornullwhen 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")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.sortedlists 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 } } }- 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
- Use your own ids in the Sort list, such as tickers or article ids.
sortedthen maps directly to your data. - Filter before you rank. On a busy day, sort only the headlines marked
material. - This is a reading list. The scores decide what a person reads first. People make the trades.
- 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.