This file is intended to orient an AI agent (or a new developer) on how to build, test, and run trading strategies in this codebase. It covers the directory layout, class hierarchy, common tasks, and the commands to run them.
- Shell:
zsh(macOS default). All scripts usezsh, notbash. - Python: A virtualenv at
.venv. Activate withsource .venv/bin/activatebefore running any Python or freqtrade commands. (Not conda.) - Working directory: All scripts in
scripts/and freqtrade commands must be run from the project root (~/freqtrade/), not fromscripts/or any subdir. The scripts use relative paths (strat_dir="user_data/strategies", config atuser_data/strategies/config/…) and prepend.toPYTHONPATHso the in-treefreqtradepackage resolves — both only work when the CWD is the repo root. Running from the wrong directory fails with a misleadingModuleNotFoundError: No module named 'freqtrade'+config file not found(see Troubleshooting). - PYTHONPATH: The scripts set this automatically. If running manually, export:
export PYTHONPATH=~/freqtrade/user_data/strategies:$PYTHONPATH
user_data/strategies/
├── Framework/ ← Universal base classes + mixins
│ ├── BaseStrategy.py ← Root base class for ALL strategies (ROI, stoploss,
│ │ bot_start lifecycle, custom_exit, guards)
│ ├── BaseNNStrategy.py← NN base (inherits the mixins below + BaseStrategy):
│ │ lifecycle hooks, label generation, prediction,
│ │ entry/exit wiring
│ ├── StrategyDiagnostics.py ← mixin: assessment/probability/correlation
│ │ printing (pure diagnostics; mixed into BaseStrategy)
│ ├── FeatureNormalizer.py ← mixin: feature lists (include_list /
│ │ pre_normalized_columns), scaler + PCA state/methods
│ ├── TrainingEngine.py ← mixin: training pipeline — prepare/class-weights/
│ │ train_model, markov, GAN augmentation, maybe_train
│ ├── TrainingSignals.py ← Future-aware label generation
│ └── CreateScalers.py ← Run once to generate normalization scalers
├── utils/ ← Shared utility code
│ ├── DataframeUtils.py
│ ├── DataframePopulator.py ← Adds all technical indicators to a dataframe
│ ├── ClassifierKeras*.py ← Keras classifier implementations
│ ├── ClassifierMLX*.py ← Apple MLX classifier implementations
│ ├── ClassifierMLXMultiTask.py ← MLX multi-task base (focal loss + grad clipping)
│ ├── ClassifierSklearn.py ← sklearn classifier implementations
│ ├── Wavelets.py
│ ├── Forecasters.py
│ └── ...
├── NNNC/ ← N-ary (trinary) Classification strategies + NNNClassifier
├── NNMT/ ← Multi-Task strategies + NNMTClassifier (TF)
│ + NNMTClassifierMLX (Apple Silicon)
├── NNPredict/ ← Regression strategies (continuous future_gain
│ target → rolling-quantile signal). Keras / MLX /
│ Ridge regressor backends.
├── Anomaly/ ← Anomaly Detection strategies (autoencoder + GANomaly)
├── Sklearn/ ← sklearn classifier strategies (RandomForest, XGBoost, …)
│ inherit from BaseNNStrategy via SklearnStrategy
├── GANs/ ← GAN implementations + GANInterface + GANBackend ABC
│ ├── GANInterface.py ← Thin facade: fit/generate/save/load
│ ├── GANBackend.py ← Abstract base + registry (resolve_backend, fit/load_with_fallback)
│ ├── backends/ ← Concrete backends, one file per type/backend pair
│ ├── df_*_gp.py ← TensorFlow trainer implementations
│ ├── df_*_mlx.py ← MLX trainer implementations
│ ├── Create*GAN*.py ← Strategy classes you run under freqtrade backtesting
│ │ to train + save a GAN
│ └── tests/ ← Contract + robustness tests
├── MLX/ ← Apple MLX neural net components (Mamba, etc.)
├── TSPredict/ ← Time-series/wavelet-based strategies
├── SimpleStrategies/ ← Single-indicator strategies (no ML)
├── Debug/ ← Debug/visualisation utilities
├── hyperopts/ ← Custom hyperopt loss functions
├── config/ ← Exchange-specific config files
├── saved_data/ ← Trained model files (keyed by strategy name)
│ + GANs/<gan_type>/ subdirs for every GAN type
│ (GANs_PCA/<gan_type>/ for PCA-reduced strategies)
├── scripts/ ← Shell scripts for all workflow tasks
├── archived/ ← Old/abandoned strategies (reference only)
└── reference/ ← External strategies for learning
Historical note: an older
NeuralNets/directory used to contain a separate git repo holding the NN base class (NNStrategy) and the scaler storage. That directory is now deprecated — its contents have been folded intoFramework/(base classes) and the top-levelsaved_data/(scalers). Older docs / older branches still referenceNeuralNets/; those references are stale.
BaseStrategy(StrategyDiagnostics, IStrategy) (Framework/BaseStrategy.py)
├── BaseNNStrategy(TrainingEngine, FeatureNormalizer, BaseStrategy) ← composes the ML mixins
│ ├── NNNCStrategy (NNNC/NNNCStrategy.py)
│ │ └── NNNC_CGP, NNNC_CGP_LSTM2, NNNC_CGP_MLX_*, ... (concrete strategies)
│ ├── NNMTStrategy (NNMT/NNMTStrategy.py)
│ │ └── NNMT_WGAN, NNMT_WGAN_MLX, NNMT_CGP, ... (concrete strategies)
│ ├── NNPredictStrategy (NNPredict/NNPredictStrategy.py) ← regression family
│ │ └── NNPredict_LSTM, NNPredict_MLX_LSTM, NNPredict_Ridge
│ ├── NNAnomalyStrategy (Anomaly/NNAnomalyStrategy.py)
│ └── SklearnStrategy (Sklearn/SklearnStrategy.py)
│ └── Skl_RandomForest, Skl_XGBoost, Skl_RandomForest_WGAN, ...
├── SimpleStrategy (SimpleStrategies/SimpleStrategy.py)
│ └── AO, BBBreakout, EMACross, ... (each in own file)
└── TSPredict (TSPredict/TSPredict.py)
└── TS_Wavelet_DWT, TS_Coeff_FFT, ... (concrete strategies)
There is no separate NNStrategy class — BaseNNStrategy is the ML
pipeline base and the per-family bases (NNNC/NNMT/Anomaly/Sklearn)
inherit directly from it. Older docs may still reference NNStrategy;
that's stale.
The ML pipeline is decomposed into mixins (composed in MRO order, mixin first) rather than a single monolith — each is byte-identical-relocated, not rewritten:
StrategyDiagnostics(intoBaseStrategy) — assessment / probability / correlation printing. Pure diagnostics; no effect on training or trading.FeatureNormalizer(intoBaseNNStrategy) — feature-list selection (include_list/pre_normalized_columns/one_hot_columns) + scaler/PCA state and the methods that consume them (rolling_dataframe_normalise,normalise_for_gan,clean_for_tensor,apply_pca,get_normalized_size). Generic engine — list contents are overridable class attrs the families set.TrainingEngine(intoBaseNNStrategy) —prepare_training_data,get_training_class_weights,train_model, markov helpers, GAN augmentation (enhance_training_data/preprocess_training_data+ their helpers), andmaybe_train(the training triggerpopulate_indicatorsdelegates to).
The mixins assume composition (they call collaborators via self, e.g.
TrainingEngine calls FeatureNormalizer.scale_dataframe) and introduce no
literal references to their host class — consistent with the one-way dependency
rule below.
- ROI table, stoploss, trailing stop config
bot_start()— freqtrade's one-time-init hook. Handles environment setup, hyperopt-parameter printing, and shared utility instantiation (DataframeUtils,DataframePopulator). Subclasses overriding this MUST callsuper().bot_start(**kwargs).iteration_init()— runs at the start of eachpopulate_indicators()cycle. Now slim: just per-iteration scaler reset.custom_exit()— most actual sells happen here, not inpopulate_exit_trendcustom_stoploss()confirm_trade_entry()/confirm_trade_exit()- Guard conditions (disable trading in bad market conditions)
- Hyperopt parameters: guards, prediction threshold
populate_indicators()callsDataframePopulatorto add all technical indicators- Assessment / distribution / correlation printing — via the
StrategyDiagnosticsmixin (pure diagnostics; safe to ignore when tracing trading behaviour)
BaseNNStrategy itself keeps the freqtrade lifecycle (bot_start/iteration_init/
populate_indicators), label generation (get_training_labels + TrainingSignals
- MASTER thresholds), prediction, and entry/exit wiring. The rest is provided by its composed mixins:
- Classifier construction via
get_classifier_type()+get_classifier()(family-specific; called fromTrainingEngine.maybe_train). - Normalization / feature lists →
FeatureNormalizer. - Training + GAN augmentation + class weights →
TrainingEngine. GAN augmentation is a single dispatcher:enhance_training_datainspectsgan_typeand the label shape (ndarray vs dict), validates the saved GAN's metadata against the strategy's current config, and routes toGANs.balance.balance_single_task/balance_multi_task. Concrete strategies declaregan_type(and optionallygan_target_ratio,gan_run_diagnostics,gan_passthrough_columns,gan_augment_seed) — they don't see GAN-type-specific code. Multi-task 3-D pipelines (e.g.NNMT_WGAN) turn off the 2-D dispatcher withgan_augment = Falseand run their ownpreprocess_training_data. - Train / save / load lifecycle —
populate_indicators()delegates toTrainingEngine.maybe_train()(classifier setup + multi-pair aggregation + training trigger).
Reproducibility: GAN augmentation seeds its sampler from
gan_augment_seed(default 42). Seeded runs are byte-reproducible across separate processes (each cold-starts identically), but NOT across repeated augmentations within one process — the AE-filter's lazy model load drawsmx.randomon first call, polluting the seeded stream. Validate GAN-path changes with a separate-process augmentation diff, not an in-process golden. Setgan_augment_seed = Nonefor non-deterministic augmentation.
- Create a new
.pyfile in the appropriate family directory (e.g.,NNNC/,NNMT/,NNPredict/,Anomaly/,Sklearn/) - Inherit from the appropriate family base class (e.g.,
NNNCStrategy,NNMTStrategy,NNPredictStrategy,SklearnStrategy) - Override
get_classifier_type()andget_classifier()to return your model (or regressor, forNNPredictStrategysubclasses) - Optionally override
add_strategy_indicators(),get_custom_training_data(), etc. - Run backtest over a long period to train and save the model
- Create a new
.pyfile inSimpleStrategies/ - Inherit from
SimpleStrategy - Override
populate_entry_trend()and (optionally)populate_exit_trend() - No training needed
All config files live in user_data/strategies/config/.
| File | Purpose |
|---|---|
config_binanceus.json |
Main backtest/hyperopt config (static pairlist) |
config_binanceus_short.json |
Futures/short trading config |
config_binanceus_download.json |
Download config (may use VolumePairlist) |
config_binanceus_train.json |
Long-range config for training NN models |
config_binanceus_leveraged.json |
Leveraged trading config |
Config files used with scripts are referenced by exchange name. The scripts resolve the path automatically.
# Download last 180 days for all exchanges
zsh user_data/strategies/scripts/download.sh
# Specific exchange
zsh user_data/strategies/scripts/download.sh binanceus
# Futures/short data
zsh user_data/strategies/scripts/download.sh --short binanceus
# Manual command
freqtrade download-data --timerange=20230101- \
-c user_data/strategies/config/config_binanceus.json \
-t 5m 15m 1h 1dzsh user_data/strategies/scripts/test_strat.sh NNNC NNNC_CGP
# Manual equivalent
freqtrade backtesting \
-c user_data/strategies/config/config_binanceus.json \
--strategy-path user_data/strategies/NNNC \
--strategy NNNC_CGP \
--timerange=20230101-20231231zsh user_data/strategies/scripts/test_group.sh NNNC "NNNC_CGP_MLX*"
zsh user_data/strategies/scripts/test_group.sh TSPredict "TS_Wavelet*"zsh user_data/strategies/scripts/hyp_strat.sh NNNC NNNC_CGP
# With custom loss and spaces
zsh user_data/strategies/scripts/hyp_strat.sh \
-l ExpectancyHyperOptLoss \
-s "buy sell roi" \
binanceus NNNC_CGP
# Manual equivalent
freqtrade hyperopt \
-c user_data/strategies/config/config_binanceus.json \
--strategy-path user_data/strategies/NNNC \
--strategy NNNC_CGP \
--spaces buy sell roi \
--hyperopt-loss ExpectancyHyperOptLoss \
--timerange=20230101-20231231zsh user_data/strategies/scripts/check_bias.sh NNNC NNNC_CGPzsh user_data/strategies/scripts/plot_strat.sh NNNC NNNC_CGP BTC/USDT
# Output: user_data/plot/freqtrade-plot-BTC_USDT-5m.htmlfreqtrade backtesting \
-c user_data/strategies/config/config_binanceus.json \
--strategy-path user_data/strategies/Framework \
--strategy CreateScalers \
--timerange=20220101-Scalers are saved to user_data/strategies/saved_data/.
Delete the existing model files and re-run backtest with a long timerange:
rm -rf user_data/strategies/saved_data/NNNC_CGP/*
zsh user_data/strategies/scripts/test_strat.sh NNNC NNNC_CGP \
--timerange 20220101-zsh user_data/strategies/scripts/dryrun_strat.sh NNNC NNNC_CGP
# With port (for multiple simultaneous strategies)
zsh user_data/strategies/scripts/dryrun_strat.sh -p 8081 NNNC NNNC_CGPzsh user_data/strategies/scripts/run_strat.sh NNNC NNNC_CGPUnderstanding the data flow is critical for debugging or extending NN strategies. The pipeline lives in Framework/BaseNNStrategy.py and its mixins (FeatureNormalizer = normalization, TrainingEngine = training/GAN), and is shared across NNNC, NNMT, Anomaly, and Sklearn family bases.
- Calls
super().bot_start()(BaseStrategy: banner, environment, helpers). - Configures TF/MLX device visibility for
util_no_exchangeruns. - Loads MASTER thresholds from saved GAN metadata if present (so the strategy
always uses the same
MIN_BUY_GAIN_THRESHOLD/MIN_SELL_LOSS_THRESHOLD/TRAINING_TYPEthe GAN was trained with). - Falls back to
buy_params/sell_paramsoverrides if no GAN metadata.
- Checks that scalers exist in
saved_data/. - Calls
DataframePopulatorto add all technical indicators. - Adds training labels via
get_training_labels(TrainingSignals). - Delegates to
TrainingEngine.maybe_train(): sets up the classifier, and (if no saved model) collects dataframes across the whitelist, trains viatrain_model— which normalizes (FeatureNormalizer), GAN-augments, computes class weights, and fits — then saves. If a model exists, training is skipped.
- Normalizes the dataframe
- Converts to sequences of shape
[batch, seq_len, num_features] - Returns probability predictions from the model
- Applies threshold to get discrete class (sell/hold/buy)
- Applies guard conditions
- Combines model predictions with additional technical filters
aggregate_pairs = True # train on all pairs combined
use_gan = False # augment with GAN-generated data
seq_len = 8 # input sequence length
num_epochs = 100 # training epochs
batch_size = 1024NN strategies need pair-agnostic features — indicators that have consistent ranges across all pairs. The following are prohibited:
- Raw price (open, close, high, low)
- Raw volume
- Any indicator directly proportional to price
Use instead:
- Oscillators (RSI, MFI, CMF — already bounded 0-100 or -1 to 1)
- Z-score normalized indicators
- Percentage changes (returns)
- Ratio-based indicators
The DataframePopulator class already handles all of this — it adds a standard set of pre-approved indicators. Custom indicators must follow the same rules.
Copy from user_data/strategies/hyperopts/ to user_data/hyperopts/ before using.
| Function | Best for |
|---|---|
ExpectancyHyperOptLoss |
General use — robust across datasets (recommended) |
OnlyExpectancyHyperOptLoss |
When you want pure expectancy, nothing else |
WeightedProfitHyperOptLoss |
Maximising total profit |
QuickProfitHyperOptLoss |
Maximising profit with short-duration trades |
WinHyperOptLoss |
Maximising win rate |
MarketHyperOptLoss |
Market-condition-adjusted win rate |
MedianProfitHyperOptLoss |
Robust profit (less sensitive to outliers) |
PEDHyperOptLoss |
Balanced: Profit + Expectancy + Duration |
Check the .json file beside the .py file (e.g., NNNC_CGP.json). Hyperopt results in this file override Python defaults.
Delete the saved model directory:
rm -rf user_data/strategies/saved_data/<StrategyName>/*First check your working directory — ModuleNotFoundError: No module named 'freqtrade'
(usually alongside config file not found: user_data/strategies/config/config.json) when
running a scripts/*.sh almost always means you launched it from the wrong CWD. The scripts
use relative paths and a .-prefixed PYTHONPATH, so they only work from the project root:
cd ~/freqtrade
zsh user_data/strategies/scripts/test_strat.sh NNNC NNNC_CGPIf you're running Python/freqtrade manually, also make sure PYTHONPATH includes the strategies directory:
export PYTHONPATH=~/freqtrade/user_data/strategies:$PYTHONPATHAlmost certainly lookahead bias. Check that:
- No global
mean()/min()/max()applied to the full column - All rolling operations use
min_periodsand only look backwards - TA-lib functions are used instead of manual rolling where possible
- Run
check_bias.shto confirm
Run CreateScalers (see "Create scalers" above). NN strategies will fail at startup without them.
- Create a new file in
user_data/strategies/hyperopts/inheriting fromIHyperOptLoss - Implement
hyperopt_loss_function(results, trade_count, min_date, max_date, config, processed, backtest_stats, *args, **kwargs) -> float - Return a float where lower is better
- Copy the file to
user_data/hyperopts/to make it available to freqtrade - Reference it with
--hyperopt-loss <ClassName>or-l <ClassName>
There are two distinct extension cases:
Most additions are this case (e.g. a CTAB-GAN+ trained on a different feature set, or a WGAN with a different augmentation ratio).
- Create a new builder script in
GANs/(e.g.CreateMyGAN.py). - Inherit from
CreateGAN(single-task) orCreateMTGAN(multi-task) and setgan_type = GANType.Xplus any per-class config overrides. The pre-existing classes (CreateWGAN,CreateCtabGanPlus, etc.) are thin shims over these two unified bases — copy one of them as a template. - Run the new strategy via backtesting on a long timerange to train and save.
- The saved model goes to
saved_data/<StrategyName>/GANs/<gan_type>/(orGANs_PCA/<gan_type>/for PCA-reduced strategies) — the layout is centralised inGANs/paths.py::gan_save_path, so subclasses don't pick a directory name. - Strategies consume the GAN by setting
gan_type = GANType.Xon the class.BaseNNStrategy.enhance_training_datathen loads the model viaGANInterface, validates its saved metadata against the strategy's current thresholds (raisingGANMetadataMismatchErroron drift), and dispatches class balancing throughGANs.balance.balance_single_task/balance_multi_task. Override_gan_expected_metadataif your strategy needs to validate extra keys on top of the default thresholds + training_type.
Rare. Add when the existing types can't capture the new behaviour (e.g. a different label modality or a fundamentally different conditioning).
Reference: TabDDPM (
GANType.TAB_DDPM) was added as a Case B follow-up — seedocs/superpowers/specs/2026-05-11-tabddpm-design.mdanddocs/superpowers/plans/2026-05-11-tabddpm-implementation.mdfor a concrete worked example. It's MLX-only and continuous-only.
- Add the new enum entry to
GANType(GANs/GANType.py). - Create the trainer/model class(es) in
GANs/df_<name>_*.py(TF and/or MLX, following the existingdf_wgan_*anddf_ctab_*patterns). - Create a backend class in
GANs/backends/<name>.pysubclassingGANBackendand decorate with@register_backend. Implementfit / generate / save / loadandis_available(). - Add
from . import <name> # noqa: F401toGANs/backends/__init__.pyso it registers at import time. - Add a
_DEFAULTSentry inGANInterfaceif your type has trainer-specific defaults that callers shouldn't be forced to know about. - Add the new type to
_BACKEND_MIGRATEDinGANInterface.py. - Cover it with the contract tests in
GANs/tests/:test_gan_metadata_roundtrip.py(what gets persisted),test_gan_output_contracts.py(shape/dtype/finiteness), and (gated)test_gan_robustness.py.
These apply to all code under user_data/strategies/. When in doubt, match the
surrounding file; consistency within a module beats global purity.
- Formatting / linting:
ruffis the source of truth (formatter + linter). 88-character lines. Runruff formatandruff check --fixbefore declaring a change done. Don't hand-format what the formatter owns. - Naming:
snake_casefor functions and variables,PascalCasefor classes,UPPER_CASEfor module-level constants. Strategy class names are load-bearing — they're referenced by configs,saved_data/directories, and thetest_*.shscripts — so never rename a strategy class as a side effect of a refactor. If a rename is genuinely needed, list it explicitly and update the matchingsaved_data/<Name>/dir and any<Name>.json. - Type hints on all new/edited function signatures (params and return).
Prefer
from __future__ import annotationsat module top so forward references andX | Nonework without runtime cost. - Docstrings on public classes and methods — one line on what and why, not a restatement of the signature. Document the contract (shapes, units, side effects), especially for anything touching the NN pipeline.
- No secrets in code. Exchange keys, API tokens, etc. live in config/env,
never in
.pyfiles, and configs with secrets stay out of git. - Imports: absolute within the strategies tree (the directory is on
PYTHONPATH). No wildcard imports except the deliberatefrom . import <name> # noqa: F401registry pattern inGANs/backends/. - Lookahead safety is a hard rule, not a style preference. No global
mean/min/maxover a full column; rolling ops usemin_periodsand only look backwards (see Troubleshooting). A "cleaner" refactor that introduces lookahead bias is a regression — verify withcheck_bias.sh.
The hierarchy here grew over a long time and has drifted. These rules are the target state; the refactor work should move code toward them.
- A base class must never reference a subclass — not in code, not in type
hints, not even in comments or docstrings. No imports from a subclass module,
no
isinstance(self, NNNC_CGP_MLX), no "used by NNMT_WGAN" notes inBaseNNStrategy. If a base needs subclass-specific behavior, that's a signal to invert the dependency: expose a hook/abstract method the subclass overrides, or a registry the subclass registers into (the@register_backendpattern inGANs/is the model to copy). - Imports flow upward only:
NNNC_CGP → NNNCStrategy → BaseNNStrategy → BaseStrategy. A lower layer importing from a higher/sibling family layer is a layering violation to flag.
- Inheritance is for genuine is-a with shared behavior. The current
Base → NNNCStrategy → NNNC_CGP → NNNC_CGP_MLX → NNNC_CGP_MLX_LSTMdepth is a smell — each level should earn its existence with real differentiated behavior, not just hold one overridden constant. - When subclasses differ only in a value (model name, a threshold, a backend
choice), that's configuration, not a class. Collapse them into one
parameterized class or a small data-driven table rather than N near-identical
files. (
DataframePopulator,GANInterface, and theCreateGAN/CreateMTGANshims already embody this — extend that style.) - Reach for mixins/composition when behavior is shared across siblings in
different branches (e.g. an MLX-device concern shared by NNNC and NNMT). A
mixin shared by two families belongs in
Framework/orutils/, not duplicated in each family dir.
- A subclass override that is byte-identical to the parent should be deleted — it's dead weight and hides where behavior actually lives.
- An override that changes behavior must respect the parent contract: same shapes / return types / side effects. If it can't, the method is mis-placed in the hierarchy.
- Lifecycle hooks that the framework chains (
bot_start,iteration_init) must callsuper()— overriding withoutsuper()silently drops base setup. Flag any override that doesn't.
- One class, one job. New behavior should go into a collaborator (
utils/, a backend, a populator) the strategy uses, or one of the Framework mixins, not anotherif-branch in the base. - The
BaseNNStrategydecomposition intoStrategyDiagnostics/FeatureNormalizer/TrainingEnginemixins is the realized example of this: each was a behavior-preserving (byte-identical) relocation, verified by the training-pipeline characterization fixtures + scaler-diff/plot/backtest. Add to the matching mixin rather than re-growingBaseNNStrategy. - Cross-family shared logic lives in exactly one place. Duplicate helper methods
across family bases are consolidation targets — lift them to
BaseNNStrategyor autils/helper. - Abstract bases (
GANBackend, the family bases' required overrides) should declare their required methods explicitly (@abstractmethod) so a half-built subclass fails loudly at construction, not deep in a backtest.