Skip to content

Learn

Stock Screening

Natural language stock screening: executable rules

A natural language stock screen becomes useful when the sentence has a reviewable meaning and produces a reproducible candidate set. Start with the person’s words, translate them into a small condition table, confirm the unresolved choices, and run that table against a dated stock universe. The output should include passing stocks, rejected stocks and records that cannot yet be evaluated. A fluent explanation alone cannot show that the requested conditions were executed.

This tutorial uses fictional stocks and a manually confirmed translation. It does not implement a general language parser, connect to a market feed or establish that CoTrading has a screening interface. The same method can support A-shares, Hong Kong stocks or US stocks only after their respective data fields, identifiers and measurement conventions have been checked.

Capture the request before choosing thresholds

Suppose the request is: “Find reasonably priced growing companies, with PE below 20, revenue growth of at least 10%, and good management.” Preserve this exact sentence. “Below 20” supplies a numerical boundary, but “reasonably priced” could refer to several valuation measures. “Growing” needs a period and a metric. “Good management” has no agreed executable definition. The translation must expose those gaps instead of replacing them with whatever fields the current data source happens to offer.

For this example the researcher confirms trailing twelve-month PE and year-over-year annual revenue growth. The universe is three fictional ordinary shares, measured at the same screening date. Growth is stored as a ratio, so 10% becomes 0.10. Management quality remains an unresolved qualitative requirement for later reading. The confirmed numerical screen is therefore a partial execution of the request; it cannot honestly claim to have selected good management.

Original phraseConfirmed fieldOperatorStored thresholdRemaining question
PE below 20pe_ttmstrictly less than20Reject nonpositive PE before extending the universe
Revenue growth at least 10%revenue_growthgreater than or equal0.10Annual comparable period confirmed
Good managementnonenot executednoneSeparate qualitative review

Do not guess whether a loss-making company should satisfy a low-PE request. Our small fixture contains only positive reported PE values. Extending it to negative or unavailable PE requires an explicit eligibility rule and another test. This is a semantic boundary, separate from a missing growth observation.

Build a field contract with dated inputs

Each field needs a definition, unit, observation date, availability date and source locator. For PE, record whether the price is a close or another observation and which earnings denominator it uses. For revenue growth, retain both comparable periods, any currency translation policy and the calculation used by the supplier. A field label cannot establish compatibility by itself.

The SEC’s EDGAR API documentation describes reported XBRL facts with separate units and period contexts. It is a useful illustration of why an extracted number must keep its context; it is not a universal stock screener or an A-share data source. Obtain equivalent disclosure context from the appropriate source for each market. Do not mix a current price with an arbitrarily selected old denominator merely because both are available.

Freeze the universe alongside the input table. Use an exchange-qualified identifier, not an ambiguous short ticker. Save the rule version, data cutoff and raw input reference in the run record. If a provider corrects a value later, a repeat run should be able to identify that input revision instead of silently changing the meaning of the original result.

Execute a small confirmed rule table

The worked fixture contains AAA with PE 18 and growth 0.12, BBB with PE 24 and growth 0.30, and CCC with PE 16 and missing growth. The executable expression is (pe_ttm < 20) AND (revenue_growth >= 0.10). Evaluation uses three states. An observed false condition rejects the record; otherwise a missing condition leaves it unknown; only two observed true conditions pass it.

from decimal import Decimal as D

def evaluate(row, active):
    checks = []
    for field, operator, threshold in active:
        if operator not in {"lt", "ge"}:
            raise ValueError("unsupported_operator")
        value = row.get(field)
        checks.append(None if value is None else
                      (D(value) < threshold if operator == "lt"
                       else D(value) >= threshold))
    return "fail" if False in checks else "unknown" if None in checks else "pass"

The complete standard-library fixture is docs/growth/research/round-5/examples/R517.py in the authoring repository. Run it with python docs/growth/research/round-5/examples/R517.py. Its actual result is AAA pass, BBB fail, CCC unknown, with management quality retained in the unresolved list. Additional assertions check that PE exactly 20 fails and growth exactly 0.10 passes when PE is 19. Those equality cases verify the requested operators rather than only the easy examples.

Download the complete example. Use Python 3.12+ with the standard library only; all inputs are synthetic. After downloading, run python R517.py.

Python’s decimal documentation supports the explicit decimal representation used here. Decimal arithmetic does not choose the right financial definition. This fixture supports only its two declared operators and raises unsupported_operator for any other name. An injected gte spelling is asserted to stop execution, preventing an unknown rule from being treated as an accepted comparison.

Review the output as a research handoff

AAA enters the candidate list because both numerical statements are supported by the provided fixture. BBB remains rejected even though its growth is higher. CCC enters the data completion queue, with revenue_growth named as the blocking field. Do not replace that missing value with zero: zero would assert measured stagnation, while the input says nothing about growth.

The handoff should attach the evaluated conditions to each row. For a small universe, a table with observed values and condition outcomes is enough. A larger run can keep those details in a separate file, but the candidate list must retain a run identifier that reaches them. Record total input count and check that pass, fail and unknown counts sum to it. In this fixture they are one each, accounting for all three inputs.

Ask the reviewer to verify the translation before judging how many stocks appeared. The reviewer should be able to change “at least” to “more than,” rerun the equality fixture and see the intended difference. If the person meant forward PE instead of trailing PE, create a new rule version and acquire the corresponding forecast data. Relabeling the same column would conceal a material change.

Handle unsupported requirements without silent substitution

The different-mechanism counterexample is management quality. It is present in the request but absent from the executable field contract, so no amount of complete PE and growth data resolves it. Place it in a qualitative review queue, ask for a measurable proxy, or narrow the stated deliverable to the numerical subset. A high return on equity is not automatically an accepted replacement for management quality.

Missing growth, by contrast, has an agreed definition and lacks an observation. That can be addressed by retrieving the right disclosure and rerunning the affected row. Keeping the two failures separate prevents a data acquisition task from being confused with a disagreement about the user’s intent.

Before extending the screen, test stale data, unsupported fields, ambiguous identifiers and ineligible earnings values. Preserve unresolved requirements in the final report, even when there are passing candidates. Update the saved rules only after a deliberate definition change; update the inputs when new observations arrive. This separation makes the screen useful as a research starting point while keeping its actual scope visible.

Make the condition review a concrete checkpoint

Export the confirmed condition table before the first full run. Give each condition a short identifier and show the original phrase beside its translation. Reviewers should check the field, operator, threshold, unit, period and missing-value policy independently. A correct threshold applied to the wrong period is still the wrong condition. Save approval of the table with the rule version so later explanations can refer to an agreed specification.

Then read one output row backward: candidate state to condition outcomes, observed values, normalized values and original source. Repeat that trace for a failure and an unknown. If any link is absent, repair the handoff before adding more stocks. The three-row fixture is useful precisely because each state has a different path that can be inspected without statistical inference.

For future edits, distinguish language clarification from new data. Changing “growth” from revenue to profit requires a new condition definition, while supplying CCC’s missing revenue observation only updates that row’s input. Reusing the same rule identifier for both would make saved-screen comparisons difficult to interpret.

FAQ

Does this script understand arbitrary natural language?

No. A person confirmed the field table. The script executes only the resulting declared rules.

Is an unknown stock the same as a rejected stock?

No. Unknown means the agreed conditions cannot all be evaluated. Keep its blocking fields available for completion.