A live TopstepX bot that trades mechanical entries graded by AI, with a
reinforcement-learned trailing exit — for 3-min futures (NQ, ES, RTY,
YM, GC).
⚠️ Educational — live mode places LIVE orders. Run it on a practice/evaluation account first. (Backtests place no orders, but still need credentials — contract specs are fetched from the broker API.)
- Python 3.10+ and git (dependencies install the public
futures_foundationlibrary from GitHub). - Internet — downloads the ~45 MB
amazon/chronos-bolt-tinycheckpoint on first run, and the bot reads contract specs from the broker API at startup. - A TopstepX account + API key — required for both live and backtest.
Tick size / tick value come from the broker API (
/Contract/search); there is no offline mode. Backtests still use local CSV bars — only the contract specs come from the API.
git clone https://github.com/johnamcruz/algoTraderBot.git
cd algoTraderBot
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txtmacOS note: if you hit a segfault (the torch/xgboost OpenMP clash), prefix commands with
KMP_DUPLICATE_LIB_OK=TRUE, e.g.KMP_DUPLICATE_LIB_OK=TRUE python bot.py ….
Credentials live in a gitignored .env file. Copy the template:
cp .env.example .envOpen .env and fill in your TopstepX details:
TOPSTEPX_USERNAME=your_login # your TopstepX login
TOPSTEPX_API_KEY=your_api_key # API KEY from the dashboard — NOT your password
TOPSTEPX_ACCOUNT= # blank = first tradable account, or an id/name- Get the API key from your TopstepX dashboard (Settings → API). It is a key, not your account password.
- Leave
TOPSTEPX_ACCOUNTblank to use your first tradable account — the bot prints every account on startup, so you can copy an id or name here to pin a specific one (do this to be sure it's your practice account). - Real environment variables (
export TOPSTEPX_API_KEY=…) override.envif set — handy for CI or secrets managers.
Verify the install with a short backtest (uses your creds for the contract spec; the first run also downloads the Chronos checkpoint, so give it a minute):
python bot.py --backtest --symbol NQ --start 2026-05-23 --end 2026-05-25You should see candles/signals scroll past and a BACKTEST NQ | trades=… win=…
summary at the end. If you get that, you're ready.
python bot.py # config defaults
python bot.py --strategy ema # run one strategy
python bot.py --strategy ema keltner bos # run several (highest proba wins)
python bot.py --size 3 # fixed: 3 contracts per trade
python bot.py --risk 500 # risk-based: ~$500 risked per trade
python bot.py --risk 500 --max-contracts 5
python bot.py --proba-floor 0.45 # only take entries graded ≥ 0.45 confidenceStrategies (--strategy overrides config.ACTIVE_STRATEGIES; pick one or
several — when more than one fires on a bar, the highest-proba signal wins):
| name | entry |
|---|---|
supertrend |
SuperTrend flip (period 10, mult 3.0) |
ema |
9/20 EMA crossover, gated to ADX ≥ 18 |
keltner |
Keltner-channel breakout, gated to ADX ≥ 20 |
bos |
break of the last confirmed swing (break of structure) |
orb |
15-min opening-range breakout (09:30 ET), gated to ADX ≥ 18 |
cisd_ote |
CISD displacement → OTE fib-zone pullback (mean-reversion, ICT/SMC) |
jev |
a reasoning model is the strategy: each flat bar it decides long / short / none itself, from all the context the six strategies have (market structure, SuperTrend, EMA, ADX, Keltner, ORB, CISD, their signals and track records) plus its last 8 decisions — the six never trade under jev. See Jev (opt-in; swappable model) |
Jev (--strategy jev, opt-in, never the default) — a reasoning model is the
strategy. The six strategies don't trade; they become its context. On every flat
bar it reads the whole market picture (trend, structure, momentum, volatility,
volume, session, liquidity sweeps, 1h/4h), what every strategy currently sees, any
signals they fired with their model scores, how each one's recent signals actually
played out, and its own last 8 decisions — then decides long, short or none.
Its decision is the entry (no --proba-floor), with the standard STOP_ATR × ATR
stop; the PPO exit manages it. Run it on its own (it can't be combined with other
strategies). See Jev: a reasoning model decides from all the context.
The model is swappable via JEV_DECIDER (.env) as backend:model:
| decider | runs |
|---|---|
jev:jev-latest (default) |
TypeSafe's hosted Jev — needs TYPESAFE_AI_API_KEY |
mlx:mlx-community/Qwen3-0.6B-4bit |
locally in-process on Apple silicon (pip install mlx-lm) |
openai:<model> |
any OpenAI-compatible server at JEV_OPENAI_URL (Ollama, LM Studio, mlx_lm.server, llama.cpp) |
python bot.py --strategy jev # live, with JEV_DECIDER from .env
# backtest with a local Qwen served by mlx_lm.server (thinking off for speed)
mlx_lm.server --model Qwen/Qwen3-4B-MLX-4bit --port 8080
JEV_DECIDER=openai:Qwen/Qwen3-4B-MLX-4bit JEV_OPENAI_URL=http://localhost:8080/v1 \
JEV_LLM_THINK=0 python bot.py --backtest --symbol NQ --strategy jev --start 2026-06-03 --end 2026-06-04Install
mlx-lmin its own venv (or use Ollama) rather than the bot's: it pulls a newertransformersthat the Chronos embedding may not support.
Timeframe (--timeframe MIN, default 3): the bar interval. Models are
per-timeframe and there is no cross-timeframe fallback — only strategies with a
model for the chosen interval are allowed, so a 3-min model is never run on 1-min
bars. Today: 3-min has all six strategies; 1-min has supertrend only
(jev works on either — its panel is whichever strategies the timeframe has).
python bot.py --strategy supertrend --timeframe 1 # 1-min SuperTrend
python bot.py --strategy ema --timeframe 1 # error: no 1-min ema modelA non-3-min run loads data/<symbol>_<tf>min.csv for backtests and the
<model>_<tf>min.joblib entry bundle (e.g. supertrend_chronos_1min.joblib). The
PPO exit is per-timeframe too — if no policy exists for the chosen timeframe yet,
the bot flags it and trains one automatically (python -m ppo_exit.train_ppo_exit --timeframe <tf>,
run once in a subprocess) before trading; it falls back to the fixed-RR exit only if
that training can't produce a policy.
On startup the bot prints your tradable accounts and a banner —
✅ <account> | <contract> | 3-min | [ema] | conf≥0.35 | exit: PPO stop-reprice | size: fixed 1. Confirm it picked your practice/eval account. From there,
every candle, every graded signal (with a TAKE/skip flag), each entry, and
every stop move are logged to the console and log/bot.log. Stop with
Ctrl-C.
Pick strategies and exit shaping in config.py:
ACTIVE_STRATEGIES = ["ema"] # any of: supertrend, ema, keltner, bos, orb (or --strategy)
PROBA_FLOOR = 0.35 # min entry confidence (or pass --proba-floor)
USE_PPO_EXIT = True # False → simple fixed-RR bracket instead
ACTIVATE_R = 2.0 # default exit shaping; the ACTUAL per-timeframe
GIVEBACK_R = 0.75 # ACTIVATE_R/GIVEBACK_R/STOP_ATR live in
# ppo_exit/exit_configs.json (see Tune the exit config)Position sizing — use a fixed size or risk-based sizing (not both).
With --risk (or config.RISK_PER_TRADE), contracts are sized from the stop so
each trade risks roughly the same dollars:
contracts = min(MAX_CONTRACTS, max(1, floor(risk_$ / (stop_ticks × tick_value))))
tick_value is read from the broker contract (/Contract/search → tickValue,
e.g. NQ ≈ $5/tick, MNQ ≈ $0.50/tick). A tighter stop ⇒ more contracts, a wider
stop ⇒ fewer — so dollar risk stays roughly constant, which pairs naturally with
the 0.5×ATR stop. Micros (MNQ, MES, MGC, M2K, MYM) just work — same models
and bars as their parent, sized at the micro's smaller tickValue.
Backtesting runs the exact live logic — same strategies, grading, and PPO trailing exit — over a local CSV, with a simulated broker filling entries/stops/trailing against history. Contract specs (tick size / value) are still looked up from the broker API, so credentials are required.
# one month of NQ
python bot.py --backtest --symbol NQ --start 2026-05-01 --end 2026-06-01
# a micro and a different ticker
python bot.py --backtest --symbol MNQ --start 2026-05-01 --end 2026-06-01
python bot.py --backtest --symbol ES --risk 500--symbolreadsdata/<symbol>_<timeframe>min.csv(ships with NQ, ES, RTY, YM, GC at 3-min); micros use their parent's bars (MNQ → NQ) at the micro's tick value.--start/--endareYYYY-MM-DD(start inclusive, end exclusive); omit either to run from the warmup point / to the end of the file.--size,--risk,--proba-floorand theconfig.pyknobs all apply, so you can A/B a setting by re-running.
It prints a summary — trades, win rate, mean/sum R, profit factor, plus MFE and
per-strategy / per-exit breakdowns — and writes every trade to
log/backtest_<symbol>.csv. Entries fill at the signal bar's close; when a bar
straddles both stop and target the stop is assumed first. Grading embeds each
signal through Chronos, so longer ranges take a few minutes.
each bar ─► every active strategy detects its entry
─► its model grades the signal → proba = P(win)
─► best signal with proba ≥ floor is taken (highest proba wins)
─► one shared Chronos embedding per bar feeds every strategy's grade
─► PPO policy trails the stop bar-by-bar until exit
Each strategy is a thin signal generator paired with its own Chronos+XGBoost model; the model decides which signals to take, and a PPO policy decides when to get out. The entry models are trained on multiple 3-min futures (NQ, ES, RTY, YM, GC) and generalize across them; the framework is ticker- and broker-agnostic.
- Entry — when flat, every active strategy gets a chance to
detect()+grade(). Signals withproba ≥ PROBA_FLOORare candidates; the highest proba wins (one position per contract). The trade enters at market with a protective stop at0.5×ATR(20)— exactly how the models scored the trade. With--strategy jev, a reasoning model decides long / short / none itself from all the strategies' context instead (see Jev). - Exit — each bar the PPO policy (
ppo_exit/policies/) reads the open trade's R-state (unrealized R, MFE, stop distance, ATR/risk, time, momentum) — strategy-agnostic, it never sees how the trade was entered, so one policy fits every strategy on the standard 0.5×ATR(20) stop. It computes a trailing-stop level and reprices the live stop to it via/Order/modify(ratcheting only in your favor). Two knobs shape it:ACTIVATE_R(hold the initial 1R stop until the peak reaches +2R, so winners survive early pullbacks) andGIVEBACK_R(once trailing, the stop never sits more than 0.75R below the running peak). So a trade risks 1R, and once it's up +2R it locks in ≥ +1.25R and rides, giving back ≤ 0.75R from the best point. The policy forward-pass is pure numpy, so the bot never loads torch/SB3 next to xgboost. - Trailed-stop enforcement (intra-bar). The peak is tracked from each bar's
favorable extreme (not just the close), and if a bar's unfavorable wick
crosses the trailed stop the bot closes at market (
/Position/closeContract) rather than rely on a resting broker stop — which the broker rejects ("Invalid stop price") when a fast reversal puts the lock level on the wrong side of the market. This fixes a bug where a big winner could ride all the way back to the initial −1R stop because every stop-modify was rejected. Stops are also direction-aware tick-snapped (floor longs / ceil shorts) so rounding never lands them on the wrong side. - Training = live parity. The policy is trained inside
TrailExitSim(ppo_exit/trail_exit_env.py) and run live byppo_exit/exit_manager.py; the two implement the same give-back logic — activation gate, peak from the bar's favorable extreme, give-back cap, and theMAX_HOLDforce-exit (the policy observesbars_held/MAX_HOLD, so live force-exits at the same horizon it was trained on). Crucially they share the same two-tier fill model (the design algoTraderAI uses): the protective stop is a resting broker stop order kept at the give-back floor, repriced each bar. A give-back exit therefore fills one of two ways, and the sim models both so the policy trains on realistic prices:- resting-stop fill at the floor — when price crosses the floor that was already resting from a prior bar (the common, slow give-back); accurate.
- market-close at the bar close — only when a fast spike-and-reverse
crosses a floor that was tightened this bar and isn't a resting order yet,
so live closes at market (worse than the floor). The old sim recorded this
optimistically at the floor; it now fills at the bar close, matching live.
Entry and exit are never the same candle (both engines start evaluating exits on
the bar after entry). All of this is locked by
tests/test_train_live_parity.py, which drives the same trade through both engines and asserts an identical exit bar and price, including the spike-and-reverse market-close case. The PPO was retrained on this corrected sim.
With --strategy jev a reasoning model is the strategy. The six mechanical
strategies never place a trade — they run every bar purely to give the model their
context, and the model decides on its own whether to go long, short or stay flat.
each flat bar ─► all six strategies run (views + any signals, graded by their models)
─► the consolidated context is built (below) + the last 8 decisions
─► the decider answers: long / short / none
─► long/short enters at market, stop STOP_ATR × ATR; PPO trails the exit
What it sees — every fact once, in readable units (ATR relative to the current close, US Eastern time), consolidated so nothing is duplicated:
| block | contents |
|---|---|
market |
the shared picture: time (session, ET time, weekday) · trend (ADX + slope, swing structure, 1h / 4h move and range position, whether 1h and 4h agree) · momentum (returns over 1/5/20/60 bars, last 30 closes, speed, consistency) · volatility (ATR in bps, vs its median, z-score, range ratios, realized vol) · volume (vs averages, buy/sell pressure, absorption, whether it confirms the move) · lastBar (candle type, body/wicks, rejection) · session (distance from open/high/low/VWAP) · range (10/20-bar high/low/position) · liquiditySweeps (1h/4h bullish/bearish, age, size) |
strategies |
per strategy, only what's unique to it: its view (SuperTrend bias + bars since flip, EMA bias/spread/slope, Keltner position, swing highs/lows, opening range, CISD 1h EMA trend + recent setup, whether each ADX gate is open); its signal if it fired this bar (side, stop, its model's win probability + expected R, setup details like break strength or sweep/displacement); its track record — every signal it fires is followed to +2R / −1R and Jev sees the average R, win rate and last outcomes, so it knows which setups are working now |
yourRecentPicks |
the same +2R / −1R record for its own entries |
recentDecisions |
its last JEV_HISTORY_KEEP (8) decisions as worked examples: the situation, which signals fired, what it chose, and how both a long and a short entered there turned out — so a missed winner or a dodged loser is visible. Quiet bars with no signal and no entry aren't logged. |
The question tells it the outcome being judged (+2R before the −1R stop within 20 bars), the timing (fill at the close, stop size, trailing exit after entry), and the economics (2:1 payoff ⇒ worth taking above ~0.33), and explains every field.
Guards
- No hindsight. Nothing is absolute (no prices, dates or raw volume), the context at a bar is identical whether or not later bars exist (tested), and outcomes in the track records and decision history appear only once they've actually happened.
- No leaked model scores. A strategy model's win probability is withheld
(
modelInSample: true) on bars inside that model's training span — there it would be a leaked label. The backtester also warns loudly when a test range overlaps any model's training (todaybos,emaandorbare trained through the end of the shipped data, so honest tests of those scores need data after 2026-06-04). - One decision per bar. A bar is never decided twice (the live loop also skips a bar the broker returns again during halts/weekends).
- Stale decisions don't trade. Live, if deciding took longer than
JEV_MAX_DECISION_SEC(20 s), the entry is skipped. After any entry, the exit's R accounting is anchored to the actual fill. - Failures stay flat.
none, a timeout, an error, or a malformed answer (validated: the choice must be one of the options) → no trade. Hosted Jev calls time out after 5 s and retry rate limits / 5xx twice. - Misconfiguration fails at startup, not mid-session (unknown backend, missing
key or
mlx-lm). - Contract roll resets the track records and decision history (old-contract prices don't carry over).
- Same data live and in backtests. Both use the newest
BARS_WINDOW(500) completed bars; the TopstepX fetch pads its request across the daily halt and weekends so it always returns a full window of OHLCV. - Decisions are cached in
log/jev_cache.jsonlkeyed by the model, state, question and the backend's prompt/settings — re-running a backtest makes no model calls, and changing a prompt or setting never reuses old answers.
The decision is only as good as the model. In testing, a tiny local model
(Qwen3-0.6B) entered almost every time and lost money, while a larger one used the
track records and declined weak setups. Before trading any decider, backtest it
against the plain multi-strategy baseline (--strategy supertrend ema keltner bos orb cisd_ote) over several out-of-sample weeks, and treat any dates you tuned prompts on
as used.
Knobs (config.py): JEV_DECIDER; JEV_STRATEGIES (whose context it gets; [] = all
with a model for the timeframe); JEV_HISTORY_KEEP (past decisions fed back, 8);
JEV_ONLY_ON_SIGNALS (ask only on bars where a strategy fired — fewer calls for a slow
local model); JEV_MEMORY_TARGET_R / JEV_MEMORY_MAX_BARS / JEV_MEMORY_KEEP (track
record); JEV_MAX_DECISION_SEC, JEV_API_TIMEOUT, JEV_RETRIES, JEV_TIMEOUT;
JEV_LLM_THINK / JEV_LLM_MAX_TOKENS (chat models).
Trading real orders has sharp edges; these are handled so a desync can't leave an unmanaged position. Each is covered by a test (see Tests):
- Signed bracket ticks. SL/TP distances are signed relative to the fill — a long's stop is negative ticks (below), a short's positive (above), TP the mirror — clamped to the 4-tick broker minimum. (Fixes "Invalid stop loss ticks (57). Ticks should be less than zero when longing.")
- No orphaned brackets. A market close (
/Position/closeContract) does not fire the OCO, so the broker leaves the protective stop working.close_positiontherefore sweeps and cancels every resting order for the contract — otherwise a stray stop could later fill and open a brand-new naked position the bot never opened (seen live: a +0.63R short close left its buy-stop, which filled into a naked long at the exact stop price). - Mid-session reconcile. A flat account should have no resting orders, so on every flat bar the bot cancels any strays (an orphaned bracket, a missed exit, a manual order). This is the general safety net: it keeps the account in sync even if a close-time cancel was missed, and is what stops the naked-position scenario above from persisting.
- No silent exits. The common exit is the resting broker stop filling
intra-bar — which happens at the broker, so
manage_trailnever runs and the close was previously unlogged (a −1R stop-out just vanished from the logs). The bot now detects the in-position→flat transition and logs the exit with its realized R, inferred from the level the stop rested at, so every close is on the record. - ORB session gate. The 09:30-ET opening range stays mathematically active
until midnight ET; ORB entries are gated to the RTH window
[~09:45, 16:00)ET (ORB_CLOSE_MIN) so the bot doesn't take stale overnight breakouts of the morning range.
Small, single-responsibility modules:
| file | responsibility |
|---|---|
bot.py |
entry point — handle_bar (detect → grade → enter → trail) + live loop + CLI |
config.py |
all settings: strategies, sizing, exit shaping, strategy params |
broker_base.py |
BrokerClient / OrderRouter — the broker interface |
broker.py |
TopstepXClient (a BrokerClient) over the ProjectX Gateway API + make_broker() |
sim_broker.py |
SimBroker (an OrderRouter) — fills/stops/trailing against a CSV for backtests |
backtest.py |
drives handle_bar over history with date-range selection |
indicators.py |
SuperTrend / ATR / ADX / EMA / Keltner / swings / opening range |
embedder.py / embed_worker.py |
warm Chronos embedding worker — model loaded once per session |
strategies/ |
the pluggable strategies (one file each) + shared base |
ppo_exit/ |
the whole PPO trailing-exit subsystem (see below) |
logsetup.py |
logging to log/bot.log |
models/ |
the entry models (joblib) + FFM feature order |
strategies/ ppo_exit/ (the PPO trailing-exit subsystem)
base.py Strategy ABC exit_manager.py live exit management
supertrend.py → supertrend trail_exit_env.py training env/sim + numpy policy loader
ema_cross.py → ema train_ppo_exit.py trainer (python -m ppo_exit.train_ppo_exit)
keltner.py → keltner optimize_exit.py Optuna config search
bos.py → bos precompute_proba.py entry-grading for training
orb.py → orb exit_configs.json per-timeframe ACTIVATE_R/GIVEBACK_R/STOP_ATR
cisd_ote.py → cisd_ote policies/ the trained .npz policies (per timeframe)
jev/ → jev (a reasoning model decides long/short/none from the others' context)
strategy.py runs the panel each bar, asks the decider, turns the answer into the entry
state.py the consolidated context + the question (what the decider sees)
memory.py track records (+2R / stop) and the last-8-decisions log
deciders.py Decider interface + jev / mlx / openai backends
models/ supertrend_chronos.joblib + _1min ema_cross / keltner_adx / bos / orb _chronos.joblib
ffm_feature_columns.json (FFM feature order)
The bot depends only on the public futures_foundation library (Chronos
embedding + the model head classes + indicator primitives) — no proprietary
code. The joblib bundles run inference directly; the FFM feature block is
computed live via futures_foundation.features.derive_features.
Embeddings stay warm. Chronos runs in a persistent subprocess
(embed_worker.py) that loads the model once per session — torch isolated
from xgboost, model never reloaded. A grade drops from ~3–4 s (cold reload each
call) to ~0.03 s, so backtests/retrains run in minutes and live signal bars are
near-instant. Falls back to the one-shot library path if the worker can't start.
Adding a strategy = one new file in strategies/ implementing _fired() /
_hand_features(), plus its joblib model in models/, then register it in
strategies/__init__.py. The strategy-agnostic exit applies automatically — no
exit work per strategy. Six ship today (supertrend, ema, keltner, bos, orb,
cisd_ote), and a new strategy automatically becomes context for jev too (add a
view in strategies/jev/state.py, and its setup features to SETUP_FEATURES, to give
Jev its full read of the market).
Adding a decider = subclass Decider (or ChatDecider for a chat LLM) in
strategies/jev/deciders.py and register it in DECIDERS; select it with
JEV_DECIDER=<backend>:<model>.
Adding a broker = implement BrokerClient (broker_base.py) in a new module
and add a case to broker.make_broker() + config.BROKER. The bar loop, sizing,
and exit all go through that interface, so nothing else changes — e.g. a Rithmic
client would just provide the same account / market-data / order methods.
python bot.py --retrain-exit # full retrain, then exit
python bot.py --retrain-exit --quick # fast smoke retrain
python -m ppo_exit.train_ppo_exit # same thing, standaloneCatalogs a representative set of entry points in data/NQ_3min.csv, keeps the
ones the bot would enter (proba ≥ 0.35, cached per-timeframe in ppo_exit/proba_cache*.npz), simulates
each trade from the live 0.5×ATR(20) stop with the ACTIVATE_R/GIVEBACK_R
shaping while the agent learns the trail, then benchmarks vs fixed-RR /
constant-trail baselines and writes the policy into ppo_exit/policies/. The
exit is strategy-agnostic (it only sees the trade's R-state), so the same policy
serves every strategy. The printed holdout table is the source of truth for
current performance.
The exit's behaviour is set by ACTIVATE_R / GIVEBACK_R / STOP_ATR — and the
PPO trail collapses to that give-back cap, so the config is the lever. Search it:
python -m ppo_exit.optimize_exit --timeframe 3 --tickers NQ --trials 200 --save
python -m ppo_exit.optimize_exit --timeframe 1 --tickers NQ ES RTY YM GC --trials 300 --saveIt replays the exact give-back sim (TrailExitSim) per config — no PPO retrain per
trial — scoring expectancy on a validation slice and reporting the winner on a
held-out test slice, so the chosen config isn't overfit to one window. Pool
multiple tickers with --tickers for more data. It prints the best
ACTIVATE_R/GIVEBACK_R/STOP_ATR (and whether it beats the current config on
test); with --save it writes the winner to that timeframe's entry in
ppo_exit/exit_configs.json. Then retrain so the policy matches the new shaping:
python -m ppo_exit.train_ppo_exit --timeframe <tf>. (1-min CSVs are local-only —
see Backtest.)
Per-timeframe configs. ppo_exit/exit_configs.json holds the exit shaping
(ACTIVATE_R/GIVEBACK_R/STOP_ATR) keyed by timeframe — 1-min and 3-min
bars want different settings (1-min is noisier, so it whipsaws into the stop far
more; a wider STOP_ATR helps). The active timeframe's config is applied at
runtime to both the live exit and the training sim, so training stays equal to
live. Defaults (no JSON entry) fall back to config.py's ACTIVATE_R/GIVEBACK_R.
pytest tests/ # unit + end-to-end, ~1s
git config core.hooksPath .githooks # once per clone: run tests on every commitThe versioned .githooks/pre-commit runs the suite before each commit and aborts
if anything fails (git commit --no-verify skips it). Enable it once per clone
with the command above.
No network, broker, or Chronos needed — everything runs against the SimBroker
and lightweight fakes. Coverage focuses on the order/exit money paths:
test_bracket_ticks— SL/TP ticks signed by direction (the order-rejection fix)test_close_cancels_brackets/test_no_orphan_orders— no orphaned SL or TP: against a stateful gateway, after a market close, a give-back exit (manage_trail), or a flat reconcile, zero protective orders are left working for the contract (and other contracts are untouched)test_reconcile— a flat account is swept of stray orders every bar; an in-position bar never cancels its live protective stop (mid-session reconcile)test_stop_fill_exit— a broker-stop fill (in-position→flat with no bot close) is logged with its realized R, not silently droppedtest_exit_manager— PPO give-back: activation gate, give-back cap, and the intra-bar wick-cross that closes a winner at market instead of riding it backtest_train_live_parity— the trained sim (TrailExitSim) and the live exit (manage_trail) exit on the same bar at the same price (give-back long/short, MAX_HOLD timeout), and never on the entry candletest_orb_gate— ORB only fires during the RTH window (no overnight breakouts)test_indicators— indicator correctness + strict causality (no look-ahead in EMA / Keltner / opening-range / confirmed swings — they feed every feature)test_strategy_triggers— each strategy's_firedfires long/short on the right pattern and respects its ADX/trend gatetest_detect_signal— entry/stop/risk math (risk = STOP_ATR×ATR, stop on the correct side) that drives sizing and the exittest_position_size— fixed vs risk-based sizing, cap and 1-lot floortest_config_micros— micro→parent symbol mapping (MNQ→NQ, …)test_broker_contract— active front-month selection (rollover) and working-stop filtering, against a stubbed gatewaytest_sim_broker— stop/target/trailing fills, stop-before-target tie, closetest_e2e_trade— full lifecycles throughbacktest.drive→bot.handle_bar(the real driver): long/short give-back winners, stop-out loser, fixed-RR target + stop, the highest-proba resolver, the proba floor, and reconstruct-on-restarttest_timeframe— per-timeframe model resolution + gating (only strategies with a model for the timeframe are allowed; no cross-timeframe fallback)test_ensure_policy— auto-train the PPO exit when a timeframe has no policy (and fall back to fixed-RR if training can't produce one)test_exit_configs— per-timeframe exit config load/save (exit_configs.json)test_optimize_exit— the Optuna scanner's metric/split math and that the give-back replay is genuinely sensitive to each config knobtest_jev_strategy— the Jev strategy: decider wire format, answer validation, retries, cache key and reply parsing; causal track records and decision history; the consolidated context (no duplicates, readable labels, no lookahead, no prices or dates, in-sample model scores withheld); long/short/none entries with the standard stop; one decision per bar; stale-decision skip; startup checks; fill rebase
-
In-sample models:
bos,emaandorbwere trained on the shipped data through 2026-06-04 (no holdout), and thecisd_otetraining pipeline peeks up to ~21 min ahead when filtering zones. Their scores are optimistic on any shipped date; retrain with a holdout (in the Futures-Foundation-Model repo) for honest backtests.jevwithholds in-sample scores from its decider. -
Scope: entry models are trained on NQ/ES/RTY/YM/GC 3-min UTC bars and generalize across them; other tickers/timeframes are out of distribution until retrained.
-
Feature fidelity: 68 of 76 FFM features are computed live; the 8 session/time columns the current library doesn't emit are left NaN (XGBoost handles missing natively) — faithful but not bit-identical to training.
-
PPO exit: one strategy-agnostic policy on the standard 0.5×ATR(20) stop — it sees only the trade's R-state, so it applies identically to every strategy (it trains on a representative catalog of NQ entry points). With
GIVEBACK_R = 0.75the give-back cap dominates, so the exit is effectively a deterministic "trail 0.75R from peak" — loosen it to let trend-riding matter more, tighten it for consistency. -
Exit mode: default
USE_TRAILING_STOP = Falseis the PPO-driven reprice (what it's trained for).Trueuses the ProjectX native trailing bracket (type 5); the PPO can only tighten that, so it mostly sits idle. -
Contract rollover: live trading always uses the broker's active front month (
/Contract/search), and the bot re-resolves it once a day while flat, so a long-running session follows the quarterly roll to the new contract — and its clean warmup history — without a restart. The API is the source of truth; there is no roll calendar to drift. (Backtests use continuous CSV history, so they're unaffected.) -
Online only: the broker API is the single source of truth for contract specs (tick size / value) — there is no hard-coded fallback, so the bot needs credentials + connectivity at startup for both live and backtest. (The Chronos checkpoint itself is cached after the first download.)