The Future That Could Have Been: Turning Web Text into Semantic Stock Betas

Markets move on wars, tariffs, pandemics, elections, and central bank decisions. These events are reported in text. The problem? Risk models cannot read. They are built from prices and financial statements because that is what we could measure when they were invented. Today we are releasing the fix.
Today we're proud to announce the release of semantic-factors, an open-source Python library that turns NOSIBLE World coverage into daily risk factors and stock-level betas. Simply define a risk in plain text. The package quantifies it every day going back more than a decade. All you need is a NOSIBLE World API key.
Explore the 30 semantic factors or read the package on GitHub.
Risk models cannot read
In 1973, Robert Merton's ICAPM showed that equilibrium expected returns compensate investors for exposure to shocks that change future investment opportunities. What the theory left unresolved was empirical identification: which state variables summarize the information investors actually possess?
Contemporary empirical finance largely worked with structured return data, under genuine computational constraints. Fifty years later, news text provides a new measurement layer. As Bybee, Kelly, and Su put it, ICAPM risk is tied to news about state variables tracking wealth and future investment opportunities.
Fama and MacBeth's 1973 empirical work used structured monthly percentage returns and described the balance between computation costs and the desire to reform portfolios frequently. That was a sensible research design for its time. It also left a clear opening for a new measurement layer built from dated observations of what is happening in the world.
Researchers have published more than 400 factors. The A Census of the Factor Zoo shows why a factor needs careful definition, transparent construction, and honest validation. Research on factor decay shows why signals need to be monitored after publication. The replication literature is a reminder that a result is more useful when other researchers can reproduce the construction.
Text contains information that prices and filings do not. A news report can describe a military escalation, an export restriction, a new epidemic, or a change in central-bank language before the event has a clean financial representation. The problem is turning that language into a dated, comparable measurement without hiding the research choices or obscuring the researcher's judgment.
NOSIBLE World provides the point-in-time event archive. semantic-factors provides the definition layer. Together they let a quant researcher ask a direct question: what did the world say about this risk on each day in the archive?
A worked example: geopolitical risk
The gallery below shows the public package-rendered outputs for 30 example semantic factors. The geopolitical-risk definition uses 24 relevance anchors and 3 polarity pairs. The full definition is visible in the semantic-factors explorer, where the anchors, polarity sentences, code, research references, CSV, and chart can be inspected together.
The following is a complete runnable example using the full geopolitical-risk definition. It uses the same public World search path as the published example and requires a NOSIBLE API key plus an OpenRouter API key to recompute the series.
import os
from pathlib import Path
from semantic_factors import SemanticFactor
anchors = [
"Military forces attack, invade, or occupy another sovereign state's territory.",
"Fighting expands across new fronts and into populated civilian areas.",
"A ceasefire collapses and sustained combat operations resume.",
"Air strikes or naval attacks hit another country's territory.",
"A government sends troops and weapons across an international border to begin hostilities.",
"Armed forces exchange direct fire across a disputed frontier zone.",
"An organised group bombs or shoots civilians to cause terror.",
"A state-backed group sabotages a cross-border pipeline used by a rival government.",
"An armed proxy attacks shipping, airports, or energy facilities abroad.",
"Armed attackers seize a civilian district and hold it against national security forces.",
"An armed cross-border movement attacks a border post and demands political concessions.",
"Security services report a credible plot against major government buildings.",
"Two governments exchange threats and move toward an open confrontation.",
"A territorial dispute worsens and both claimants reinforce their garrisons.",
"Diplomatic talks break down and each government withdraws its ambassador.",
"Rival warships or aircraft conduct dangerous manoeuvres near each other.",
"A government issues an ultimatum demanding the withdrawal of foreign troops from disputed territory.",
"Rival states expel diplomats and suspend their longstanding treaties.",
"An attempted coup challenges control of an elected national government.",
"Security forces lose control of major cities during mass protests.",
"A disputed election leads to violence between rival armed movements.",
"Armed factions fight each other for control of national territory.",
"A government collapses and no authority can enforce public order.",
"Insurgents seize provinces and the national army withdraws from them.",
]
poles = [
(
[
"Direct interstate combat is occurring between organized armed forces.",
],
[
"No direct interstate combat is occurring between organized armed forces.",
],
),
(
[
"An organized political-violence attack against civilians is active.",
],
[
"No organized political-violence attack against civilians is active.",
],
),
(
[
"Two governments have severed diplomatic relations during an active security dispute.",
],
[
"Two governments maintain diplomatic relations during an active security dispute.",
],
),
]
factor = SemanticFactor(
name="gpr_global",
anchors=anchors,
poles=poles,
filters=None,
floor=0.30,
aggregate="max",
)
output = Path("output")
output.mkdir(parents=True, exist_ok=True)
factor.compute(
weights="netlocs",
residualize=False,
stabilize=True,
polarity_weighted=True,
start_from="2015-01-01",
stop_at="2026-06-26",
api_key=os.environ["NOSIBLE_API_KEY"],
embed_api_key=os.environ["OPENROUTER_API_KEY"],
)
factor.to_csv(
path=str(output / "gpr_global.csv"),
)
factor.plot(
title="Geopolitical risk",
annotate=False,
path=str(output / "gpr_global.png"),
)
The full geopolitical-risk definition has 24 relevance anchors and 3 polarity pairs. Inspect and edit every sentence in the semantic-factors explorer, then replace them with your own research question.
From a factor to a stock beta
Once the factor has been computed, estimate a stock-specific loading by aligning the factor and market data on the same weekly clock:
r_i,t = alpha_i + beta_market,i * r_market,t
+ beta_semantic,i * Delta F_t + epsilon_i,t
Here, r_i,t is the stock return, r_market,t is the SPY return, and Delta F_t is the change in the weekly semantic factor. The beta input uses the same 0.25-0.50, k=6 squash as the public charts, followed by the 30-day log1p geometric mean. The seven-day average shown for presentation is not used in the regression because it adds avoidable lag. The calculation uses EODHD adjusted closes, SPY.US as the market control, and standardizes the weekly factor change over the overlapping regression window. The published GPR example has 586 overlapping weekly observations.
These are real historical OLS outputs from the public example, not placeholder values:
| Stock | Role | GPR semantic beta |
|---|---|---|
| RTX.US | defense | +0.110% |
| LMT.US | defense | +0.199% |
| CAT.US | industrial | +0.027% |
| DAL.US | airline | +0.066% |
These values use one signal definition across every stock. The defense names have positive loadings in this sample, while the industrial and airline estimates are smaller. That is the kind of cross-sectional difference the factor is intended to expose, not a hand-written interpretation of one company.
A positive loading means the stock tended to have a higher return when the standardized weekly factor change was positive, after controlling for the market in this specification. A negative loading means the opposite association. A beta is a conditional historical association, not a claim that the factor caused the return or that the relationship will persist.
The semantic-factors page lets you switch between all 30 factors and inspect their company beta outputs. The numbers belong to a measurement window, model specification, and data source. They are inputs for research, not investment recommendations or promises about future performance.
This is not a one-to-one reproduction of the published GPR methodology. The corpus, event definitions, anchors, threshold, weighting, polarity construction, and aggregation are different. The point is to make a new semantic factor possible, inspectable, and reproducible from the World data.
See the 30 semantic factors
These are package-rendered outputs from the current public snapshot, not mock data. Click any chart to open the full-resolution image. The same definitions, code, and downloadable data are available in the semantic-factors explorer.
Geopolitical risk semantic factor chart
Trade policy uncertainty semantic factor chart
Pandemic and health risk semantic factor chart
Sanctions and export controls semantic factor chart
Recession nowcast semantic factor chart
Oil supply risk semantic factor chart
Supply chain pressure semantic factor chart
Economic policy uncertainty semantic factor chart
Financial stress semantic factor chart
Energy security semantic factor chart
Inflation attention semantic factor chart
Climate policy uncertainty semantic factor chart
Bank regulation uncertainty semantic factor chart
Fiscal uncertainty semantic factor chart
Country geopolitical risk semantic factor chart
Bilateral tension semantic factor chart
Nuclear threat semantic factor chart
Food security semantic factor chart
Commodity supply risk semantic factor chart
Sovereign distress semantic factor chart
Risk on risk off semantic factor chart
Migration policy semantic factor chart
Currency crisis semantic factor chart
Cyber risk semantic factor chart
Terrorism and unrest semantic factor chart
Partisan conflict semantic factor chart
Monetary policy uncertainty semantic factor chart
Equity volatility tracker semantic factor chart
Firm level political risk semantic factor chart
Hawk dove sentiment semantic factor chart
Download the combined 30-factor daily CSV, inspect the full semantic-factor page, or open the package repository on GitHub.
What you can do with it
With semantic-factors, a quant team can:
- monitor a risk vendors do not cover in the language your team uses;
- measure a developing exposure before it appears in a conventional risk report;
- build differentiated signals from a transparent research definition;
- compare the same concept across countries, sectors, or event scopes;
- rerun the measurement when the question changes without waiting for vendors.
The library is useful for research that begins with a question rather than a ticker. What does escalation look like in the news? How does a supply shock appear in the text? Which companies have historically moved with the resulting time series?
Why not just ask an LLM?
An answer from an LLM is not a dated time series. It is difficult to reproduce, difficult to audit, and usually disconnected from a fixed historical corpus.
semantic-factors uses a point-in-time World archive, explicit sentences, a declared threshold, and a deterministic aggregation path. The output can be inspected event by event. The definition can be versioned. The CSV can be downloaded. The chart can be regenerated. A colleague can challenge an anchor or a polarity pair and rerun the measurement.
This makes the system useful for research workflows where the question matters as much as the number. It does not remove judgment. It puts judgment where a researcher can see, test, challenge, and carefully revise it.
Get started
Install the open-source package:
python -m pip install "semantic-factors[plot]"
To recompute a factor, you need access to NOSIBLE World and an embedding provider:
export NOSIBLE_API_KEY=nos_sk_...
export OPENROUTER_API_KEY=sk-or-...
The public page includes a snapshot of the 30 worked examples so you can inspect the definitions and outputs without recomputing them. When you are ready to build your own, start with a concept, write the sentences, and run the package.
Explore the 30 semantic factors
Start a NOSIBLE trial
View the package on GitHub
For fifty years, quantitative researchers have turned observations into factors. Now the observation can begin with the words describing what is happening in the world.

Two Tricks for Turning Sentence Embeddings into Clean Features

An Embedding-Based Approach to Trade and Economic Policy Uncertainty
