Short answer: Machine learning trading bot development adds a training pipeline, a model file and a second copy of every feature to an ordinary bot, and much of what breaks lives in those additions. On a synthetic random walk, a look-ahead feature with a shuffled split scored 70.8% accuracy, while past-only features under TimeSeriesSplit scored 48.5% (seed 7, run 5 November 2026). A high score proves nothing until leakage and feature parity are tested, and never forecasts returns.
This guide uses no real asset data, and each script exists to show a failure, so no feature, model or hyperparameter here is a setting to use. Scripts ran on 5 and 6 November 2026 on Python 3.11.3, scikit-learn 1.8.0, numpy 2.0.0 and pandas 2.2.3.
Fincode works from rules the customer wrote and supplies no models or strategies, so this page does not sell ML trading, and nothing here suggests a model can predict markets. Automated trading can lose money. For readers in Turkey: Article 99/A of Capital Markets Law No. 6362 treats activity by foreign crypto-asset platforms directed at Turkish residents as unauthorized.
What changes when a model sits inside a trading bot
The order, risk and exchange layers described in trading bot development stay the same; you add artifacts that can fail with no code change.
| Item | Rule-based | With a model |
|---|---|---|
| Decision logic | Code you can read | A file produced by training |
| Proof it is correct | Expected output for a given input | A score on unseen data, easy to inflate |
| Inputs | Market data | A feature pipeline that exists twice, in research and live |
| Typical failure | An error or a wrong rule | A confident output from bad input |
Machine learning trading bot in Python: a leakage test on random data
This script produces strong validation accuracy on returns that are random by construction.
The label is the sign of the sum of the next 10 returns. Three features use past returns only; a fourth is a centered average that reaches 5 bars ahead.
import numpy as np, pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import KFold, TimeSeriesSplit, cross_val_score
SEED, N, H = 7, 4000, 10 # H: bars ahead in the label
rng = np.random.default_rng(SEED)
ret = pd.Series(rng.normal(0, 0.01, N)) # synthetic random walk
fwd = ret.rolling(H).sum().shift(-H) # sum of the next H returns
y = (fwd > 0).astype(int)
past = pd.concat({f"mean_{w}": ret.rolling(w).mean() for w in (5, 20, 60)}, axis=1)
leak = past.assign(centered=ret.rolling(11, center=True).mean())
keep = leak.notna().all(axis=1) & fwd.notna()
shuffled = KFold(5, shuffle=True, random_state=SEED)
tests = [("look-ahead feature, shuffled KFold", leak, shuffled),
("look-ahead feature, TimeSeriesSplit", leak, TimeSeriesSplit(5)),
("past-only features, shuffled KFold", past, shuffled),
("past-only features, TimeSeriesSplit", past, TimeSeriesSplit(5)),
("past-only features, TimeSeriesSplit gap=10", past, TimeSeriesSplit(5, gap=H))]
print("seed", SEED, "rows", keep.sum(), "share of class 1", round(y[keep].mean(), 3))
for name, X, cv in tests:
score = cross_val_score(RandomForestClassifier(random_state=SEED), X[keep], y[keep], cv=cv)
print(f"{name:43s}{score.mean():.3f}")Output:
seed 7 rows 3931 share of class 1 0.485
look-ahead feature, shuffled KFold 0.708
look-ahead feature, TimeSeriesSplit 0.664
past-only features, shuffled KFold 0.550
past-only features, TimeSeriesSplit 0.485
past-only features, TimeSeriesSplit gap=10 0.494- 0.708: the look-ahead feature sees half of the label's window.
- 0.664: walk-forward validation does not cure a leaky feature.
TimeSeriesSplitonly orders the rows. - 0.550: no look-ahead feature here. Labels overlap, so shuffled neighbors share most of their future, and the split itself leaks.
- 0.485 and 0.494: back to chance.
gapdrops rows just before each test fold; its default is 0 and it knows nothing about your label, so set it to at least the label's horizon.
Over seeds 0 to 19, the last row averaged 0.502 and ranged from 0.475 to 0.522, while the past-only shuffled row never fell below 0.536. So one run at 52% is ordinary noise. Run your own pipeline on synthetic returns: a score clearly above chance means it leaks. scikit-learn defines leakage as information unavailable at prediction time; general causes are in backtesting pitfalls, and engines in backtesting Python libraries.
Feature parity between research and live code
The same feature name can hold different numbers in the notebook and the bot, so compare both code paths on the same bars.
from collections import deque
import numpy as np, pandas as pd
ret = pd.Series(np.random.default_rng(7).normal(0, 0.01, 500)) # synthetic bars
research = ret.rolling(20).std() # pandas default: ddof=1
for ddof in (0, 1): # numpy default: ddof=0
window, live = deque(maxlen=20), []
for x in ret: # live code: one bar at a time
window.append(x)
live.append(np.std(window, ddof=ddof) if len(window) == 20 else np.nan)
assert (research.isna() == pd.isna(live)).all(), "NaN on different bars"
gap = (research - pd.Series(live)).abs().max()
print(f"live ddof={ddof}: largest gap {gap:.8f}")Output:
live ddof=0: largest gap 0.00032525
live ddof=1: largest gap 0.00000000The defaults differ, as the pandas reference notes, so two correct-looking lines differ by a steady 2.5% and no error ever appears. A second case: ewm(span=10) with pandas' default adjust=True against a recursive live average differed by 0.00154 at bar 5 and by under 1e-17 at bar 400, and adjust=False removed the gap, so compare the first bars after a restart as well as the last. Run this in continuous integration, or better, import one feature module in both paths.
Model versioning, retraining and rollback
Treat a trained model as a release: versioned, reproducible, tied to its data and replaceable by the previous one.
| Item | What to require |
|---|---|
| Environment | Load in the same package versions. scikit-learn says pickle, joblib, cloudpickle and skops do not support loading a model trained with a different version |
| File safety | Pickle and joblib can execute code when loaded, so load only trusted, verified files |
| Metadata | Data snapshot, source code, library versions and cross-validation score (scikit-learn's list), plus the label definition and gap |
| Retraining | A retrain is a deploy: same tests, then a dry run |
| Rollback | Keep the previous model with its features. FreqAI's purge_old_models removes older models, so set it with rollback in mind |
| Expiry | Refuse new entries from an old model, as FreqAI's expiration_hours does |
Check the data license before training. The Binance Vision Dataset Terms (updated 26 August 2026, read 5 November 2026) grant only a non-commercial license (section 3), extend to models built from the files (2.4) and bar live proprietary trading and commercial trading bot platforms (4.2, 4.4). Read any source's terms with a lawyer before a commercial build.
Drift monitoring and fallback when inference fails
Decide what the bot does when the model is down, stale or fed bad input, because some failures raise no error.
import numpy as np, sklearn
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
rng = np.random.default_rng(7)
X, y = rng.normal(size=(2000, 3)), rng.integers(0, 2, 2000) # synthetic, no NaN in training
nan_row = np.array([[np.nan, 0.1, 0.2]])
for m in (LogisticRegression(), RandomForestClassifier(random_state=7)):
m.fit(X, y)
try:
print(type(m).__name__, "default:", m.predict(nan_row))
except ValueError:
print(type(m).__name__, "default: ValueError")
with sklearn.config_context(assume_finite=True):
print(type(m).__name__, "assume_finite:", m.predict(nan_row))Output:
LogisticRegression default: ValueError
LogisticRegression assume_finite: [0]
RandomForestClassifier default: [1]
RandomForestClassifier assume_finite: [1]A random forest trained on clean data returned a class for a NaN row with no error. scikit-learn's tree page documents the rule for single trees: such values go to the child with the most samples. Logistic regression raises by default, but under assume_finite=True, which the performance page offers to cut validation time, it also returned a class. Check np.isfinite(row).all() yourself before predicting.
| Event | Bot behavior |
|---|---|
| NaN feature or stale bar | No new entries, alert |
| Inference error or timeout | No new entries; open positions follow deterministic exit rules |
| Live features or outputs drift from the training window | Alert and human review, no automatic retrain |
Drift shows the data changed, not that the model is wrong, so log feature and output distributions rather than judging by recent results. Limits outside the model are in trading bot risk management. Measure latency too: on one busy Windows machine (three runs, 5 November 2026), a single-row predict_proba took a median of about 0.1 ms for logistic regression, 9 to 12 ms for a default forest and 32 to 35 ms with n_jobs=-1; not a benchmark, but threads slowed a single row.
Reinforcement learning trading bot: why results often reflect the simulator
An RL agent optimizes the simulator's reward, so any free fill or missing cost in the environment becomes something to exploit.
- Seeds. Stable-Baselines3's guide says results vary when only the seed changes and asks for several runs and a separate test environment. Henderson et al. found that two groups of five runs of TRPO on HalfCheetah, differing only in seed, gave statistically different distributions.
- Sample size. The same guide says these algorithms can need millions of interactions, and that most RL successes came in games or simulation.
- Frictions. The 2020 FinRL paper treats transaction cost and bid-ask spread as parameters, and its text does not discuss market impact or slippage. What you enter is the market the agent learns.
- Reward. FreqAI's RL page says its training environment is much simpler than a backtest, that its default reward is not for production and that poorly trained agents may find "cheats" that raise reward without winning trades.
Worked example. One million steps is 3,968 years of daily bars at 252 a year, or 1.9 years of one-minute bars in a 24-hour market, so an agent on daily data replays the same history many times.
A null test in Python catches the worst of this: on pure noise, every policy must earn zero less costs.
import numpy as np
r = np.random.default_rng(7).normal(0, 1, 200_000) # synthetic returns, no signal exists
pos = np.sign(r) # any policy works; this one reacts to r
same_bar = pos * r # bug: paid the return it just saw
next_bar = pos[:-1] * r[1:] # correct: position now, paid next bar
turnover = np.abs(np.diff(pos[:-1], prepend=0))
with_cost = next_bar - 0.05 * turnover # hypothetical cost per unit traded
print(f"same-bar fill {same_bar.mean():+.3f}")
print(f"next-bar fill {next_bar.mean():+.3f}")
print(f"next-bar fill with cost {with_cost.mean():+.3f}")Output, in units of one standard deviation per bar:
same-bar fill +0.797
next-bar fill -0.001
next-bar fill with cost -0.051The first line is a one-line environment bug paying a policy on noise. Stable-Baselines3's check_env checks the interface, not this. These figures describe a simulator on synthetic data and say nothing about any market.
Machine learning trading bot frameworks: what each one is
Each tool leaves the problems above to you.
| Tool | License | What it is | Watch for |
|---|---|---|---|
| FinRL | MIT (opens in a new tab) | Deep RL library with market environments and backtesting; its README says education, benchmarking and research prototyping | Costs and spread are yours to set |
| FreqAI (Freqtrade) | GPL-3.0 (opens in a new tab) | Trains and retrains models beside a bot | Example strategy is not for production; in backtests, features are built once over the training range and must not look ahead; no dynamic volume pairlists; GPL terms apply if you distribute it |
| scikit-learn | BSD-3-Clause (opens in a new tab) | General ML library with splitters and pipelines | It knows nothing about bars or label horizons |
| Stable-Baselines3 | MIT (opens in a new tab) | RL algorithm implementations | You supply the environment and its economics |
What to ask when reviewing a machine learning trading bot
Ask for evidence of behavior, since no review can say what a model will earn.
- Does a synthetic random walk score near chance through the full pipeline?
- Is
gapat least the label horizon, and can the label be computed only from data after the decision? - Do research and live features match bar for bar in continuous integration?
- Which data snapshot and commit built the running model, and what restores the previous one?
- What happens on NaN, on a timeout and on an expired model?
- What does the training data's license allow?
Fincode's trading bot code review can put these to an existing codebase; it checks software behavior and offers no view on a model's results.
Frequently asked questions
Can machine learning predict markets for a trading bot?
This guide makes no such claim. Its test uses random data to show how validation mistakes manufacture accuracy.
Why is my ML trading backtest so accurate?
Suspect leakage first. Feed the same pipeline a synthetic random walk: a score clearly above chance means something leaks.
Is reinforcement learning better than supervised learning for a trading bot?
This guide ranks neither. RL adds the simulator as one more source of false results.
Is there AI trading bot Python code I can copy from here?
This page gives test code, not a bot. Run these tests on any code you copy.
Sources
- scikit-learn: TimeSeriesSplit (documentation 1.9.1) (opens in a new tab)
- scikit-learn: Common pitfalls and recommended practices (data leakage) (opens in a new tab)
- scikit-learn: Model persistence (security, version compatibility, metadata) (opens in a new tab)
- scikit-learn: Computational performance (prediction latency, assume_finite) (opens in a new tab)
- scikit-learn: Decision trees (missing values support) (opens in a new tab)
- pandas: Rolling.std reference (ddof default and numpy note) (opens in a new tab)
- Stable-Baselines3: Reinforcement Learning Tips and Tricks (opens in a new tab)
- Henderson et al.: Deep Reinforcement Learning that Matters (arXiv 1709.06560) (opens in a new tab)
- Liu et al.: FinRL: A Deep Reinforcement Learning Library for Automated Stock Trading in Quantitative Finance (arXiv 2011.09607) (opens in a new tab)
- AI4Finance Foundation: FinRL repository README (scope statement) (opens in a new tab)
- Freqtrade: FreqAI introduction (opens in a new tab)
- Freqtrade: Running FreqAI (retraining, expiration_hours, purge_old_models) (opens in a new tab)
- Freqtrade: FreqAI reinforcement learning (opens in a new tab)
- Binance Vision Dataset Terms, last updated 26 August 2026 (opens in a new tab)
- Capital Markets Law No. 6362, consolidated text, Article 99/A (Turkish) (opens in a new tab)
This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the editorial policy.