Skip to content

Learn

Stock Screening

Build a stock screen with AND, OR and grouped rules

A multi-condition stock screen needs a precise answer to “which requirements apply together?” Two alternative ways to qualify may share the same exclusion. Put that relationship into a logic tree before writing an expression, then test stocks that exercise each route. Parentheses can determine whether an excluded stock slips into the candidate set even though every individual comparison was calculated correctly.

This guide executes a declared Boolean tree using synthetic stock records. It does not interpret arbitrary natural language or demonstrate a visual rule editor in CoTrading. The product workflow remains planned or unverified. The practical deliverable is a grouped rule, an executable evaluation function, a truth table and a list showing which stock changes under the wrong grouping.

Define the two routes and their shared gate

Suppose a researcher accepts a value-and-growth route A or a profitability-and-debt route B. One possible confirmed definition is A: positive PE below 15 and annual revenue growth at least 5%; B: return on equity at least 15% and the specified debt ratio at most 40%. These numerical definitions need their own dated source fields before a real stock screen can run.

For the logic fixture, those field comparisons have already produced route flags. This isolates grouping from financial-data acquisition. route_a means all of A’s confirmed comparisons passed; route_b means all of B’s comparisons passed. A shared suspended flag excludes the stock regardless of the route. Here it is an explicit synthetic eligibility flag, not a promise that a provider supplies every market’s suspension information.

Write the intended expression as (route_a OR route_b) AND NOT suspended. The outer AND owns the exclusion. The OR owns the alternatives. A diagram or nested record should show that structure instead of relying on indentation in a natural language request. Each internal node is an operator; each leaf is a named, already evaluated condition.

Translate the tree without changing its ownership

The root node is AND with two children: an OR node containing A and B, and a NOT node containing suspended. Keeping the tree in a data structure makes it possible to inspect the same grouping used for execution. A displayed tree and a separately handwritten expression can drift apart if they are maintained independently.

tree = {"op": "and", "children": [
    {"op": "or", "children": [{"leaf": "route_a"}, {"leaf": "route_b"}]},
    {"op": "not", "children": [{"leaf": "suspended"}]}]}

Use a small operator allowlist and validate node arity. NOT needs exactly one child; AND and OR need at least one. Unknown operators must fail explicitly. Do not execute user-provided text through eval merely to avoid writing a narrow interpreter. The fixture supports this declared tree format, not arbitrary Python programs or a complete query language.

The Python expression reference documents Boolean operations and precedence. In Python, AND binds more tightly than OR. Explicit parentheses remain valuable even when a programmer knows that rule, because the research specification should be readable without reconstructing language precedence from memory.

Construct examples that distinguish the branches

A useful truth table contains more than one passing company. Include A only, B only, both routes, neither route and a shared exclusion that would otherwise qualify. Our five synthetic records deliberately fill those positions.

StockRoute ARoute BSuspendedIntended result
AAAtruefalsefalsepass
BBBfalsetruefalsepass
CCCtruetruefalsepass
DDDtruefalsetruefail
EEEfalsefalsefalsefail

DDD is the critical distinguishing observation. If the table contained only nonsuspended stocks, the incorrect grouping could pass all visible examples. Deliberately choosing a row that challenges the shared gate is more informative than adding many similar passing rows.

Keep the manually expected result separate from the evaluator’s output. A test that computes its expected answer using the same malformed tree can reproduce the same mistake twice. Here the independent expectation is the written expression with explicit grouping and the five manually specified dispositions.

Execute and inspect the actual result

The interpreter recursively evaluates each child, then applies the declared operator. It requires actual Boolean leaf values, rejecting None or numeric values instead of using their implicit truthiness. This fixture intentionally does not implement three-state missing-data logic; unresolved financial fields must be addressed before creating these route flags.

Download the complete example. Use Python 3.12+, the standard library and synthetic inputs only. Run python R519.py after downloading. The authoring source is docs/growth/research/round-5/examples/R519.py.

Actual output passes AAA, BBB and CCC, rejects DDD and EEE, and identifies DDD as the stock changed by wrong grouping. The script additionally enumerates all eight combinations of A, B and suspended and compares the tree with (a or b) and not suspended. An injected xor operator must raise invalid_node. These assertions check grouping, complete Boolean coverage and operator rejection separately.

Notice that CCC appears once, although it passes both routes. Candidate membership is a set-like result, not one opportunity per passing path. Preserve the route details for explanation without duplicating the stock in the research queue or inflating the candidate count.

Reproduce the bypass counterexample

The wrong expression is route_a OR (route_b AND NOT suspended). Its exclusion belongs only to B. DDD passes A, so it enters without satisfying the shared gate. The numerical definitions of A and B did not change, yet the resulting stock set did. This is an ownership error in the logic, not a data-quality or threshold problem.

The script calculates the wrong expression as a diagnostic experiment and compares its membership with the intended result. It does not silently substitute that version. Store the changed-member list beside the rule review so the researcher can see the practical consequence of parentheses rather than merely receiving a warning about syntax.

If several exclusions apply to every route, place their conjunction at the shared level. If an exclusion genuinely belongs only to one route, attach it there and create a stock that distinguishes those meanings. Grouping should reflect the confirmed research policy; visual symmetry is not a reason to move a condition.

Keep the specification executable as it evolves

Save the tree version, leaf definitions, data cutoff and truth-table revision together. When A gains another condition, rerun A-only and both-route examples. When a common exclusion changes, rerun the suspended examples across both routes. Add a missing-leaf case if the input contract changes, and preserve the explicit stop until a three-state policy is designed.

For a real dataset, attach each leaf to its original field, unit, period and source. The grouping test cannot validate the financial correctness of those upstream comparisons. First confirm field calculations, then run the tree, then reconcile each original stock to exactly one final state.

The handoff should include the logic tree, human-readable expression, truth table, final unique candidate list and any rejected configuration. Another researcher should be able to reconstruct DDD’s rejection without interpreting a generated narrative. That is the useful acceptance standard: every alternative works, every shared exclusion applies where intended, and a syntax change has a visible membership consequence before the full screen is trusted.

Review changes by their affected path

Use a change table with the old tree node, new node, intended policy change and expected affected examples. If a researcher moves the suspension gate inside B, the table should explicitly say that A can now bypass it. An accidental drag in a future visual editor should not become an accepted policy change merely because the tree remains syntactically valid.

For a real input row, preserve both the route-level result and the underlying field comparisons. A route flag alone is sufficient for this grouping test but insufficient for explaining how the original financial conditions were measured. The two layers should have different checks and linked versions, so a field correction can reopen the relevant route without inventing a different grouping.

Before delivering a revised candidate list, compare the old and new member sets and attach the rule change responsible for each difference. If no input or approved rule changed, a different membership is an execution defect to investigate. This keeps the truth table useful after the first implementation instead of treating it as a one-time design illustration.

FAQ

Does passing two routes create two candidates?

No. Keep one stock with two supporting paths, then deduplicate the research list by the confirmed security identifier.

Can a missing field simply become false here?

No. This fixture requires Boolean leaves. Design and test missing-data semantics separately before supplying route flags.