This file provides guidance to agents (i.e., ADAL) when working with code in this repository.
This project uses uv (not Poetry). The lock file is uv.lock.
uv sync # Core deps only
uv sync --extra dev --extra notebooks # With dev/notebook extras
uv sync --all-extras # All optional features
source .venv/bin/activateOptional dependency groups: dev, notebooks, ml (torch/transformers/litellm), tuning, viz, storage, full.
uv run pytest -m "not integration" # Unit tests only (CI/CD - no API keys needed)
uv run pytest -m integration # Integration tests (requires .env with API keys)
uv run pytest tests/data/test_indicators.py # Specific test file
uv run pytest -k "test_portfolio" # Tests matching pattern
uv run pytest --cov=quantrl_lab # With coveragePre-commit hooks run automatically on git commit. Run manually with:
pre-commit install # One-time setup
pre-commit run --all-files
pre-commit run --files path/to/file.py # Before committing changesHook configuration (.pre-commit-config.yaml):
- Black:
--line-length=120 --skip-string-normalization - isort:
--profile black - flake8:
--max-line-length=120 - docformatter: Google-style,
--wrap-summaries=72
CRITICAL: Always verify pre-commit hooks pass after making changes. Common failures: unused imports (flake8), line length violations, missing/malformed docstrings.
uv run mkdocs serve # Local preview
uv run mkdocs build # Build (always run before committing API changes)When renaming modules or changing public APIs, update docs/api-reference/ to match actual module paths and run mkdocs build to verify.
Standalone guide tabs (top-level nav, each a self-contained reference):
docs/DATA_SOURCES.md— data loaders, capability matrix, usage per sourcedocs/data-processing.md—DataProcessorandDataPipelinewith all 7 pipeline stepsdocs/environments.md— action/observation/reward spaces, all reward strategies, full env exampledocs/experiments.md—BacktestRunner,ExperimentJob,JobGenerator,AgentExplainer,OptunaRunner
# Install Jupyter kernel (one-time)
python -m ipykernel install --user --name quantrl-lab --display-name "QuantRL-Lab"
# Start Jupyter
jupyter notebook
# Then select "QuantRL-Lab" kernelKey notebooks:
notebooks/backtesting_example.ipynb- Main workflow demonotebooks/feature_selection.ipynb- Feature engineeringnotebooks/hyperparameter_tuning.ipynb- Optuna tuning
- Never commit
.env- contains API keys (Alpaca, Alpha Vantage, etc.) - Use
.env.exampleas template for required environment variables - Python 3.10+ required (see pyproject.toml)
- Module imports:
quantrl_lab.*(package installed in editable mode)
The environment accepts 3 pluggable strategy objects at instantiation — the central pattern of the entire codebase:
env = SingleStockTradingEnv(
data=df,
config=config,
action_strategy=action_strategy, # How actions are processed
reward_strategy=reward_strategy, # How rewards are calculated
observation_strategy=observation_strategy # What the agent observes
)This decouples environment logic from algorithmic choices. Change reward functions without touching environment code. Swap observation features without rewriting state logic.
Step execution order (inside env.step(action)):
- Store
prev_portfolio_value portfolio.process_open_orders()— execute pending limit/stop ordersaction_strategy.handle_action(env, action)— decode & execute new order- Advance
current_step, checkterminated/truncated reward_strategy.calculate_reward(env)— compute reward- Clip reward to
reward_clip_range reward_strategy.on_step_end(env)— stateful hookobservation_strategy.build_observation(env)— compute state
DataLoader (Alpaca/YFinance/AlphaVantage/FMP)
→ get_historical_ohlcv_data() → DataFrame with OHLCV
→ DataProcessor.data_processing_pipeline() → DataFrame with indicators
→ SingleStockTradingEnv → step() delegates to strategies
→ RL Agent (PPO/SAC/A2C via stable-baselines3)
1. Protocol-Based Data Sources (src/quantrl_lab/data/interface.py)
Data sources implement capability protocols for runtime feature detection:
HistoricalDataCapable,LiveDataCapable,NewsDataCapable,StreamingCapable,FundamentalDataCapable,MacroDataCapable,AnalystDataCapable- Check capabilities:
loader.supported_features()→{'historical': True, 'live': False, ...}
2. Indicator Registry (src/quantrl_lab/data/indicators/registry.py)
Technical indicators are auto-registered via decorator:
@IndicatorRegistry.register('RSI')
def rsi(df: pd.DataFrame, window=14) -> pd.DataFrame: ...
df = IndicatorRegistry.apply('RSI', df, window=14)
IndicatorRegistry.list_all() # ['SMA', 'EMA', 'RSI', 'MACD', ...]3. Composite Reward Pattern (src/quantrl_lab/environments/stock/strategies/rewards/composite.py)
Combines reward components with configurable weights:
reward_strategy = CompositeReward(
strategies=[PortfolioValueChangeReward(), DifferentialSortinoReward(), InvalidActionPenalty()],
weights=[0.5, 0.3, 0.2],
normalize_weights=True, # default; auto-normalises weights to sum to 1
auto_scale=False, # if True, z-scores each component before weighting
)4. Data Pipeline (src/quantrl_lab/data/processing/pipeline.py)
Builder pattern for composable data transformations via DataPipeline.
src/quantrl_lab/
├── environments/
│ ├── core/ # Base classes: interfaces.py, config.py, portfolio.py, types.py
│ └── stock/
│ ├── single.py # SingleStockTradingEnv (main environment)
│ ├── multi.py # Multi-stock environment
│ ├── components/ # Portfolio and config components
│ └── strategies/
│ ├── actions/ # standard.py, time_in_force.py
│ ├── observations/ # feature_aware.py, etc.
│ └── rewards/ # portfolio_value, sharpe, sortino, drawdown, turnover,
│ # expiration, invalid_action, boredom, execution_bonus, composite
├── data/
│ ├── sources/ # alpaca_loader, yfinance_loader, alpha_vantage_loader, fmp_loader
│ ├── indicators/ # registry.py, technical.py
│ ├── processing/ # pipeline.py, processor.py
│ │ └── steps/ # cleaning/, features/, alternative/
│ ├── utils/ # date_parsing, symbol_handling, dataframe_normalization,
│ │ # response_validation, request_utils, async_request_utils
│ ├── partitioning/ # ratio.py, date_range.py
│ └── interface.py # Protocol definitions
├── experiments/
│ ├── backtesting/ # runner.py (BacktestRunner), training.py, evaluation.py,
│ │ # builder.py, core.py, metrics.py, explainer.py, analysis.py
│ │ └── config/ # environment_config.py
│ └── tuning/ # optuna_runner.py
├── alpha_research/ # registry.py, models.py, analysis.py, metrics.py, alpha_strategies.py
├── screening/ # llm_hedge_screener.py, response_schemas.py, data_models.py, prompt.py
├── deployment/trading/ # alpaca_trader.py (live trading)
└── utils/ # math.py
tests/ # Pytest test suite (mirrors src/ structure)
├── conftest.py
├── data/
│ ├── sources/ # test_data_sources.py, test_data_sources_integration.py, test_async_loaders.py
│ ├── processing/steps/ # test_analyst.py, etc.
│ ├── utils/ # test_async_request_utils.py
│ └── test_indicators.py
├── environments/stock/
│ ├── strategies/rewards/ # test_boredom.py
│ ├── test_env.py, test_portfolio.py, test_action.py, test_reward.py
│ └── test_time_in_force_action.py
├── experiments/
│ ├── backtesting/ # test_builder.py, test_metrics.py, test_runner.py
│ └── tuning/ # test_optuna_runner.py
└── alpha_research/ # test_alpha_research.py
examples/end_to_end/ # End-to-end training scripts
├── shared/data_utils.py # init_data_sources(), select_alpha_indicators(), process_symbol()
├── train_single_symbol.py, train_single_symbol_sac.py, train_single_symbol_a2c.py
├── train_multi_symbol.py, train_multi_symbol_sac.py, train_multi_symbol_a2c.py
└── tune_single_symbol.py
notebooks/ # Usage example notebooks
# Preferred: fluent builder
from quantrl_lab.experiments.backtesting.builder import BacktestEnvironmentBuilder
env_config = (
BacktestEnvironmentBuilder()
.with_data(train_data=train_df, test_data=test_df)
.with_strategies(action=..., reward=..., observation=...)
.with_env_params(initial_balance=100_000, window_size=20)
.build()
)
# Single job
job = ExperimentJob(algorithm_class=PPO, env_config=env_config, total_timesteps=50000)
runner = BacktestRunner(verbose=True)
result = runner.run_job(job)
BacktestRunner.inspect_result(result)
# Batch / grid
jobs = JobGenerator.generate_grid(algorithms=[PPO, SAC], env_configs={...}, total_timesteps=50000)
results = runner.run_batch(jobs)
BacktestRunner.inspect_batch(results)create_env_config_factory still exists but is DEPRECATED — use BacktestEnvironmentBuilder instead.
New technical indicator: Add to src/quantrl_lab/data/indicators/technical.py with @IndicatorRegistry.register('NAME') decorator. Signature: def name(df: pd.DataFrame, **kwargs) -> pd.DataFrame.
New reward strategy: Inherit BaseRewardStrategy from src/quantrl_lab/environments/core/interfaces.py. Implement calculate_reward(self, env: TradingEnvProtocol) -> float. Optionally implement on_step_end(env) for state updates.
New action strategy: Inherit BaseActionStrategy. Implement define_action_space() and handle_action(env_self, action).
New observation strategy: Inherit BaseObservationStrategy. Implement define_observation_space(env), build_observation(env), and get_feature_names(env).
Typical flow:
- Load data with DataLoader
- Process features with
DataProcessor.data_processing_pipeline() - Define strategies (action/observation/reward)
- Build env config via
BacktestEnvironmentBuilder - Create
ExperimentJoband run viaBacktestRunner - Analyze results with
inspect_result()/inspect_batch()
See: examples/end_to_end/ for complete training scripts, notebooks/backtesting_example.ipynb for notebook workflow.
- Use
PortfolioValueChangeRewardas the primary signal with a minimalTurnoverPenaltyReward. - Heavy penalties (Sortino, drawdown) cause "do nothing" convergence in short training runs.
- Available reward strategies: portfolio_value, sharpe, sortino, drawdown, turnover, expiration, invalid_action, boredom, execution_bonus, composite.
- Line length: 120 characters; single quotes (skip string normalization)
- Docstrings: Google-style, required on all public APIs
- Type hints: Required everywhere; use
Protocolfor interfaces - Python: 3.10+ required
Google-style docstring format:
def fn(param1: str, param2: int = 10) -> pd.DataFrame:
"""
Brief description.
Args:
param1 (str): Description.
param2 (int, optional): Description. Defaults to 10.
Returns:
pd.DataFrame: Description.
Raises:
ValueError: When something is invalid.
"""current_stepadvances before reward: reward sees the price after the action, not beforeaction_typeset byhandle_action(): reward strategies readenv.action_typeandenv.decoded_action_infowhich are set just beforecalculate_reward()is called- Portfolio resets on
reset()but running stats in stateful reward strategies (Sharpe, Sortino) do not reset between episodes by default — callreward_strategy.reset()manually if needed - Price column auto-detected: searches 'close', 'Close', 'adj_close', or 4th column; override with
price_column=arg window_sizeis padding only: observations at the start of an episode are padded by repeating the first row, not by slicing earlier dataCompositeRewardweights not auto-normalised unlessnormalize_weights=True(default);auto_scalerunning stats persist across episodesn_envs > 1only affects training (viamake_vec_env); evaluation always uses a single env- Limit/stop orders with
OrderTIF.TTLexpire afterorder_expiration_steps(default 5); GTC orders persist indefinitely
- Alpaca: Historical, Live, Streaming, News (requires API keys)
- YFinance: Historical, Fundamentals (free, no key needed)
- AlphaVantage: Historical, Fundamentals, Macroeconomic, News (requires API key)
- FMP: Historical (daily + intraday), Analyst data (grades/ratings) (requires API key)
Alpha Vantage free tier: 25 req/day, 1 req/sec; outputsize=full and intraday require premium; loader auto-handles rate limiting.
FMP: single symbol per request; intraday timeframes: 5min, 15min, 30min, 1hour, 4hour.
Copy .env.example to .env:
ALPACA_API_KEY=... # Alpaca (Historical, Live, Streaming, News)
ALPACA_SECRET_KEY=...
ALPACA_BASE_URL=https://paper-api.alpaca.markets
ALPHA_VANTAGE_API_KEY=... # Fundamentals, Macroeconomic, News
FMP_API_KEY=... # Intraday data, Analyst grades/ratings
OPENAI_API_KEY=... # Optional: LLM hedge screenerWhen modifying:
- Action spaces →
src/quantrl_lab/environments/stock/strategies/actions/ - Reward logic →
src/quantrl_lab/environments/stock/strategies/rewards/ - Data loading →
src/quantrl_lab/data/sources/ - Feature engineering →
src/quantrl_lab/data/processing/processor.py - Backtesting flow →
src/quantrl_lab/experiments/backtesting/runner.py - Alpha research →
src/quantrl_lab/alpha_research/
When debugging:
- Check
StockPortfoliostate insrc/quantrl_lab/environments/stock/components/ - Verify data array shape and
current_stepindex - Confirm strategy injection in environment
__init__ - Review
BacktestRunner.inspect_result()output
Core ML stack:
stable-baselines3- PPO, SAC, A2C algorithmsgymnasium- RL environment interfaceoptuna- Hyperparameter tuning
Data & analysis:
pandas,numpy- Data manipulationyfinance,alpaca-py- Market dataseaborn,matplotlib- Visualization
See pyproject.toml for complete list.
- DataValidator: Build a dedicated
DataValidatorclass for systematic quality checks (nulls, price relationships, duplicates). - FillNA Strategy: Implement
FillStrategyprotocol to replace hard-coded fillna logic inSentimentFeatureGenerator. - Caching: Implement
CachedDataSourcewrapper to reduce API calls.
- Protocol Consistency: Standardize on pure Protocol-based design (structural typing) vs ABCs.
- Testing: Add pytest fixtures for common data shapes and improve dependency injection.