feature_a, generic windows,
model interfaces and promotion checks illustrate engineering patterns. They are not the production MLAlpha
feature set, target definition, thresholds, weights, ranking formula or current research signals.
Machine learning has made it remarkably easy to build a stock-selection demo.
A dataframe, a set of market variables, a model and a few lines of Python can produce a score:
model.fit(X_train, y_train)
scores = model.predict_proba(X_test)[:, 1]
The code runs. The metric improves. The backtest looks convincing.
And there is still a very real possibility that the experiment is wrong.
That gap—between the model produced an answer and the answer deserves to be believed—is where financial machine-learning engineering becomes interesting.
At StockScreen.art Labs, our MLAlpha work has increasingly pushed Python into a role that is broader than model training. It acts as a control plane around the research process: constructing historical observations, enforcing data boundaries, generating features, orchestrating temporal tests, writing immutable artifacts, comparing experimental arms and eventually packaging validated components for production.
The hard problem is not getting a model to fit. It is building a system that makes cheating, drift and accidental success harder to hide.
What is machine learning stock selection?
Machine learning stock selection uses statistical learning algorithms to compare stocks using historical observations and defined model inputs, then produce scores, classifications or rankings that can help narrow a larger investment universe into candidates for further research.
The important distinction is that stock selection is not the same as stock-price prediction. A model does not need to forecast an exact future price to be useful. It may instead estimate relative likelihoods, rank already qualified securities, or identify combinations of conditions that historically separated stronger candidates from weaker ones.
In a serious financial ML workflow, the algorithm is only one component. Historical universe construction, point-in-time data, feature engineering, leakage control, walk-forward validation, reproducibility and research-to-production consistency determine whether the resulting ranking deserves to be trusted.
Python is particularly useful here because the same ecosystem can connect data engineering, numerical computing, machine-learning libraries, testing, experiment tracking and production APIs without requiring the research logic to be repeatedly rewritten in different languages.
The model is only one box
A machine-learning stock-selection pipeline is often described as though the algorithm sits in the middle of everything. In practice, a serious research stack looks more like this:
Market data
↓
Canonical data layer
↓
Point-in-time reconstruction
↓
Historical candidate set
↓
Feature generation
↓
Experimental model
↓
Time-aware validation
↓
Baseline comparison
↓
Diagnostics and audit
↓
Reject / Continue
↓
Production review
The algorithm occupies one box. Most of the engineering lives everywhere else.
Financial ML makes ordinary shortcuts dangerous because:
- time is part of the data model;
- observations and outcome periods can overlap;
- securities enter and leave investable universes;
- market regimes change;
- later corrections can leak into historical data;
- research and production can calculate nominally identical inputs differently;
- cheap experimentation makes accidental discoveries statistically inevitable.
The first job of the research platform is therefore not to make the model smarter. It is to make the experiment more difficult to fool.
Python's first job is keeping time honest
Consider the standard machine-learning pattern:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.20,
random_state=42,
)
For many ML problems, that is perfectly reasonable. For financial observations, a random split can destroy the historical meaning of the experiment. Later market states can end up teaching earlier ones. Serial dependence is broken. Overlapping outcomes can appear on opposite sides of the split.
A financial observation therefore needs an explicit temporal identity.
from dataclasses import dataclass
from datetime import date
@dataclass(frozen=True)
class Observation:
symbol: str
observation_date: date
target_date: date
features: dict[str, float]
outcome: float
The important field is not the ticker. It is the boundary represented by observation_date.
An observation means: this is what the system was allowed to know at this point in time.
Once that rule becomes explicit, it can be tested.
Point-in-time correctness should fail loudly
Data leakage is particularly unpleasant because it rarely crashes the program. The dataframe is valid. The model trains. The output is numerically plausible. The result may even improve dramatically.
Contamination can enter through seemingly harmless paths:
- today's security master being used to reconstruct an older universe;
- revised metadata appearing in historical observations;
- benchmark data being aligned one period too late;
- future outcomes remaining inside a feature dataframe;
- a historical cache containing records that did not exist at the original decision time.
A useful Python research framework converts those assumptions into assertions.
def assert_point_in_time(frame, observation_date):
latest_source_date = frame["date"].max()
if latest_source_date > observation_date:
raise ValueError(
"Future data detected in historical observation"
)
Feature matrices can be guarded as well:
def assert_no_forbidden_columns(columns, forbidden_columns):
leaked = set(columns).intersection(forbidden_columns)
if leaked:
raise RuntimeError(
f"Forbidden data detected: {sorted(leaked)}"
)
The historical universe is part of the experiment
One of the most overlooked model inputs is not a feature at all. It is the list of securities the model was allowed to consider.
Testing several years of market history using only securities that survive today quietly tells the experiment which companies made it through the period. That is future information.
A historical candidate set therefore needs to be reconstructed:
Historical date
↓
Securities available then
↓
Eligibility rules
↓
Data-quality requirements
↓
Candidate cohort
In a layered system, ML may then solve a narrower problem: compare candidates that have already satisfied a deterministic qualification process. That is the distinction between AlphaEngine™ quantitative stock rankings and the narrower machine-learning research layer being studied around MLAlpha.
That is very different from asking a model to roam through every listed instrument and discover an investment strategy from scratch. For a broader discussion of what an AI stock picker can and cannot reasonably do, see our AI stock-picking research.
A narrower model mandate is often easier to validate, easier to explain and harder to accidentally overstate.
The baseline matters more than the headline metric
Suppose an experimental model produces attractive returns. That statement is incomplete until we answer: compared with what?
A useful research design can compare an experimental ranking against multiple reference processes:
- a broad market reference;
- the underlying eligible cohort;
- an existing deterministic ranking;
- alternative experimental methods.
The exact baselines are implementation-specific. The engineering principle is not.
A machine-learning layer should prove that it adds something over the process that already works.
This is why a number such as accuracy = 0.64 can be nearly meaningless by itself. Stock selection is
frequently a ranking problem rather than a pure classification problem.
scores = model.predict_proba(X_test)[:, 1]
results["score"] = scores
ranked = results.sort_values(
"score",
ascending=False,
)
The relevant evaluation is then performed on the part of the ranking that the system would actually use—not on a disconnected headline statistic.
Feature engineering needs an interface contract
Research/production skew is one of the easiest ways to deploy a model that is not actually the model you tested.
Imagine that research normalizes a volatility-derived input one way while production normalizes the same named field another way. Both produce valid floating-point values. Nothing crashes. The model simply receives a different distribution from the one that was validated.
The safer pattern is to treat model inputs as an interface contract.
FEATURE_CONTRACT = (
"feature_a",
"feature_b",
"feature_c",
"feature_d",
)
The generic names above are deliberate. What matters here is that the identity and ordering are explicit.
import numpy as np
def build_model_matrix(frame):
missing = [
name
for name in FEATURE_CONTRACT
if name not in frame.columns
]
if missing:
raise RuntimeError(
f"Missing model features: {missing}"
)
X = frame.loc[:, FEATURE_CONTRACT].to_numpy()
if not np.isfinite(X).all():
raise RuntimeError(
"Feature matrix contains invalid values"
)
return X
The model no longer depends on whatever column order a dataframe happened to contain that day.
Hash the contract
The same contract can become part of model provenance.
import hashlib
import json
def feature_contract_hash(features):
payload = json.dumps(
list(features),
separators=(",", ":"),
).encode("utf-8")
return hashlib.sha256(payload).hexdigest()
Persist the hash with model metadata:
{
"model_version": "example-model",
"model_family": "example-family",
"feature_contract_sha256": "...",
"training_cutoff": "..."
}
Then make production verify it:
if runtime_hash != metadata["feature_contract_sha256"]:
raise RuntimeError(
"Feature contract mismatch"
)
Inference should refuse to proceed when its contract does not match the model package. A wrong prediction is bad. A prediction generated from an unknown input contract is worse because it may look completely normal.
pandas is a tool, not a data architecture
Python financial research often begins in pandas for a good reason. Grouped returns, rolling transformations and joins are expressive and fast to prototype.
g = prices.groupby(
"symbol",
group_keys=False,
)
prices["return_short"] = (
g["close"].pct_change(SHORT_WINDOW)
)
prices["rolling_metric"] = (
g["close"].transform(
lambda s: s.rolling(WINDOW).mean()
)
)
That is excellent research code until repeated groupings, merges, copies and sorts turn a manageable logical dataset into a very large working set.
At that point, the architecture may need to evolve:
- filter in SQL before materializing a dataframe;
- persist reusable columnar datasets;
- cache stable feature matrices;
- move dense numerical work into NumPy arrays;
- batch by observation period or symbol;
- parallelize coarse experimental units instead of every transformation.
Time-aware validation should resemble deployment
If a model is intended to live through time, its validation should do the same.
Fold A
[ training history ][ unseen period ]
Fold B
[ longer training history ][ unseen period ]
Fold C
[ longer training history ][ unseen period ]
Python makes the orchestration straightforward:
for fold in folds:
train = build_training_set(fold)
test = build_test_set(fold)
train = purge_overlap(
train,
test,
)
model = build_model()
model.fit(
train[FEATURE_COLUMNS],
train[TARGET_COLUMN],
)
test["score"] = score_model(
model,
test,
)
evaluate_fold(test)
The loop is the easy part. The difficult engineering lives inside functions such as
build_training_set(), purge_overlap() and evaluate_fold().
Why overlapping outcomes matter
A training observation can be dated before the test period while its outcome window still extends into that period. The test features were not directly used, but the training label contains information from the supposedly unseen future.
A temporal validation framework therefore needs a purge between training outcomes and the next evaluation window. The exact interval is specific to the experiment and is intentionally not discussed here.
Training outcomes cannot contain information that belongs to the evaluation period.
Model complexity has to earn its way into the system
Python makes it easy to compare different model families. That convenience can create an unhealthy assumption: more sophisticated must mean better.
Tree-based models can be useful for nonlinear relationships in structured tabular data. Sequence-oriented models can test whether the path of information contains incremental structure beyond the current state.
A generic sequence input might be represented as:
# samples × time × features
X.shape
But the research question should never be Which architecture sounds more advanced?
It should be:
Does the additional complexity create stable out-of-sample improvement that justifies its cost and operational risk?
If not, complexity has added more parameters, compute, tuning and failure modes without adding useful information.
In that case, the simpler model wins.
Reproducibility becomes infrastructure
Once a research program produces more than a handful of experiments, every run needs an identity.
experiment-run/
├── manifest.json
├── cohort-summary.csv
├── coverage-summary.csv
├── fold-results.csv
├── predictions.csv
├── diagnostics.json
└── model-artifacts/
A manifest can record the execution contract:
manifest = {
"experiment_id": run_id,
"code_revision": revision,
"model_family": model_family,
"feature_contract": feature_hash,
"training_cutoff": str(training_cutoff),
"random_seed": random_seed,
}
Important inputs can be hashed as well:
def sha256_file(path):
digest = hashlib.sha256()
with open(path, "rb") as fh:
while chunk := fh.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
Six weeks later, I reran it and got something different should have a better response than interesting.
We want to be able to ask:
- same code revision?
- same input cohort?
- same feature contract?
- same data snapshot?
- same parameters?
- same dependency environment?
Reproducibility is not paperwork around the experiment. It is part of the result.
Failed experiments should survive too
Research memory becomes dangerously optimistic when only successful experiments remain visible.
If dozens of hypotheses are tested and one produces a spectacular result, retaining only the winner changes the historical story from we searched many possibilities to we tested an idea and it worked.
Those are statistically different statements.
Negative outcomes deserve first-class artifacts:
{
"verdict": "NOT_PROMOTED",
"reason": "OUT_OF_SAMPLE_EFFECT_NOT_STABLE"
}
or:
{
"verdict": "INCOMPLETE",
"reason": "INSUFFICIENT_RESEARCH_COVERAGE"
}
or:
{
"verdict": "REJECTED",
"reason": "NO_INCREMENTAL_VALUE"
}
There is significant technical value in code that says no.
Promotion can be executable policy
The person who designed an experimental feature is rarely the most neutral observer of a promising chart.
Instead of relying entirely on visual judgment, a research framework can encode categories of requirements that must be satisfied before an experiment becomes a production candidate.
promotion_pass = all([
sufficient_coverage,
sufficient_validation,
meaningful_incremental_effect,
acceptable_risk_behavior,
no_integrity_failures,
])
The actual thresholds and production rules are research IP and are not shown. The useful engineering pattern is that the gates exist and are evaluated consistently.
verdict = (
"PROMOTION_CANDIDATE"
if promotion_pass
else "RESEARCH_ONLY"
)
Human judgment remains necessary. What changes is that enthusiasm and evidence are forced into separate boxes.
Alternative data should first prove it deserves to exist
New datasets are exciting because they create the possibility of information not represented in conventional price-derived features. They also create new ways to build a biased historical sample.
Before asking whether an alternative source improves a model, a research system should ask:
- Is historical coverage sufficient?
- Are timestamps trustworthy?
- Does coverage change systematically through time?
- Are particular sectors or market conditions overrepresented?
- Are missing observations random?
- Can the historical dataset reproduce what would actually have been available then?
Only after those questions are answered does predictive testing become interesting.
if not coverage_is_adequate(dataset):
return ResearchVerdict(
status="INCOMPLETE",
reason="INSUFFICIENT_DATA_COVERAGE",
)
At scale, ML research becomes systems engineering
Python makes parallel experimentation easy enough that it is also easy to overdo it.
A Python process may create workers while NumPy, BLAS, gradient-boosting libraries or neural-network frameworks create native threads of their own. Several innocent-looking worker settings can produce far more runnable threads than the CPU can execute efficiently.
Python workers
×
native library threads
=
possible oversubscription
Thread limits therefore become part of experiment configuration:
export OMP_NUM_THREADS="$LIMIT"
export MKL_NUM_THREADS="$LIMIT"
The right values depend on the hardware and workload. The important point is that CPU topology, memory pressure, storage throughput and scheduler behaviour eventually matter to ML research.
Production should win resource contention
When live services and long-running experiments share infrastructure, they should not have equal priority.
Production services
higher scheduling priority
protected memory headroom
restart policy
Research workloads
lower priority
bounded resources
interruptible execution
Linux systemd and cgroups provide mature primitives for this. The precise resource values depend on the host and are operational details, not model logic.
An experiment should not be able to take the public service offline simply because it discovered every available CPU core.
Python shortens the distance from research to production
A Python-centric stack can reduce one of the classic causes of ML failure: rewriting validated feature logic in a different implementation before deployment.
A simplified inference boundary might look like:
from fastapi import FastAPI
app = FastAPI()
model = load_model()
contract = load_feature_contract()
@app.post("/rank")
def rank_candidates(rows: list[dict]):
frame = build_feature_frame(
rows,
contract,
)
validate_feature_frame(frame)
scores = score_candidates(
model,
frame,
)
return build_response(
frame,
scores,
)
A production service obviously requires more:
- authentication;
- request/schema validation;
- model-version enforcement;
- structured logging;
- health checks;
- timeouts and error isolation;
- deployment and rollback controls.
But the architectural property is valuable:
Historically validated implementation
↓
Packaged component
↓
Production service
Every avoided rewrite is one fewer opportunity for research and production to disagree silently.
Tests should validate assumptions, not merely functions
A conventional unit test may verify a calculation:
assert calculate_metric(series) == expected
Financial ML needs another class of test: tests of experimental integrity.
Temporal boundary
assert (
observation.latest_source_date
<= observation.observation_date
)
Fold isolation
assert (
training.latest_outcome_date
< test.earliest_observation_date
)
Feature contract
assert (
tuple(model_frame.columns)
== expected_feature_contract
)
Cohort reproducibility
assert cohort_hash == expected_hash
These tests ask something more valuable than does the function execute correctly?
Is this actually the experiment we intended to run?
Python's greatest strength can become a statistical liability
Python makes experimentation cheap. Change a feature. Rerun. Change the label construction. Rerun. Try another model family. Rerun. Add another data treatment. Rerun.
That productivity is enormously useful. It also expands the number of opportunities to find something that works by chance.
The faster the research engine becomes, the more important experiment histories, untouched evaluation periods, negative-result retention and predefined decision rules become.
A sophisticated research platform should therefore make it easy to run experiments while making it difficult to forget how many experiments were run before the attractive one appeared.
The MLAlpha lesson so far
MLAlpha has gone through repeated cycles of modelling, reconstruction, validation and redesign.
Some ideas have produced enough evidence to justify further investigation. Others have failed. Some initially promising results exposed weaknesses in the experiment itself and required the test to be rebuilt before any conclusion could be trusted.
We consider that a feature of the development process, not a failure of it.
The objective is not to build the most complicated stock-selection model possible. It is to build an environment where increasingly sophisticated hypotheses face increasingly sophisticated attempts to disprove them.
Python happens to be particularly well suited to that environment because the same ecosystem can participate in data preparation, numerical computing, modelling, validation, test automation, artifact generation and production APIs.
What Labs publishes—and what it does not
There is already plenty of financial content online telling readers which stock an algorithm supposedly likes. StockScreen.art Labs has a different role.
Labs is intended to expose enough of the engineering discipline behind StockScreen.art to make the methodology inspectable by developers, data scientists, quantitative researchers and technically curious investors.
That includes discussion of:
- temporal integrity;
- historical reconstruction;
- research architecture;
- reproducibility;
- model governance;
- research/production consistency;
- infrastructure;
- statistical failure modes;
- deployment engineering;
- experiments that do not work.
What we do not publish are the production ingredients that materially reproduce the model:
- exact feature inventories;
- feature weights or contribution formulas;
- decision and promotion thresholds;
- training/label horizons;
- proprietary candidate-eligibility rules;
- current experimental signals;
- production hyperparameters;
- ranking formulas.
The methodology is open for technical discussion. The implementation remains ours.
That boundary is intentional. Technical credibility should come from demonstrating that the difficult engineering problems are understood and controlled—not from publishing the recipe that makes a proprietary research system distinct.
For readers who want to go deeper, Labs will continue to document the engineering and experiments behind the platform. StockScreen.art Learning covers the market mechanics underneath those systems, while MarketEngine™ AI stock analysis shows the individual-ticker research layer and AlphaEngine™ quantitative stock rankings show the broader opportunity-discovery layer.
A model is only one component. Much of the credibility lives in the machinery designed to stop us from believing it too quickly.