chore: Regenerate all playbooks

This commit is contained in:
GitLab CI 2026-01-02 22:21:53 +00:00
parent a0d99066db
commit a5431dd77a
39 changed files with 17320 additions and 98 deletions

View File

@ -27,7 +27,9 @@ Each playbook includes prerequisites, step-by-step instructions, troubleshooting
- [CUDA-X Data Science](nvidia/cuda-x-data-science/)
- [DGX Dashboard](nvidia/dgx-dashboard/)
- [FLUX.1 Dreambooth LoRA Fine-tuning](nvidia/flux-finetuning/)
- [Install and Use Isaac Sim and Isaac Lab](nvidia/isaac/)
- [Optimized JAX](nvidia/jax/)
- [Live VLM WebUI](nvidia/live-vlm-webui/)
- [LLaMA Factory](nvidia/llama-factory/)
- [Build and Deploy a Multi-Agent Chatbot](nvidia/multi-agent-chatbot/)
- [Multi-modal Inference](nvidia/multi-modal-inference/)
@ -38,9 +40,11 @@ Each playbook includes prerequisites, step-by-step instructions, troubleshooting
- [NVFP4 Quantization](nvidia/nvfp4-quantization/)
- [Ollama](nvidia/ollama/)
- [Open WebUI with Ollama](nvidia/open-webui/)
- [Portfolio Optimization](nvidia/portfolio-optimization/)
- [Fine-tune with Pytorch](nvidia/pytorch-fine-tune/)
- [RAG Application in AI Workbench](nvidia/rag-ai-workbench/)
- [SGLang Inference Server](nvidia/sglang/)
- [Single-cell RNA Sequencing](nvidia/single-cell/)
- [Speculative Decoding](nvidia/speculative-decoding/)
- [Set up Tailscale on Your Spark](nvidia/tailscale/)
- [TRT LLM for Inference](nvidia/trt-llm/)

190
nvidia/isaac/README.md Normal file
View File

@ -0,0 +1,190 @@
# Install and Use Isaac Sim and Isaac Lab
> Build Isaac Sim and Isaac Lab from source for Spark
## Table of Contents
- [Overview](#overview)
- [Run Isaac Sim](#run-isaac-sim)
- [Run Isaac Lab](#run-isaac-lab)
- [Troubleshooting](#troubleshooting)
---
## Overview
## Basic idea
Isaac Sim is a robotics simulation platform built on NVIDIA Omniverse that enables photorealistic, physically accurate simulations of robots and environments. It provides a comprehensive toolkit for robotics development, including physics simulation, sensor simulation, and visualization capabilities. Isaac Lab is a reinforcement learning framework built on top of Isaac Sim, designed for training and deploying RL policies for robotics applications.
Isaac Sim uses GPU-accelerated physics simulation to enable fast, realistic robot simulations that can run faster than real-time. Isaac Lab extends this with pre-built RL environments, training scripts, and evaluation tools for common robotics tasks like locomotion, manipulation, and navigation. Together, they provide an end-to-end solution for developing, training, and testing robotics applications entirely in simulation before deploying to real hardware.
## What you'll accomplish
You'll build Isaac Sim from source on your NVIDIA DGX Spark device and set up Isaac Lab for reinforcement learning experiments. This includes compiling the Isaac Sim engine, configuring the development environment, and running a sample RL training task to verify the installation.
## What to know before starting
- Experience building software from source using CMake and build systems
- Familiarity with Linux command line operations and environment variables
- Understanding of Git version control and Git LFS for large file management
- Basic knowledge of Python package management and virtual environments
- Familiarity with robotics simulation concepts (helpful but not required)
## Prerequisites
**Hardware Requirements:**
- NVIDIA Grace Blackwell GB10 Superchip System
- At least 50GB available storage space for Isaac Sim build artifacts and dependencies
**Software Requirements:**
- NVIDIA DGX OS
- GCC/G++ 11 compiler: `gcc --version` shows version 11.x
- Git and Git LFS installed: `git --version` and `git lfs version` succeed
- Network access to clone repositories from GitHub and download dependencies
## Ancillary files
All required assets can be found in the Isaac Sim and Isaac Lab repositories on GitHub:
- [Isaac Sim repository](https://github.com/isaac-sim/IsaacSim) - Main Isaac Sim source code
- [Isaac Lab repository](https://github.com/isaac-sim/IsaacLab) - Isaac Lab RL framework
## Time & risk
* **Estimated time:** 30 min (including build time which typically takes 10-15 minutes)
* **Risk level:** Medium
* Large repository clones with Git LFS may fail due to network issues
* Build process requires significant compilation time and may encounter dependency issues
* Build artifacts consume substantial disk space
* **Rollback:** Isaac Sim build directory can be removed to free space. Git repositories can be deleted and re-cloned if needed.
* **Last Updated:** 1/06/2024
* First Publication
## Run Isaac Sim
## Step 1. Install gcc-11 and git-lfs
Confirm that GCC/G++ 11 is being used before building using the following commands:
```bash
sudo apt update && sudo apt install -y gcc-11 g++-11
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 200
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-11 200
sudo apt install git-lfs
gcc --version
g++ --version
```
## Step 2. Clone the Isaac Sim repository into your workspace
Clone Isaac Sim from the NVIDIA GitHub repository and set up Git LFS to pull large files.
> **Note:** For Isaac Sim 6.0.0 Early Developer Release, use:
> ```bash
> git clone --depth=1 --recursive --branch=develop https://github.com/isaac-sim/IsaacSim
> ```
```bash
git clone --depth=1 --recursive https://github.com/isaac-sim/IsaacSim
cd IsaacSim
git lfs install
git lfs pull
```
## Step 3. Build Isaac Sim
Build Isaac Sim and accept the license agreement.
```bash
./build.sh
```
You get this following message when build is successful: **BUILD (RELEASE) SUCCEEDED (Took 674.39 seconds)**
## Step 4. Recognize Isaac Sim for the system.
Be sure that you are inside Isaac Sim directory when running the following commands.
```bash
export ISAACSIM_PATH="${PWD}/_build/linux-aarch64/release"
export ISAACSIM_PYTHON_EXE="${ISAACSIM_PATH}/python.sh"
```
## Step 5. Run Isaac Sim
Launch Isaac Sim using the provided Python executable.
```bash
export LD_PRELOAD="$LD_PRELOAD:/lib/aarch64-linux-gnu/libgomp.so.1"
${ISAACSIM_PATH}/isaac-sim.sh
```
## Run Isaac Lab
## Step 1. Install Isaac Sim
If you haven't already done so, install [Isaac Sim](build.nvidia.com/spark/isaac/isaac-sim) first.
## Step 2. Clone the Isaac Lab repository into your workspace
Clone Isaac Lab from the NVIDIA GitHub repository.
```bash
git clone --recursive https://github.com/isaac-sim/IsaacLab
cd IsaacLab
```
## Step 3. Create a symbolic link to the Isaac Sim installation
Be sure that you have already installed Isaac Sim from [Isaac Sim](build.nvidia.com/spark/isaac/isaac-sim) before running the following command.
```bash
echo "ISAACSIM_PATH=$ISAACSIM_PATH"
```
Create a symbolic link to the Isaac Sim installation directory.
```bash
ln -sfn "${ISAACSIM_PATH}" "${PWD}/_isaac_sim"
ls -l "${PWD}/_isaac_sim/python.sh"
```
## Step 4. Install Isaac Lab.
```bash
./isaaclab.sh --install
```
## Step 5. Run Isaac Lab and Validate Humanoid Reinforcement Learning Training
Launch Isaac Lab using the provided Python executable. You can run the training in one of the following modes:
**Option 1: Headless Mode (Recommended for Faster Training)**
Runs without visualization and outputs logs directly to the terminal.
```bash
export LD_PRELOAD="$LD_PRELOAD:/lib/aarch64-linux-gnu/libgomp.so.1"
./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Velocity-Rough-H1-v0 --headless
```
**Option 2: Visualization Enabled**
Runs with real-time visualization in Isaac Sim, allowing you to monitor the training process interactively.
```bash
export LD_PRELOAD="$LD_PRELOAD:/lib/aarch64-linux-gnu/libgomp.so.1"
./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Velocity-Rough-H1-v0
```
## Troubleshooting
## Common issues for Isaac Sim
| Symptom | Cause | Fix |
|-----------------------------|--------------------------|-----------------------------------|
| Isaac Sim error compilation | gcc+11 is not by default | Be sure that gcc+11 is by default |
| Isaac Sim not executes | Error libgomp.so.1 | Add export LD_PRELOAD |
| Error in build | old installation | Remove .cache folder |
## Common Issues for Isaac Lab
| Symptom | Cause | Fix |
|----------------------------------|--------|-----|
| Isaac Lab not executes | Error libgomp.so.1 | Add export LD_PRELOAD |

View File

@ -0,0 +1,393 @@
# Live VLM WebUI
> Real-time Vision Language Model interaction with webcam streaming
## Table of Contents
- [Overview](#overview)
- [Instructions](#instructions)
- [Command Line Options](#command-line-options)
- [Accept the SSL Certificate](#accept-the-ssl-certificate)
- [Grant Camera Permissions](#grant-camera-permissions)
- [Performance Optimization Tips](#performance-optimization-tips)
- [Troubleshooting](#troubleshooting)
---
## Overview
## Basic idea
Live VLM WebUI is a universal web interface for real-time Vision Language Model (VLM) interaction and benchmarking. It enables you to stream your webcam directly to any VLM backend (Ollama, vLLM, SGLang, or cloud APIs) and receive live AI-powered analysis. This tool is perfect for testing VLM models, benchmarking performance across different hardware configurations, and exploring vision AI capabilities.
The interface provides WebRTC-based video streaming, integrated GPU monitoring, customizable prompts, and support for multiple VLM backends. It works seamlessly with the powerful Blackwell GPU in your DGX Spark, enabling real-time vision inference at impressive speeds.
## What you'll accomplish
You'll set up a complete real-time vision AI testing environment on your DGX Spark that allows you to:
- Stream webcam video and get instant VLM analysis through a web browser
- Test and compare different vision language models (Gemma 3, Llama Vision, Qwen VL, etc.)
- Monitor GPU and system performance in real-time while models process video frames
- Customize prompts for various use cases (object detection, scene description, OCR, safety monitoring)
- Access the interface from any device on your network with a web browser
## What to know before starting
- Basic familiarity with Linux command line and terminal operations
- Basic knowledge of Python package installation with pip
- Basic knowledge of REST APIs and how services communicate via HTTP
- Familiarity with web browsers and network access (IP addresses, ports)
- Optional: Knowledge of Vision Language Models and their capabilities (helpful but not required)
## Prerequisites
**Hardware Requirements:**
- Webcam (laptop built-in camera, USB camera, or remote browser with camera)
- At least 10GB available storage space for Python packages and model downloads
**Software Requirements:**
- DGX Spark with DGX OS installed
- Python 3.10 or later (verify with `python3 --version`)
- pip package manager (verify with `pip --version`)
- Network access to download Python packages from PyPI
- A VLM backend running locally (Ollama being easiest) or cloud API access
- Web browser access to `https://<SPARK_IP>:8090`
**VLM Backend Options:**
1. **Ollama** (recommended for beginners) - Easy to install and use
2. **vLLM** - Higher performance for production workloads
3. **SGLang** - Alternative high-performance backend
4. **NIM** - NVIDIA Inference Microservices for optimized performance
5. **Cloud APIs** - NVIDIA API Catalog, OpenAI, or other OpenAI-compatible APIs
## Ancillary files
All source code and documentation can be found at the [Live VLM WebUI GitHub repository](https://github.com/NVIDIA-AI-IOT/live-vlm-webui).
The package will be installed directly via pip, so no additional files are required for basic installation.
## Time & risk
* **Estimated time:** 20-30 minutes (including Ollama installation and model download)
* 5 minutes to install Live VLM WebUI via pip
* 10-15 minutes to install Ollama and download a model (varies by model size)
* 5 minutes to configure and test
* **Risk level:** Low
* Python packages installed in user space, isolated from system
* No system-level changes required
* Port 8090 must be accessible for web interface functionality
* Self-signed SSL certificate requires browser security exception
* **Rollback:** Uninstall the Python package with `pip uninstall live-vlm-webui`. Ollama can be uninstalled with standard package removal. No persistent changes to DGX Spark configuration.
* **Last Updated:** December 2025
* First Publication
## Instructions
## Step 1. Install Ollama as VLM Backend
First, install Ollama to serve Vision Language Models. Ollama is one of the easiest options to run/serve models locally on your DGX Spark.
```bash
## Install Ollama
curl -fsSL https://ollama.com/install.sh | sh
## Verify installation
ollama --version
```
Ollama will automatically start as a system service and detect your Blackwell GPU.
Now download a vision language model. We recommend starting with `gemma3:4b` for quick testing:
```bash
## Download a lightweight model (recommended for testing)
ollama pull gemma3:4b
## Alternative models you can try:
## ollama pull llama3.2-vision:11b # Sometime better quality, slower
## ollama pull qwen2.5-vl:7b #
```
The model download may take 5-15 minutes depending on your network speed and model size.
Verify Ollama is working:
```bash
## Check if Ollama API is accessible
curl http://localhost:11434/v1/models
```
Expected output should show a JSON response listing your downloaded models.
## Step 2. Install Live VLM WebUI
Install Live VLM WebUI using pip:
```bash
pip install live-vlm-webui
```
The installation will download all required Python dependencies and install the `live-vlm-webui` command.
Now start the server:
```bash
## Launch the web server
live-vlm-webui
```
The server will:
- Auto-generate SSL certificates for HTTPS (required for webcam access)
- Start the WebRTC server on port 8090
- Detect your Blackwell GPU automatically
The server will start and display output like:
```
Starting Live VLM WebUI...
Generating SSL certificates...
GPU detected: NVIDIA GB10 Blackwell
Access the WebUI at:
Local URL: https://localhost:8090
Network URL: https://<YOUR_SPARK_IP>:8090
Press Ctrl+C to stop the server
```
### Command Line Options
Live VLM WebUI supports several command-line options for customization:
```bash
## Specify a different port
live-vlm-webui --port 8091
## Use custom SSL certificates
live-vlm-webui --ssl-cert /path/to/cert.pem --ssl-key /path/to/key.pem
## Change default API endpoint
live-vlm-webui --default-api-base http://localhost:8000/v1
## Run in background (optional)
nohup live-vlm-webui > live-vlm.log 2>&1 &
```
## Step 3. Access the Web Interface
Open your web browser and navigate to:
```
https://<YOUR_SPARK_IP>:8090
```
Replace `<YOUR_SPARK_IP>` with your DGX Spark's IP address. You can find it with:
```bash
hostname -I | awk '{print $1}'
```
**Important:** You must use `https://` (not `http://`) because modern browsers require secure connections for webcam access.
### Accept the SSL Certificate
Since the application uses a self-signed SSL certificate, your browser will show a security warning. This is expected and safe.
**In Chrome/Edge:**
1. Click "**Advanced**" button
2. Click "**Proceed to \<YOUR_SPARK_IP\> (unsafe)**"
**In Firefox:**
1. Click "**Advanced...**"
2. Click "**Accept the Risk and Continue**"
### Grant Camera Permissions
When prompted, allow the website to access your camera. The webcam stream should appear in the interface.
> [!TIP]
> **Remote Access Recommended:** For the best experience, access the web interface from a laptop or PC on the same network. This provides better browser performance and built-in webcam access compared to accessing locally on the DGX Spark.
## Step 4. Configure VLM Settings
The interface auto-detects local VLM backends. Verify the configuration in the **VLM API Configuration** section on the left sidebar:
**API Endpoint:** Should show `http://localhost:11434/v1` (Ollama)
**Model Selection:** Click the dropdown and select your downloaded model (e.g., `gemma3:4b`)
**Optional Settings:**
- **Max Tokens:** Controls response length (default: 512, reduce to 100-200 for faster responses)
- **Frame Processing Interval:** How many frames to skip between analyses (default: 30 frames, increase for slower pace)
### Performance Optimization Tips
For the best performance on DGX Spark Blackwell GPU:
- **Model Selection:** `gemma3:4b` gives 1-2s/frame, `llama3.2-vision:11b` gives slower speed.
- **Frame Interval:** Set to 60 frames (2 seconds at 30 fps) or bigger for comfortable viewing
- **Max Tokens:** Reduce to 100 for faster responses
> [!NOTE]
> DGX Spark uses a Unified Memory Architecture (UMA), which enables dynamic memory sharing between the GPU and CPU.
> With many applications still updating to take advantage of UMA, you may encounter memory issues even when within
> the memory capacity of DGX Spark. If that happens, manually flush the buffer cache with:
```bash
sudo sh -c 'sync; echo 3 > /proc/sys/vm/drop_caches'
```
## Step 5. Start Analyzing Video
Click the green "**Start Camera and Start VLM Analysis**" button.
The interface will:
1. Start streaming your webcam via WebRTC
2. Begin processing frames and sending them to the VLM
3. Display AI analysis results in real-time
4. Show GPU/CPU/RAM metrics at the bottom
You should see:
- **Live video feed** on the right side (with mirror toggle)
- **VLM analysis results** overlaid on video or in the info box
- **Performance metrics** showing latency and frame count
- **GPU monitoring** showing Blackwell GPU utilization and VRAM usage
With the Blackwell GPU in DGX Spark, you should see inference times of **1-2 seconds per frame** for `gemma3:4b` and similar speeds for `llama3.2-vision:11b`.
## Step 6. Customize Prompts
The **Prompt Editor** at the bottom of the left sidebar allows you to customize what the VLM analyzes.
**Quick Prompts** - 8 presets ready to use:
- **Scene Description** - "Describe what you see in this image in one sentence."
- **Object Detection** - "List all objects you can see in this image, separated by commas."
- **Activity Recognition** - "Describe the person's activity and what they are doing."
- **Safety Monitoring** - "Are there any safety hazards visible? Answer with 'ALERT: description' or 'SAFE'."
- **OCR / Text Recognition** - "Read and transcribe any text visible in the image."
- And more...
**Custom Prompts** - Enter your own:
Try this for real-time CSV output (useful for downstream applications):
```
List all objects you can see in this image, separated by commas.
Do not include explanatory text. Output only the comma-separated list.
```
The VLM will immediately start using the new prompt for the next frame analysis. This enables real-time "prompt engineering" where you can iterate and refine prompts while watching live results.
## Step 7. Test Different Models (Optional)
Want to compare models? Download another model and switch:
```bash
## Download another model
ollama pull llama3.2-vision:11b
## The model will appear in the Model dropdown in the web interface
```
In the web interface:
1. Stop VLM analysis (if running)
2. Select the new model from the **Model** dropdown
3. Start VLM analysis again
Compare inference speed and quality between models on your DGX Spark's Blackwell GPU.
## Step 8. Monitor Performance
The bottom section shows real-time system metrics:
- **GPU Usage** - Blackwell GPU utilization percentage
- **VRAM Usage** - GPU memory consumption
- **CPU Usage** - System CPU utilization
- **System RAM** - Memory usage
Use these metrics to:
- Benchmark different models on the same hardware
- Identify performance bottlenecks
- Optimize settings for your use case
## Step 9. Cleanup
When you're done, stop the server with `Ctrl+C` in the terminal where it's running.
To completely remove Live VLM WebUI:
```bash
pip uninstall live-vlm-webui
```
Your Ollama installation and downloaded models remain available for future use.
To remove Ollama as well (optional):
```bash
## Uninstall Ollama
sudo systemctl stop ollama
sudo rm /usr/local/bin/ollama
sudo rm -rf /usr/share/ollama
## Remove Ollama models (optional)
rm -rf ~/.ollama
```
## Step 10. Next Steps
Now that you have Live VLM WebUI running, explore these use cases:
**Model Benchmarking:**
- Test multiple models (Gemma 3, Llama Vision, Qwen VL) on your DGX Spark
- Compare inference latency, accuracy, and GPU utilization
- Evaluate structured output capabilities (JSON, CSV)
**Application Prototyping:**
- Use the web interface as reference for building your own VLM applications
- Integrate with ROS 2 for robotics vision
- Connect to RTSP IP cameras for security monitoring (Beta feature)
**Cloud API Integration:**
- Switch from local Ollama to cloud APIs (NVIDIA API Catalog, OpenAI)
- Compare edge vs. cloud inference performance and costs
- Test hybrid deployments
To use NVIDIA API Catalog or other cloud APIs:
1. In the **VLM API Configuration** section, change the **API Base URL** to:
- NVIDIA API Catalog: `https://integrate.api.nvidia.com/v1`
- OpenAI: `https://api.openai.com/v1`
- Other: Your custom endpoint
2. Enter your **API Key** in the field that appears
3. Select your model from the dropdown (list is fetched from the API)
**Advanced Configuration:**
- Use vLLM, SGLang, or NIM backends for higher throughput
- Set up NIM for optimized NVIDIA-specific performance
- Customize the Python backend for your specific use case
For more advanced usage, see the [full documentation](https://github.com/NVIDIA-AI-IOT/live-vlm-webui/tree/main/docs) on GitHub.
For latest known issues, please review the [DGX Spark User Guide](https://docs.nvidia.com/dgx/dgx-spark/known-issues.html) and the [Live VLM WebUI Troubleshooting Guide](https://github.com/NVIDIA-AI-IOT/live-vlm-webui/blob/main/docs/troubleshooting.md).
## Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| pip install shows "error: externally-managed-environment" | Python 3.12+ prevents system-wide pip installs | Use virtual environment: `python3 -m venv live-vlm-env && source live-vlm-env/bin/activate && pip install live-vlm-webui` |
| Browser shows "Your connection is not private" warning | Application uses self-signed SSL certificate | Click "Advanced" → "Proceed to \<IP\> (unsafe)" - this is safe and expected behavior |
| Camera not accessible or "Permission Denied" | Browser requires HTTPS for webcam access | Ensure you're using `https://` (not `http://`). Accept self-signed certificate warning and grant camera permissions when prompted |
| "Failed to connect to VLM" or "Connection refused" | Ollama or VLM backend not running | Verify Ollama is running with `curl http://localhost:11434/v1/models`. If not running, start with `sudo systemctl start ollama` |
| VLM responses are very slow (>5 seconds per frame) | Model too large for available VRAM or incorrect configuration | Try a smaller model (`gemma3:4b` instead of larger models). Increase Frame Processing Interval to 60+ frames. Reduce Max Tokens to 100-200 |
| GPU stats show "N/A" for all metrics | NVML not available or GPU driver issues | Verify GPU access with `nvidia-smi`. Ensure NVIDIA drivers are properly installed |
| "No models available" in model dropdown | API endpoint incorrect or models not downloaded | Verify API endpoint is `http://localhost:11434/v1` for Ollama. Download models with `ollama pull gemma3:4b` |
| Server fails to start with "port already in use" | Port 8090 already occupied by another service | Stop the conflicting service or use `--port` flag to specify a different port: `live-vlm-webui --port 8091` |
| Cannot access from remote browser on network | Firewall blocking port 8090 or wrong IP address | Verify firewall allows port 8090: `sudo ufw allow 8090`. Use correct IP from `hostname -I` command |
| Video stream is laggy or frozen | Network issues or browser performance | Use Chrome or Edge browser. Access from a separate PC on the network rather than locally. Check network bandwidth |
| Analysis results in unexpected language | Model supports multilingual and detected language in prompt | Explicitly specify output language in prompt: "Answer in English: describe what you see" |
| pip install fails with dependency errors | Conflicting Python package versions | Try installing with `--user` flag: `pip install --user live-vlm-webui` |
| Command `live-vlm-webui` not found after install | Binary path not in PATH | Add `~/.local/bin` to PATH: `export PATH="$HOME/.local/bin:$PATH"` then run `source ~/.bashrc` |
| Camera works but no VLM analysis results appear, browser shows InvalidStateError | Accessing via SSH port forwarding from remote machine | WebRTC requires direct network connectivity and doesn't work through SSH tunnels (SSH only forwards TCP, WebRTC needs UDP). **Solution 1**: Access the web UI directly from a browser on the same network as the server. **Solution 2**: Use the server machine's browser directly. **Solution 3**: Use X11 forwarding (`ssh -X`) to display the browser remotely |

View File

@ -0,0 +1,222 @@
# Portfolio Optimization
> GPU-Accelerated portfolio optimization using cuOpt and cuML
## Table of Contents
- [Overview](#overview)
- [Instructions](#instructions)
- [Troubleshooting](#troubleshooting)
---
## Overview
## Basic idea
This playbook demonstrates an end-to-end GPU-accelerated workflow using NVIDIA cuOpt and NVIDIA cuML to solve large-scale portfolio optimization problems, using the Mean-CVaR (Conditional Value-at-Risk) model, in near real-time.
Portfolio Optimization (PO) involves solving high-dimensional, non-linear numerical optimization problems to balance risk and return. Modern portfolios often contain thousands of assets, making traditional CPU-based solvers too slow for advanced workflows. By moving the computational heavy lifting to the GPU, this solution dramatically reduces computation time.
## What you'll accomplish
You will implement a pipeline that provides tools for performance evaluation, strategy backtesting, benchmarking, and visualization. The workflow includes:
- **GPU-Accelerated Optimization:** Leveraging NVIDIA cuOpt LP/MILP solvers
- **Data-Driven Risk Modeling:** Implementing CVaR as a scenario-based risk measure that models tail risks without making assumptions about asset return distributions.
- **Scenario Generation:** Using GPU-accelerated Kernel Density Estimation (KDE) via NVIDIA cuML to model return distributions.
- **Real-World Constraint Management:** Implementing constraints including concentration limits, leverage constraints, turnover limits, and cardinality constraints.
- **Comprehensive Backtesting:** Evaluating portfolio performance with specific tools for testing rebalancing strategies.
## What to know before starting
- **Required Skills (you'll get it):**
- Basic with Terminal and Linux command line
- Basic understanding of Docker containers
- Basic knowledge of using Jupyter Notebooks and Jupyter Lab
- Basic Python knowledge
- Basic knowledge of data science and machine learning concepts
- Basic knowledge of what the stock market and stocks are
- **Optional Skills (you'll enjoy it):**
- Background in Financial Services, especially in quantatitve finance and portfolio management
- Moderate knowledge programming algorithms and strategies, in python, using machine learning concepts
- **Terms to know:**
- **CVaR vs. Mean-Variance:** Unlike traditional mean-variance models, this workflow uses Conditional Value-at-Risk (CVaR) to capture nuances of risk, specifically tail risk or scenario-specific stresses.
- **Linear Programming:** CVaR reformulates the risk-return tradeoff as a scenario-based linear program where the problem size scales with the number of scenarios, which is why GPU acceleration is critical.
- **Benchmarking:** The pipeline includes built-in tools to streamline the benchmarking process against standard CPU-based libraries to validate performance gains.
## Prerequisites
**Hardware Requirements:**
- NVIDIA Grace Blackwell GB10 Superchip System (DGX Spark)
- Minimum 40GB Unified memory free for docker container and GPU accelerated data processing
- At least 30GB available storage space for docker container and data files
- High speed internet connection recommended
**Software Requirements:**
- NVIDIA DGX OS with working NVIDIA and CUDA drivers
- Docker
- Git
## Ancillary files
All required assets can be found [in the Portfolio Optimization repository](https://github.com/NVIDIA/dgx-spark-playbooks/blob/main/nvidia/portfolio-optimization/assets/). In the running playbook, they will all be found under the `playbook` folder.
- `cvar_basic.ipynb` - Main playbook notebook.
- `/setup/README.md` - Quick Start Guide to the Playbook Environment.
- `/setup/start_playbook.sh` - Script to start the install of the playbook in a Docker container
- `/setup/setup_playbook.sh` - Configures the Docker container before user enters jupyterlab environment
- `/setup/pyproject.toml` - used as a lists of libraries that commands in setup_playbook will install into the playbook environment
- `cuDF, cuML, and cuGraph folders` - more example notebooks to continue your GPU Accelerated Data Science Journey. These will be part of the Docker Container when you start it.
## Time & risk
* **Estimated Time** ~20 minutes for first run
- Total Notebook Processing Time: Approximately 7 minutes for the full pipeline.
- **Risks:**
- Minimal, as this is run in a Docker container.
* **Rollback:** Stop the Docker container and remove the cloned repository to fully remove the installation.
* **Last Updated:** 1/05/2026
* First Publication
## Instructions
## Step 1. Verify your environment
Let's first verify that you have a working GPU, git, and Docker. Open up Terminal, then copy and paste in the below commands:
```bash
nvidia-smi
git --version
docker --version
```
- `nvidia-smi` will output information about your GPU. If it doesn't, your GPU is not properly configured.
- `git --version` will print something like `git version 2.43.0`. If you get an error saying that git is not installed, please reinstall it.
- `docker --version` will print something like `Docker version 28.3.3, build 980b856`. If you get an error saying that Docker is not installed, please reinstall it.
## Step 2. Installation
Open up Terminal, then copy and paste in the below commands:
```bash
git clone https://github.com/NVIDIA/dgx-spark-playbooks/nvidia/portfolio-optimization
cd dgx-spark-playbooks/nvidia/portfolio-optimization/assets
bash ./setup/start_playbook.sh
```
start_playbook.sh will:
1. pull the RAPIDS 25.10 Notebooks Docker container
2. build all the environments needed for the playbook in the container using `setup_playbook.sh`
3. start Jupyterlab
Please keep the Terminal window open while using the playbook.
You can access your Jupyterlab server in three ways
1. at `http://127.0.0.1:8888` if running locally on the DGX Spark.
2. at `http://<SPARK_IP>:8888` if using your DGX Spark headless over your network.
3. by creating an SSH tunnel using `ssh -L 8888:localhost:8888 username@spark-IP` in Terminal and the going to `http://127.0.0.1:8888` in your browser on your host machine
Once in Jupyterlab, you'll be greeted with a directory containing `cvar_basic.ipynb`, and the folders `cudf`, `cuml` and `cugraph`.
- `cvar_basic.ipynb` is the playbook notebook. You will want to open this by double clicking on the file.
- `cudf`, `cuml`, `cugraph` folders contain the standard RAPIDS library example notebooks to help you continue exploring.
- `playbook` contains the playbook files. The contents of this folder are read-only inside of a rootless Docker Container.
If you want to install any of the playbook notebooks on your own system, check out the readmes within the folder that accompanies the notebook
## Step 3. Run the notebook
Once in jupyterlab, you have to do is run the `cvar_basic.ipynb`.
Before your start running the cells in the notebook, **please change the kernel to "Portfolio Optimization" as per the instructions in the notebook.** Failure to do so will cause errors by the second code cell. If you started already, you will have to set it to the correct kernel, then restart the kernel, and try again.
You can use `Shift + Enter` to manually run each cell at your own pace, or `Run > Run All` to run all the cells.
Once you're done with exploring the `cvar_basic` notebook, you can explore other RAPIDS notebooks by going into the folders, selecting other notebooks, and doing the same thing.
## Step 4. Download your work
Since the docker container is not priviledged and cannot write back to the host system, you can use Jupyterlab to download any files you may want to keep once the docker container is shut down.
Simply right click the file you want, in the browser, and click `Download` in the drop down.
## Step 5. Cleanup
Once you have downloaded all your work, Go back to the Terminal window where you started running the playbook.
In the Terminal window:
1. Type `Ctrl + C`
2. Quickly either enter `y` and then hit `Enter` at the prompt or hit `Ctrl + C` again
3. The Docker container will proceed to shut down
> [!WARNING]
> This will delete ALL data that wasn't already downloaded from the Docker container. The browser window may still show cached files if it is still open.
## Step 6. Next Steps
Once you're comfortable with this foundational workflow, please explore these advanced portfolio optimization topics in any order at the **[NVIDIA AI Blueprints](https://github.com/NVIDIA-AI-Blueprints/quantitative-portfolio-optimization/)**:
* **[`efficient_frontier.ipynb`](https://github.com/NVIDIA-AI-Blueprints/quantitative-portfolio-optimization/tree/main/notebooks/efficient_frontier.ipynb)** - Efficient Frontier Analysis
This notebook demonstrates how to:
- Generate the efficient frontier by solving multiple optimization problems
- Visualize the risk-return tradeoff across different portfolio configurations
- Compare portfolios along the efficient frontier
- Leverage GPU acceleration to quickly compute multiple optimal portfolios
* **[`rebalancing_strategies.ipynb`](https://github.com/NVIDIA-AI-Blueprints/quantitative-portfolio-optimization/tree/main/notebooks/rebalancing_strategies.ipynb)** - Dynamic Portfolio Rebalancing
This notebook introduces dynamic portfolio management techniques:
- Time-series backtesting framework
- Testing various rebalancing strategies (periodic, threshold-based, etc.)
- Evaluating the impact of transaction costs on portfolio performance
- Analyzing strategy performance over different market conditions
- Comparing multiple rebalancing approaches
* If you'd further learn how to formulate portfolio optimization problems using similar riskreturn frameworks, check out the **[DLI course: Accelerating Portfolio Optimization](https://learn.nvidia.com/courses/course-detail?course_id=course-v1:DLI+S-DS-09+V1)**
## Step 7. Further Support
For questions or issues, please visit:
- [GitHub Issues](https://github.com/NVIDIA-AI-Blueprints/quantitative-portfolio-optimization/issues)
- [GitHub Discussions](https://github.com/NVIDIA-AI-Blueprints/quantitative-portfolio-optimization/discussions)
## Troubleshooting
<!--
TROUBLESHOOTING TEMPLATE: Although optional, this resource can significantly help users resolve common issues.
Replace all placeholder content in {} with your actual troubleshooting information.
Remove these comment blocks when you're done.
PURPOSE: Provide quick solutions to problems users are likely to encounter.
FORMAT: Use the table format for easy scanning. Add detailed notes when needed.
-->
| Symptom | Cause | Fix |
|---------|-------|-----|
| Docker is not found. | Docker may have been uninstalled, as it is preinstalled on your DGX Spark | Please install Docker using their convenience script here: `curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh`. You will be prompted for your password. |
| Docker command unexpectedly exits with "permissions" error | Your user is not part of the `docker` group | Open Terminal and run these commands: `sudo groupadd docker $$ sudo usermod -aG docker $USER`. You will be prompted for your password. Then, close the Terminal, open a new one, and try again |
| Docker container download, environment build, or data download fails | There was either a connectivity issue or a resource may be temporariliy unavailable. | You may need to try again later. If this persist, please reach out to us! |
<!--
Space reserved for some common known issues that might be relevant to your project. Assess potential consequences before changing or deleting.
-->
> [!NOTE]
> DGX Spark uses a Unified Memory Architecture (UMA), which enables dynamic memory sharing between the GPU and CPU.
> With many applications still updating to take advantage of UMA, you may encounter memory issues even when within
> the memory capacity of DGX Spark. If that happens, manually flush the buffer cache with:
```bash
sudo sh -c 'sync; echo 3 > /proc/sys/vm/drop_caches'
```
For latest known issues, please review the [DGX Spark User Guide](https://docs.nvidia.com/dgx/dgx-spark/known-issues.html).

View File

@ -0,0 +1,118 @@
# **Portfolio Optimization Notebook on DGX Spark**
___
## **Overview**
___
<br>
![arch_diagram](assets/arch_diagram.png)
**[`cvar_basic.ipynb`](cvar_basic.ipynb)** is a complete portfolio optimization walkthrough Jupyter notebook that demonstrates GPU-accelerated portfolio optimization techniques using the NVIDIA DGX Spark. It primarily uses the new purpose built library **[cuFolio](https://www.nvidia.com/en-us/on-demand/session/gtc25-dlit71690/)**, which is built upon NVIDIA's **[cuOpt](https://github.com/NVIDIA/cuopt)**, and NVIDIA RAPIDS' **[cuML](https://github.com/rapidsai/cuml)** and **[cuGraph](https://github.com/rapidsai/cugraph)**.
## **[CLICK HERE TO GET STARTED](cvar_basic.ipynb)**
This notebook's step-by-step walkthrough covers:
- Data preparation and preprocessing
- Scenario generation
- **[Mean-CVaR (Conditional Value-at-Risk)](https://www.youtube.com/shorts/9u-VrCyneM4)** portfolio optimization
- Implementing real-world constraints (concentration limits, leverage, turnover)
- Portfolio construction and analysis
- Performance evaluation and backtesting
If you'd like a deep dive into the notebook itself, **[check out the blog: Accelerating Real-Time Financial Decisions with Quantitative Portfolio Optimization](https://developer.nvidia.com/blog/accelerating-real-time-financial-decisions-with-quantitative-portfolio-optimization/)**
**Be sure to run the notebook using the Portfolio Optimization Kernel!** Instructions will be at the start of the notebook.
Downloaded stock data will be stored in the `data`. Calcuated results are saved in the `results` folder.
![optimization](assets/cvar.png)
<br>
<br>
___
## **DIY Installation**
___
<br>
Installing the Portfolio Optimization packages can be moderately complexity, so we created some scripts to make it easy to build the Python environment.
You will need RAPIDS 25.10 and Jupyter installed using either `pip`/`uv` or `docker`. Please refer to the [RAPIDS Installation Selector](https://docs.rapids.ai/install/#selector) for more details.
Examples:
```bash
pip install "cudf-cu13==25.10.*" "cuml-cu13==25.10.*" jupyterlab
```
or
```bash
docker run --gpus all --pull always --rm -it \
--shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 \
-p 8888:8888 -p 8787:8787 -p 8786:8786 \
nvcr.io/nvidia/rapidsai/notebooks:25.10-cuda13-py3.13
```
Once RAPIDS is installed, please run the commands below to install the Portfolio Optimization Jupyter Kernel. If you are in Docker, please run these inside of the Docker environment.
```bash
cd Stock_Portfolio_Optimization
# Install uv (if not already installed)
curl -LsSf https://astral.sh/uv/install.sh | sh
# To add $HOME/.local/bin to your PATH, either restart your shell or run:
source $HOME/.local/bin/env
# Install with CUDA-specific dependencies
uv sync --extra cuda13
# Optional: Install development tools
# uv sync --extra cuda13 --extra dev
# Create a Jupyter kernel for this environment
uv run python -m ipykernel install --user --name=portfolio-opt --display-name "Portfolio Optimization"
# Launch Jupyter Lab (if necessary)
uv run jupyter lab --no-browser --NotebookApp.token=''
```
<br>
<br>
___
## **Next Steps**
___
<br>
### **Advanced Workflows at NVIDIA AI Blueprints**
Once you're comfortable with the basic workflow, explore these advanced topics in any order at the **[NVIDIA AI Blueprints](https://github.com/NVIDIA-AI-Blueprints/quantitative-portfolio-optimization/)**:
#### [`efficient_frontier.ipynb`](https://github.com/NVIDIA-AI-Blueprints/quantitative-portfolio-optimization/tree/main/notebooks/efficient_frontier.ipynb) - Efficient Frontier Analysis
This notebook demonstrates how to:
- Generate the **[efficient frontier](https://www.youtube.com/shorts/apvVgwg06hw)** by solving multiple optimization problems
- Visualize the risk-return tradeoff across different portfolio configurations
- Compare portfolios along the efficient frontier
- Leverage GPU acceleration to quickly compute multiple optimal portfolios
#### [`rebalancing_strategies.ipynb`](https://github.com/NVIDIA-AI-Blueprints/quantitative-portfolio-optimization/tree/main/notebooks/rebalancing_strategies.ipynb) - Dynamic Portfolio Rebalancing
This notebook introduces dynamic portfolio management techniques:
- Time-series backtesting framework
- Testing various rebalancing strategies (periodic, threshold-based, etc.)
- Evaluating the impact of transaction costs on portfolio performance
- Analyzing strategy performance over different market conditions
- Comparing multiple rebalancing approaches
<br>
<br>
___
## **Additional Resources**
___
<br>
If you'd further learn how to formulate portfolio optimization problems using similar riskreturn frameworks, check out the **[DLI course: Accelerating Portfolio Optimization](https://learn.nvidia.com/courses/course-detail?course_id=course-v1:DLI+S-DS-09+V1)**
For questions or issues, please visit:
- [GitHub Issues](https://github.com/NVIDIA-AI-Blueprints/quantitative-portfolio-optimization/issues)
- [GitHub Discussions](https://github.com/NVIDIA-AI-Blueprints/quantitative-portfolio-optimization/discussions)

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,370 @@
# Contributing to NVIDIA Quantitative Portfolio Optimization developer example
Thank you for your interest in contributing to Quantitative Portfolio Optimization developer example! This document provides guidelines and instructions for contributing to the project.
## Table of Contents
- [Code of Conduct](#code-of-conduct)
- [Ways to Contribute](#ways-to-contribute)
- [Development Setup](#development-setup)
- [Coding Standards](#coding-standards)
- [Testing](#testing)
- [Submitting Changes](#submitting-changes)
- [Issue Reporting](#issue-reporting)
- [Getting Help](#getting-help)
## Code of Conduct
We are committed to providing a welcoming and inclusive environment for all contributors. Please be respectful and professional in all interactions.
## Ways to Contribute
There are many ways to contribute to Quantitative Portfolio Optimization developer example:
- **Report bugs**: If you find a bug, please open an issue with detailed information
- **Suggest enhancements**: Have an idea for a new feature? Let us know!
- **Improve documentation**: Help us make the docs clearer and more comprehensive
- **Submit code**: Fix bugs, implement features, or improve performance
- **Share examples**: Contribute notebooks, examples, or use cases
## Development Setup
### Prerequisites
- Python 3.10 or higher
- CUDA-capable GPU (for GPU-accelerated features)
- NVIDIA PyTorch container or equivalent CUDA environment
### Setting Up Your Development Environment
1. **Fork and Clone the Repository**
```bash
git clone https://github.com/your-username/cufolio.git
cd cufolio
```
2. **Install uv (if not already installed)**
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
For Windows, use:
```powershell
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
```
3. **Install Development Dependencies**
```bash
uv sync --extra dev
```
This automatically creates a virtual environment and installs the project in editable mode along with development tools like `black`, `isort`, `flake8`, and `pre-commit`.
4. **Set Up Pre-commit Hooks**
```bash
uv run pre-commit install
```
This will automatically run code formatting and linting checks before each commit.
### Docker Development (Recommended)
For a consistent development environment with all GPU dependencies:
```bash
docker run --gpus all -it --rm -v $(pwd):/workspace nvcr.io/nvidia/pytorch:25.08-py3
cd /workspace
curl -LsSf https://astral.sh/uv/install.sh | sh
uv sync --extra dev
```
## Coding Standards
### Python Style Guide
We follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) with the following specifications:
- **Line length**: 88 characters (Black default)
- **String quotes**: Use double quotes for strings
- **Import ordering**: Managed by `isort` with Black profile
### Code Formatting
All code must be formatted with:
- **Black**: For consistent code formatting
- **isort**: For import statement ordering
Run formatters before committing:
```bash
uv run black .
uv run isort .
```
Or let pre-commit hooks handle it automatically.
### Linting
We use `flake8` for linting. Run it with:
```bash
uv run flake8 src/
```
### Documentation
- **Docstrings**: Use Google-style docstrings for all public functions, classes, and modules
- **Type hints**: Include type hints for function parameters and return values
- **Comments**: Write clear comments explaining complex logic
Example:
```python
def optimize_portfolio(
returns: np.ndarray,
risk_measure: str = "cvar",
confidence_level: float = 0.95
) -> dict:
"""
Optimize portfolio allocation using specified risk measure.
Args:
returns: Historical returns data as numpy array
risk_measure: Risk measure to use ('cvar' or 'variance')
confidence_level: Confidence level for CVaR calculation
Returns:
Dictionary containing optimal weights and performance metrics
Raises:
ValueError: If risk_measure is not supported
"""
# Implementation
pass
```
## Testing
### Running Tests
We encourage comprehensive testing of all new features and bug fixes.
```bash
# Run all tests
uv run pytest
# Run specific test file
uv run pytest tests/test_cvar_optimizer.py
# Run with coverage
uv run pytest --cov=src --cov-report=html
```
### Writing Tests
- Place test files in the appropriate `tests/` directory
- Name test files with `test_` prefix
- Use descriptive test function names
- Include both unit tests and integration tests
- Test edge cases and error conditions
### GPU Testing
For GPU-specific features, ensure tests can run on both CPU and GPU:
```python
import pytest
from cuml.common import has_cuda
@pytest.mark.skipif(not has_cuda(), reason="CUDA not available")
def test_gpu_optimization():
# GPU-specific test
pass
```
## Submitting Changes
### Branch Naming
Use descriptive branch names following this pattern:
- `feature/description` - for new features
- `bugfix/description` - for bug fixes
- `docs/description` - for documentation updates
- `refactor/description` - for code refactoring
### Commit Messages
Write clear, concise commit messages:
```
Short summary (50 chars or less)
More detailed explanation if needed. Wrap at 72 characters.
Explain what changes were made and why.
- Bullet points are okay
- Use present tense ("Add feature" not "Added feature")
- Reference issues: "Fixes #123" or "Relates to #456"
```
### Pull Request Process
1. **Update your fork** with the latest changes from main:
```bash
git fetch upstream
git rebase upstream/main
```
2. **Run all checks** before submitting:
```bash
uv run black .
uv run isort .
uv run flake8 src/
uv run pytest
```
3. **Create a Pull Request** with:
- Clear title describing the change
- Detailed description of what and why
- Link to related issues
- Screenshots or examples if applicable
4. **Address review feedback** promptly and professionally
5. **Ensure CI passes** - all automated checks must pass
### Pull Request Checklist
Before submitting, ensure:
- [ ] Code follows style guidelines (Black, isort, flake8)
- [ ] All tests pass
- [ ] New tests added for new features
- [ ] Documentation updated (docstrings, README, etc.)
- [ ] Commit messages are clear and descriptive
- [ ] No merge conflicts with main branch
- [ ] Pre-commit hooks pass
## Issue Reporting
### Bug Reports
When reporting bugs, please include:
- **Description**: Clear description of the bug
- **Steps to reproduce**: Exact steps to reproduce the issue
- **Expected behavior**: What should happen
- **Actual behavior**: What actually happens
- **Environment**: OS, Python version, CUDA version, GPU model
- **Code snippet**: Minimal reproducible example
- **Error messages**: Full error traceback
Use this template:
```markdown
**Bug Description**
A clear description of the bug.
**To Reproduce**
1. Step one
2. Step two
3. ...
**Expected Behavior**
What you expected to happen.
**Actual Behavior**
What actually happened.
**Environment**
- OS: [e.g., Ubuntu 22.04]
- Python: [e.g., 3.10]
- CUDA: [e.g., 12.2]
- GPU: [e.g., A100]
- NVIDIA Quantitative Portfolio Optimization developer example version: [e.g., 25.10]
**Additional Context**
Any other relevant information.
```
### Feature Requests
When suggesting features:
- Describe the problem it solves
- Explain the proposed solution
- Consider alternative approaches
- Note any breaking changes
## Getting Help
- **Issues**: Open an issue for bugs or questions
- **Discussions**: Use GitHub Discussions for general questions
- **Documentation**: Check the README and module-specific docs
## Signing Your Work
We require that all contributors "sign-off" on their commits. This certifies that the contribution is your original work, or you have rights to submit it under the same license, or a compatible license.
Any contribution which contains commits that are not Signed-Off will not be accepted.
To sign off on a commit you simply use the `--signoff` (or `-s`) option when committing your changes:
```bash
$ git commit -s -m "Add cool feature."
```
This will append the following to your commit message:
```
Signed-off-by: Your Name <your@email.com>
```
## Developer Certificate of Origin
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
1 Letterman Drive
Suite D4700
San Francisco, CA, 94129
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
```
```
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2020 NVIDIA Corporation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,117 @@
Quantitative Portfolio Optimization developer example Licenses
=================================
This file contains third-party license information for software packages used in this project.
The licenses below apply to one or more packages included in this project. For each license,
we list the packages that are distributed under it and include the full license text.
------------------------------------------------------------
Apache License, Version 2.0
------------------------------------------------------------
The Apache License, Version 2.0 is a permissive license that also provides an express grant of patent rights.
Packages under the Apache License, Version 2.0:
- CVXPY - Copyright the CVXPY authors
- yfinance - Copyright 2017-2025 Ran Aroussi.
Full Apache License, Version 2.0 Text:
--------------------------------------------------
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined in this document.
"Licensor" shall mean the copyright owner or entity
authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work.
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the modifications represent, as a whole, an original work of authorship.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work.
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell,
sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications,
and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark,
and attribution notices from the Source form of the Work; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute
must include a readable copy of the attribution notices contained within such NOTICE file.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution submitted for inclusion in the Work shall be under the terms and conditions of this License.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor.
7. Disclaimer of Warranty.
The Work is provided on an "AS IS" basis, without warranties or conditions of any kind, either express or implied.
8. Limitation of Liability.
In no event shall any Contributor be liable for any damages arising from the use of the Work.
END OF TERMS AND CONDITIONS
--------------------------------------------------
------------------------------------------------------------
BSD License
------------------------------------------------------------
The BSD License is a permissive license
Packages under BSD License
- numpy - Copyright (c) 2005-2025, NumPy Developers.
- pandas - Copyright (c) 2008-2011, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team.
- scikit-learn Copyright 2007 - 2025, scikit-learn developers
- seaborn - Copyright 2012-2024, Michael Waskom.
Full BSD License Text:
--------------------------------------------------
BSD 2- Clause
Copyright <YEAR> <COPYRIGHT HOLDER>
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
BSD 3-Clause
Copyright <YEAR> <COPYRIGHT HOLDER>
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------
END OF THIRD-PARTY LICENSES

View File

@ -0,0 +1,24 @@
## Security
NVIDIA is dedicated to the security and trust of our software products and services, including all source code repositories managed through our organization.
If you need to report a security issue, please use the appropriate contact points outlined below. **Please do not report security vulnerabilities through GitHub.**
## Reporting Potential Security Vulnerability in an NVIDIA Product
To report a potential security vulnerability in any NVIDIA product:
- Web: [Security Vulnerability Submission Form](https://www.nvidia.com/object/submit-security-vulnerability.html)
- E-Mail: psirt@nvidia.com
- We encourage you to use the following PGP key for secure email communication: [NVIDIA public PGP Key for communication](https://www.nvidia.com/en-us/security/pgp-key)
- Please include the following information:
- Product/Driver name and version/branch that contains the vulnerability
- Type of vulnerability (code execution, denial of service, buffer overflow, etc.)
- Instructions to reproduce the vulnerability
- Proof-of-concept or exploit code
- Potential impact of the vulnerability, including how an attacker could exploit the vulnerability
While NVIDIA currently does not have a bug bounty program, we do offer acknowledgement when an externally reported security issue is addressed under our coordinated vulnerability disclosure policy. Please visit our [Product Security Incident Response Team (PSIRT)](https://www.nvidia.com/en-us/security/psirt-policies/) policies page for more information.
## NVIDIA Product Security
For all security-related concerns, please visit NVIDIA's Product Security portal at https://www.nvidia.com/en-us/security

View File

@ -0,0 +1,76 @@
[project]
name = "cufolio"
version = "25.10"
description = "Quantitative Portfolio Optimization developer example"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"numpy==2.2.6",
"pandas==2.3.2",
"cvxpy==1.7.3",
"scikit-learn==1.7.1",
"seaborn==0.13.2",
"yfinance>=0.2.0",
]
[project.optional-dependencies]
dev = [
"black==25.9.0",
"isort",
"flake8",
"pre-commit==4.3.0",
]
cuda12 = [
"cuml-cu12==25.10.*",
"cuopt-cu12==25.10.*",
]
cuda13 = [
"cuml-cu13==25.10.*",
"cuopt-cu13==25.10.*",
]
[tool.black]
line-length = 88
target-version = ['py310']
[tool.isort]
profile = "black"
line_length = 88
[tool.setuptools]
packages = ["cufolio"]
py-modules = []
[tool.setuptools.package-dir]
cufolio = "src"
[tool.setuptools.package-data]
"*" = ["*.csv", "*.pkl", "*.parquet"]
[tool.uv]
managed = true
conflicts = [
[
{ extra = "cuda12" },
{ extra = "cuda13" },
],
]
[[tool.uv.index]]
name = "nvidia"
url = "https://pypi.nvidia.com"
[tool.uv.sources]
cuml-cu12 = { index = "nvidia" }
cuopt-cu12 = { index = "nvidia" }
cuml-cu13 = { index = "nvidia" }
cuopt-cu13 = { index = "nvidia" }
[build-system]
requires = ["setuptools>=64", "wheel"]
build-backend = "setuptools.build_meta"
[dependency-groups]
dev = [
"ipykernel>=7.1.0",
]

View File

@ -0,0 +1,30 @@
#/bin/bash
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
uv --version
cp -r ~/notebooks/playbook/setup ~/notebooks/setup/
cd /home/rapids/notebooks/setup
# Install cuOpt kernel
echo "Installing cuOpt Portfolio Optimization Kernel."
python -m venv .venv
source .venv/bin/activate
# Install with all dependencies using uv for CUDA 13 (DGX SPark CUDA version)
export PATH="$HOME/.local/bin:$PATH"
uv sync --extra cuda13 --locked --no-dev
# Install Jupyter and JupyterLab
uv pip install ipykernel
# Create a Jupyter kernel for this environment
uv run python -m ipykernel install --user --name=portfolio-opt --display-name "Portfolio Optimization"
# Copy necessaru playbook files
cp -r ~/notebooks/playbook/assets ~/notebooks/assets
cp ~/notebooks/playbook/README.md ~/notebooks/START_HERE.md
cp ~/notebooks/playbook/cvar_basic.ipynb ~/notebooks/cvar_basic.ipynb
set -m
# Start the primary process and put it in the background
jupyter-lab --notebook-dir=/home/rapids/notebooks --ip=0.0.0.0 --no-browser --NotebookApp.token='' --NotebookApp.allow_origin='*'

View File

@ -0,0 +1,23 @@
# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""cufolio: GPU-accelerated portfolio optimization with cuOpt."""
version = "1.0.0"
from .cvar_data import CvarData
from .cvar_parameters import CvarParameters
__all__ = ["CvarData", "CvarParameters", "version"]

View File

@ -0,0 +1,633 @@
# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Portfolio backtesting and performance evaluation framework.
Provides tools for backtesting portfolio strategies against historical data
and benchmarks, with support for various return metrics and scenario generation
methods including historical, KDE, and Gaussian simulation.
"""
import os
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn.neighbors import KernelDensity
from .portfolio import Portfolio
class portfolio_backtester:
"""
Portfolio backtesting framework for performance evaluation against benchmarks.
Supports multiple testing methods including historical data, KDE simulation,
and Gaussian simulation. Calculates key performance metrics including
Sharpe ratio, Sortino ratio, and maximum drawdown.
Attributes
----------
test_portfolio : Portfolio
Portfolio to be tested
test_method : str
Method for generating return scenarios
benchmark_portfolios : list
List of benchmark Portfolio objects for comparison
risk_free_rate : float
Risk-free rate for excess return calculations
"""
def __init__(
self,
test_portfolio,
returns_dict,
risk_free_rate=0.0,
test_method="historical",
benchmark_portfolios=None,
):
"""
Initialize portfolio backtester with test portfolio and return data.
Parameters
----------
test_portfolio : Portfolio
Portfolio object to backtest
returns_dict : dict
Dictionary containing return data with keys: 'return_type', 'returns',
'dates', 'mean', 'covariance', 'tickers'
risk_free_rate : float, default 0.0
Risk-free rate for Sharpe and Sortino ratio calculations
test_method : str, default "historical"
Method for return scenarios: "historical", "kde_simulation",
or "gaussian_simulation"
benchmark_portfolios : list, pd.DataFrame, or None, default None
Benchmark portfolios for comparison. If None, uses equal-weight portfolio
"""
self.test_portfolio = test_portfolio
self.test_method = test_method.lower()
self.benchmark_portfolios = self._generate_benchmark_portfolios(
benchmark_portfolios
)
self._dates = returns_dict["dates"]
self._return_type = returns_dict["return_type"]
self._return_mean = returns_dict["mean"]
self._covariance = returns_dict["covariance"]
self._returns = returns_dict["returns"]
self._R = self._get_return_scenarios()
if self._return_type == "LOG":
self.risk_free_rate = np.log(1 + risk_free_rate)
elif self._return_type == "LINEAR":
self.risk_free_rate = risk_free_rate
self._backtest_column_names = [
"returns",
"cumulative returns",
"portfolio name",
"mean portfolio return",
"sharpe",
"sortino",
"max drawdown",
]
def _generate_benchmark_portfolios(self, benchmark_portfolios):
"""
Generate benchmark portfolios from input specification.
Parameters
----------
benchmark_portfolios : list, pd.DataFrame, or None
Benchmark portfolio specification
Returns
-------
list
List of Portfolio objects to use as benchmarks
Raises
------
ValueError
If input format is not supported
"""
if benchmark_portfolios is None: # when benchmark_portfolios is not provided,
# default to equal-weight portfolio
return self._generate_equal_weights_portfolio(
self.test_portfolio.tickers, self.test_portfolio.cash
)
elif isinstance(
benchmark_portfolios, pd.DataFrame
): # if custom, then set to input portfolios stored in optimization problem
return benchmark_portfolios["portfolio"].to_list()
elif isinstance(benchmark_portfolios, list):
return benchmark_portfolios
else:
raise ValueError(
"Unacceptable input format.\n Please provide the portfolios "
"in compliant format (DataFrame)"
)
def _generate_equal_weights_portfolio(self, tickers, cash):
"""
Create equal-weight benchmark portfolio.
Parameters
----------
tickers : list
List of asset ticker symbols
cash : float
Cash allocation for the portfolio
Returns
-------
list
List containing single equal-weight Portfolio object
"""
n_assets = len(tickers)
weights = (np.ones(n_assets) - cash) / n_assets
eq_weight_portfolio = Portfolio(
name="equal-weight", tickers=tickers, weights=weights, cash=cash
)
return [eq_weight_portfolio]
def _generate_simulated_scenarios(self, generation_method="kde", num_scen=5000):
"""
Generate simulated return scenarios using specified method.
Parameters
----------
generation_method : str, default "kde"
Method for scenario generation: "gaussian" or "kde"
num_scen : int, default 5000
Number of scenarios to generate
Returns
-------
np.ndarray
Simulated return scenarios with shape (num_scen, n_assets)
Raises
------
NotImplementedError
If generation method is not supported
"""
generation_method = str(generation_method).lower()
if generation_method == "gaussian": # fit Gaussian
R = np.random.multivariate_normal(
self._return_mean, self._covariance, size=num_scen
)
elif generation_method == "kde": # kde distribution
R = self._generate_samples_kde(self._returns, num_scen, bandwidth=0.005)
else:
raise NotImplementedError("Invalid Generation Method!")
return R
def _generate_samples_kde(
self, returns_data, num_scen, bandwidth, kernel="gaussian"
):
"""
Generate return samples using Kernel Density Estimation.
Parameters
----------
returns_data : np.ndarray
Historical return data for fitting KDE
num_scen : int
Number of samples to generate
bandwidth : float
Bandwidth parameter for KDE
kernel : str, default "gaussian"
Kernel type for density estimation
Returns
-------
np.ndarray
Generated return samples with shape (num_scen, n_assets)
"""
kde = KernelDensity(kernel=kernel, bandwidth=bandwidth).fit(returns_data)
new_samples = kde.sample(num_scen)
return new_samples
def _get_return_scenarios(self):
"""
Generate return scenarios based on specified test method.
Returns
-------
np.ndarray
Return scenarios for backtesting with shape (n_scenarios, n_assets)
Raises
------
NotImplementedError
If test method is not supported
"""
if self.test_method == "historical":
R = self._returns
elif self.test_method == "kde_simulation":
R = self._generate_simulated_scenarios(generation_method="kde")
elif self.test_method == "gaussian_simulation":
R = self._generate_simulated_scenarios(generation_method="gaussian")
else:
raise NotImplementedError("invalid test method!")
return R
def backtest_against_benchmarks(
self,
plot_returns=False,
ax=None,
cut_off_date=None,
title=None,
save_plot=False,
results_dir="results",
):
"""
Backtest portfolio against benchmark portfolios and optionally plot results.
Parameters
----------
plot_returns : bool, default False
Whether to create cumulative returns plot
ax : matplotlib.axes.Axes, optional
Existing axes to plot on. If None, creates new figure
cut_off_date : str, optional
Date to mark with vertical line on plot
title : str, optional
Title for the plot
save_plot : bool, default False
Whether to save the plot to the results directory
results_dir : str, default "results"
Directory path where plots will be saved
Returns
-------
tuple
(backtest_results, ax) where backtest_results is pd.DataFrame
with performance metrics and ax is the matplotlib axes
"""
backtest_results = pd.DataFrame({}, columns=self._backtest_column_names)
# backtest optimal portfolio
backtest_results = pd.concat(
[backtest_results, self.backtest_single_portfolio(self.test_portfolio)]
)
for portfolio in self.benchmark_portfolios:
result = self.backtest_single_portfolio(portfolio)
backtest_results = pd.concat([backtest_results, result], ignore_index=True)
backtest_results.set_index("portfolio name", inplace=True)
if plot_returns:
colors = {
"frontier": "#88CEE6",
"benchmark": ["#F6C8A8", "#F18F01", "#C73E1D"],
"assets": "#7209B7",
"custom": "#F72585",
"background": "#FAFAFA",
"grid": "#E0E0E0",
}
# Apply professional styling
plt.style.use("seaborn-v0_8-whitegrid")
sns.set_context("paper", font_scale=0.9)
if ax is None:
fig, ax = plt.subplots(
figsize=(12, 8), dpi=300, facecolor=colors["background"]
)
ax.set_facecolor(colors["background"])
# Prepare data for plotting
cumulative_returns_dataframe = pd.DataFrame(
[], index=pd.to_datetime(self._dates), columns=backtest_results.index
)
for ptf_name, row in backtest_results.iterrows():
cumulative_returns = row["cumulative returns"]
cumulative_returns_dataframe[ptf_name] = cumulative_returns
# Plot each portfolio with consistent colors
portfolio_names = list(backtest_results.index)
for i, ptf_name in enumerate(portfolio_names):
if ptf_name == self.test_portfolio.name:
# Test portfolio gets frontier color
color = colors["frontier"]
linewidth = 2.5
alpha = 0.9
zorder = 3
elif "equal-weight" in ptf_name.lower():
# Equal-weight benchmark gets same color as buy & hold
# in rebalance.py
color = colors["benchmark"][1]
linewidth = 2
alpha = 0.8
zorder = 2
else:
# Other benchmarks get rotating benchmark colors
color_idx = (i - 1) % len(colors["benchmark"])
color = colors["benchmark"][color_idx]
linewidth = 1
alpha = 0.8
zorder = 2
ax.plot(
cumulative_returns_dataframe.index,
cumulative_returns_dataframe[ptf_name],
linewidth=linewidth,
color=color,
label=ptf_name,
alpha=alpha,
zorder=zorder,
)
# subtle shading under the test portfolio line
test_portfolio_data = cumulative_returns_dataframe[self.test_portfolio.name]
ax.fill_between(
test_portfolio_data.index,
test_portfolio_data.values,
alpha=0.1,
color=colors["frontier"],
zorder=1,
)
# Dynamically adjust y-axis range to zoom into data with padding
all_values = []
for col in cumulative_returns_dataframe.columns:
all_values.extend(cumulative_returns_dataframe[col].values)
y_min = min(all_values)
y_max = max(all_values)
y_range = y_max - y_min
padding = y_range * 0.05 # 5% padding on top and bottom
ax.set_ylim(y_min - padding, y_max + padding)
# Set labels and formatting
ax.set_xlabel("Date", fontsize=10)
ax.set_ylabel(
f"Cumulative {self._return_type.lower()} returns", fontsize=10
)
if title:
ax.set_title(title, fontsize=11, pad=15)
# Format legend with smaller font
ax.legend(
loc="upper left", frameon=True, fancybox=True, shadow=True, fontsize=8
)
# Rotate x-axis dates to avoid overlapping
plt.xticks(rotation=45, ha="right", fontsize=9)
plt.yticks(fontsize=9)
# Set grid style
ax.grid(True, alpha=0.3, color=colors["grid"])
ax.set_axisbelow(True)
if cut_off_date is not None:
cut_off_date = pd.to_datetime(cut_off_date)
ax.axvline(
x=cut_off_date,
color="lightgray",
linestyle="--",
linewidth=1.5,
alpha=0.6,
label="Cut-off Date",
)
# Adjust layout to prevent clipping of rotated labels
plt.tight_layout()
# Save plot if requested
if save_plot:
# Create results directory if it doesn't exist
os.makedirs(results_dir, exist_ok=True)
# Generate filename based on test portfolio and date range
portfolio_name = self.test_portfolio.name.replace(" ", "_").lower()
# Handle both datetime and string date formats
if len(self._dates) > 0:
try:
# Try to use strftime if it's a datetime object
start_date = self._dates[0].strftime("%Y%m%d")
end_date = self._dates[-1].strftime("%Y%m%d")
except AttributeError:
# If it's a string, convert to datetime first
start_date = pd.to_datetime(self._dates[0]).strftime("%Y%m%d")
end_date = pd.to_datetime(self._dates[-1]).strftime("%Y%m%d")
else:
start_date = "unknown"
end_date = "unknown"
test_method = self.test_method.replace("_", "")
filename = (
f"backtest_{portfolio_name}_{test_method}_"
f"{start_date}-{end_date}.png"
)
filepath = os.path.join(results_dir, filename)
# Save with high quality
plt.savefig(
filepath,
dpi=300,
bbox_inches="tight",
facecolor="white",
edgecolor="none",
)
print(f"Backtest plot saved: {filepath}")
return backtest_results, ax
def backtest_single_portfolio(self, portfolio):
"""
Run backtest for a single portfolio.
Parameters
----------
portfolio : Portfolio
Portfolio object to backtest
Returns
-------
pd.DataFrame
Single-row DataFrame with backtest results and performance metrics
Raises
------
NotImplementedError
If return type is not supported
"""
if self._return_type == "LOG" or self._return_type == "LINEAR":
# compute Sharpe Ratio, Sortino Ratio, and Max Drawdown (MDD)
portfolio_returns = self._compute_portfolio_returns_with_cash(
portfolio.weights, portfolio.cash
)
backtest_result = self._compute_return_metrics(
portfolio.name, portfolio_returns, portfolio.cash
)
else:
raise NotImplementedError("Return type not supported yet!")
return backtest_result
def _compute_return_metrics(self, portfolio_name, returns, cash):
"""
Calculate portfolio performance metrics.
Parameters
----------
portfolio_name : str
Name of the portfolio
returns : pd.Series or np.ndarray
Portfolio returns time series
cash : float
Cash allocation in the portfolio
Returns
-------
pd.DataFrame
Single-row DataFrame with performance metrics including
Sharpe ratio, Sortino ratio, and maximum drawdown
"""
mean_return = np.mean(returns) + cash * self.risk_free_rate
excess_returns = returns - self.risk_free_rate
if self._return_type == "LINEAR":
cumulative_returns = np.cumsum(returns)
elif self._return_type == "LOG":
cumulative_returns = np.exp(np.cumsum(returns))
sharpe = self.sharpe_ratio(excess_returns)
sortino = self.sortino_ratio(excess_returns)
mdd = self.max_drawdown(cumulative_returns)
result = pd.Series(
[
returns.to_numpy(),
cumulative_returns.to_numpy(),
portfolio_name,
mean_return,
sharpe,
sortino,
mdd,
],
index=self._backtest_column_names,
)
return result.to_frame().T
def _compute_portfolio_returns_with_cash(self, weights, cash):
"""
Calculate portfolio returns including cash allocation.
Parameters
----------
weights : np.ndarray
Asset weights in the portfolio
cash : float
Cash allocation earning risk-free rate
Returns
-------
np.ndarray
Portfolio returns time series
"""
return self._R @ weights + self.risk_free_rate * cash
def sharpe_ratio(self, excess_returns):
"""
Calculate annualized Sharpe ratio.
Parameters
----------
excess_returns : np.ndarray
Excess returns over risk-free rate
Returns
-------
float
Annualized Sharpe ratio
"""
mean_excess_return = np.mean(excess_returns)
std_dev_excess_return = np.std(excess_returns)
sharpe_ratio = mean_excess_return / std_dev_excess_return * np.sqrt(252)
return sharpe_ratio
def sortino_ratio(self, excess_returns):
"""
Calculate annualized Sortino ratio.
Parameters
----------
excess_returns : np.ndarray
Excess returns over risk-free rate
Returns
-------
float
Annualized Sortino ratio
"""
mean_excess_return = np.mean(excess_returns)
downside_returns = excess_returns[excess_returns < 0]
downside_deviation = np.std(downside_returns)
sortino_ratio = mean_excess_return / downside_deviation * np.sqrt(252)
return sortino_ratio
def max_drawdown(self, cumulative_returns):
"""
Calculate maximum drawdown from cumulative returns.
Parameters
----------
cumulative_returns : np.ndarray
Cumulative returns time series
Returns
-------
float
Maximum drawdown as a decimal (e.g., 0.20 for 20% drawdown)
"""
# Convert log returns to cumulative portfolio values
# Initial portfolio value (assuming it starts at 1 for simplicity)
initial_portfolio_value = 1
portfolio_values = initial_portfolio_value * cumulative_returns
# Compute the running maximum
running_max = np.maximum.accumulate(portfolio_values)
# Compute the max drawdown
drawdown = (running_max - portfolio_values) / running_max
max_drawdown = np.max(drawdown)
return max_drawdown

View File

@ -0,0 +1,122 @@
# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Base optimization classes and utilities for portfolio optimization.
Provides abstract base classes and common functionality shared across
different optimization algorithms, including weight constraint handling
and portfolio state management.
"""
import numpy as np
class BaseOptimizer:
"""
Base class for portfolio optimization algorithms.
Provides common functionality for different optimization methods including
weight constraint handling and portfolio state management.
Attributes
----------
returns_dict : dict
Dictionary containing return data and asset information
tickers : list
Asset ticker symbols
n_assets : int
Number of assets in the portfolio
risk_measure : str
Risk measure type (e.g., "CVaR", "variance")
weights_previous : np.ndarray
Previous portfolio weights for turnover calculations
"""
def __init__(self, returns_dict, weights_previous, risk_measure):
"""
Initialize base optimizer with return data and portfolio state.
Parameters
----------
returns_dict : dict
Dictionary containing asset returns data and tickers
weights_previous : array-like or None
Previous portfolio weights. If None or empty, creates uniform weights
risk_measure : str
Risk measure identifier (e.g., "CVaR", "variance")
"""
self.returns_dict = returns_dict
self.tickers = returns_dict["tickers"]
self.n_assets = len(self.tickers)
self.risk_measure = risk_measure
if not weights_previous: # (n_assets,) array of existing portfolio weights;
# create uniform distributed weights if weights_previous not exist
self.weights_previous = np.ones(self.n_assets) / self.n_assets
else:
self.weights_previous = weights_previous
def _update_weight_constraints(self, weight_constraints):
"""
Convert weight constraints to numpy array format.
Handles multiple input formats for weight constraints:
- numpy array: used directly
- dict: maps ticker names to constraint values
- float: uniform constraint for all assets
Parameters
----------
weight_constraints : np.ndarray, dict, or float
Weight constraint specification in various formats
Returns
-------
np.ndarray
Weight constraints as numpy array (length n_assets)
Raises
------
ValueError
If constraint format is invalid or missing ticker specifications
"""
# if numpy array, then use the array
if isinstance(weight_constraints, np.ndarray):
updated_weight_constraints = weight_constraints
# if dict, then convert to numpy array based on the tickers
elif isinstance(weight_constraints, dict):
updated_weight_constraints = np.zeros(self.n_assets)
for ticker_idx, ticker in enumerate(self.tickers):
if ticker in weight_constraints.keys():
updated_weight_constraints[ticker_idx] = weight_constraints[ticker]
elif "others" in weight_constraints.keys():
updated_weight_constraints[ticker_idx] = weight_constraints[
"others"
]
else:
raise ValueError(
"Must specify a weight constraint for each ticker or 'others'"
)
# if float, then create a numpy array with the same bound for all assets
elif isinstance(weight_constraints, float):
updated_weight_constraints = np.full(self.n_assets, weight_constraints)
else:
raise ValueError("Invalid weight constraints")
return updated_weight_constraints

View File

@ -0,0 +1,59 @@
# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from dataclasses import dataclass
import numpy as np
@dataclass
class CvarData:
"""
Data structure holding all scenario and statistical information required
for CVaR optimization.
Attributes
----------
mean : np.ndarray
Array of shape (n_assets,) of expected asset returns.
R : np.ndarray
Scenario deviations, shape (num_scenarios, n_assets),
each row is asset return deviation for a scenario.
p : np.ndarray
Scenario probabilities, shape (num_scenarios,), summing to 1.
Examples
--------
>>> import numpy as np
>>> # Create data for 3 assets with 5 scenarios
>>> mean = np.array([0.08, 0.10, 0.12])
>>> R = np.array([
... [-0.02, 0.01, 0.03],
... [0.01, -0.01, 0.02],
... [0.03, 0.02, -0.01],
... [-0.01, 0.02, 0.01],
... [0.02, -0.02, 0.00]
... ])
>>> p = np.array([0.2, 0.2, 0.2, 0.2, 0.2])
>>> data = CvarData(mean=mean, R=R, p=p)
>>> print(data.mean.shape) # (3,)
>>> print(data.R.shape) # (5, 3)
>>> print(data.p.sum()) # 1.0
"""
mean: np.ndarray
R: np.ndarray
p: np.ndarray

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,112 @@
# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from dataclasses import dataclass
from typing import Optional, Union
import numpy as np
@dataclass
class CvarParameters:
"""
Usertunable parameters and constraint limits for CVaR optimization.
Most parameters are scalars. Weight bounds ``w_min`` / ``w_max`` can be:
- numpy arrays (length n_assets) for per-asset bounds
- dict mapping asset names to bounds
- float for uniform bounds across all assets
- None for no bounds
Optional constraints (T_tar, cvar_limit, cardinality) default to None when
not specified.
"""
# Weight / cash bounds
w_min: Union[np.ndarray, dict, float] = 1.0 # Lower bound for each risky weight
w_max: Union[np.ndarray, dict, float] = 0.0 # Upper bound for each risky weight
c_min: float = 0 # Lower bound for cash allocation
c_max: float = 1 # Upper bound for cash allocation
# Risk model Parameters
risk_aversion: float = 1 # λ penalty applied to CVaR inside objective
confidence: float = 0.95 # α in CVaR_α (e.g. 0.95 -> 95 % CVaR)
# Soft / hard constraint targets
L_tar: float = 1.6 # Leverage constraint (Σ|wᵢ|)
T_tar: Optional[float] = None # Turnover constraint
cvar_limit: Optional[float] = None # Hard CVaR limit (None means "no hard limit")
cardinality: Optional[int] = None # number of assets to be selected
group_constraints: Optional[list[dict]] = None
# Group constraints:
# [{'group_name': group_name,
# 'tickers': tickers
# 'weight_bounds': {'w_min': w_min, 'w_max': w_max}}]
def update_w_min(self, new_w_min: Union[np.ndarray, dict, float]):
self.w_min = new_w_min
def update_w_max(self, new_w_max: Union[np.ndarray, dict, float]):
if new_w_max <= 1:
self.w_max = new_w_max
else:
raise ValueError("Invalid upper bound for weights!")
def update_c_min(self, new_c_min: float):
if new_c_min >= 0:
self.c_min = new_c_min
else:
raise ValueError("Cash should be non-negative!")
def update_c_max(self, new_c_max: float):
if new_c_max >= 0 and new_c_max <= 1:
self.c_max = new_c_max
else:
raise ValueError("Invalid upper bound for cash!")
def update_z_min(self, new_c_min: float):
self.z_min = new_c_min
def update_z_max(self, new_z_max: float):
self.z_max = new_z_max
def update_T_tar(self, new_T_tar: float):
self.T_tar = new_T_tar
def update_L_tar(self, new_L_tar: float):
self.L_tar = new_L_tar
def update_cvar_limit(self, new_cvar_limit: float):
self.cvar_limit = new_cvar_limit
def update_cardinality(self, new_cardinality: int):
if new_cardinality is None or (
isinstance(new_cardinality, int) and new_cardinality > 0
):
self.cardinality = new_cardinality
else:
raise ValueError("Cardinality must be a positive integer or None")
def update_risk_aversion(self, new_risk_aversion: float):
if new_risk_aversion >= 0:
self.risk_aversion = new_risk_aversion
else:
raise ValueError("Invalid risk aversion")
def update_confidence(self, new_confidence: float):
if new_confidence > 0 and new_confidence <= 1:
self.confidence = new_confidence
else:
raise ValueError(
"Invalid confidence level (should be between 0 and 1, "
"e.g. 95%, 99%, etc.)"
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,640 @@
# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Portfolio class for managing and analyzing investment portfolios."""
import json
import os
import matplotlib.colors as mcolors
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
class Portfolio:
"""
Portfolio class for managing asset allocations and cash holdings.
Stores portfolio weights, cash allocation, and provides methods for
portfolio analysis and visualization.
Attributes
----------
name : str
Portfolio identifier
tickers : list
Asset symbols/tickers
weights : np.ndarray
Portfolio weights for each asset
cash : float
Cash allocation (typically 0-1)
time_range : tuple, optional
(start_date, end_date) for portfolio period
"""
def __init__(self, name="", tickers=None, weights=None, cash=0.0, time_range=None):
"""
Initialize Portfolio with assets, weights, and cash allocation.
Parameters
----------
name : str, default ""
Portfolio identifier/name
tickers : list, optional
Asset symbols (e.g., ['AAPL', 'MSFT'])
weights : array-like, optional
Portfolio weights for each asset
cash : float, default 0.0
Cash allocation
time_range : tuple, optional
(start_date, end_date) for portfolio period
"""
self.name = name
self.tickers = tickers if tickers is not None else []
self._n_assets = len(self.tickers)
self.weights = weights if weights is not None else []
self.cash = float(cash)
self.time_range = time_range
def __eq__(self, other_portfolio, atol=1e-3):
"""Check portfolio equality based on weights within tolerance."""
if isinstance(other_portfolio, Portfolio):
return np.allclose(self.weights, other_portfolio.weights, atol=atol)
return False
def _check_self_financing(self, weights=None, cash=None):
"""Verify that portfolio weights and cash sum to 1.0
(self-financing constraint)."""
if weights is None or weights.size == 0:
weights = self.weights
if cash is None:
cash = self.cash
self_finance = np.sum(weights) + cash
if np.abs(self_finance - 1) > 1e-3:
print(f"weights: {np.sum(weights)}; cash: {cash}")
raise ValueError("Portfolio weights and cash do not sum to 1!")
def portfolio_from_dict(self, portfolio_name, user_portfolio_dict, cash):
"""
Create portfolio from user-specified weights dictionary.
Parameters
----------
portfolio_name : str
Name for the portfolio
user_portfolio_dict : dict
Asset weights as {ticker: weight}
cash : float
Cash allocation
"""
weights = pd.Series(dtype=np.float64, index=self.tickers)
for ticker, weight in user_portfolio_dict.items():
ticker = ticker.upper()
if ticker in self.tickers:
weights[ticker] = weight
else:
raise ValueError("Selected ticker is not available in the dataset!")
weights = weights.fillna(0).T
weights = weights.to_numpy()
self._check_self_financing(weights, cash)
self.weights = np.array(weights)
self.cash = float(cash)
self.name = portfolio_name
def print_clean(self, cutoff=1e-3, min_percentage=0.0, rounding=3, verbose=False):
"""
Display clean portfolio allocation with formatting.
Filters positions based on both cutoff threshold and minimum percentage.
Only positions meeting both criteria are displayed/returned.
Parameters
----------
cutoff : float, default 1e-3
Minimum absolute weight to display (positions below this are
considered zero).
min_percentage : float, default 0.0
Minimum percentage threshold (0-100) for displaying/returning assets.
Only assets with absolute allocation >= min_percentage% will be included.
Example: min_percentage=2.0 shows only assets with 2% allocation.
rounding : int, default 3
Number of decimal places for display.
verbose : bool, default False
Whether to print portfolio breakdown.
Returns
-------
tuple
(clean_portfolio_dict, cash) - Dictionary of significant positions
and cash amount.
"""
residual = 0
clean_ptf_dict = {}
long_positions = {}
short_positions = {}
# Process each position
for idx, ticker in enumerate(self.tickers):
value = self.weights[idx]
if value > cutoff:
clean_ptf_dict[ticker] = value
long_positions[ticker] = value
elif value < -cutoff:
clean_ptf_dict[ticker] = value
short_positions[ticker] = value
else:
residual += value
cash = round(self.cash, rounding)
# Filter positions by minimum percentage threshold
min_threshold = min_percentage / 100.0 # Convert percentage to decimal
if min_threshold > 0:
# Filter clean_ptf_dict to only include positions >= min_percentage%
clean_ptf_dict = {
k: v for k, v in clean_ptf_dict.items() if abs(v) >= min_threshold
}
# Re-separate long and short positions after filtering
long_positions = {k: v for k, v in clean_ptf_dict.items() if v > 0}
short_positions = {k: v for k, v in clean_ptf_dict.items() if v < 0}
# Filter cash if below threshold
if abs(cash) < min_threshold:
cash = 0.0
if verbose:
# Portfolio header
print(f"\nPORTFOLIO: {self.name.upper()}")
print(f"{'-' * 40}")
if self.time_range:
print(f"Period: {self.time_range[0]} to {self.time_range[1]}")
# Long positions section
if long_positions:
print(f"\nLONG POSITIONS ({len(long_positions)} assets)")
print(f"{'-' * 25}")
total_long = 0
for ticker, weight in sorted(
long_positions.items(), key=lambda x: x[1], reverse=True
):
print(f"{ticker:8} {weight:>8.{rounding}f} ({weight * 100:>6.2f}%)")
total_long += weight
print(
f"{'Total Long':8} {total_long:>8.{rounding}f} "
f"({total_long * 100:>6.2f}%)"
)
# Short positions section
if short_positions:
print(f"\nSHORT POSITIONS ({len(short_positions)} assets)")
print(f"{'-' * 26}")
total_short = 0
for ticker, weight in sorted(
short_positions.items(), key=lambda x: x[1]
):
print(f"{ticker:8} {weight:>8.{rounding}f} ({weight * 100:>6.2f}%)")
total_short += weight
print(
f"{'Total Short':8} {total_short:>8.{rounding}f} "
f"({total_short * 100:>6.2f}%)"
)
# Cash and summary section
print("\nCASH & SUMMARY")
print(f"{'-' * 20}")
print(f"{'Cash':8} {cash:>8.{rounding}f} ({cash * 100:>6.2f}%)")
if abs(residual) > 1e-6:
print(
f"{'Residual':8} {residual:>8.{rounding}f} "
f"({residual * 100:>6.2f}%)"
)
# Portfolio totals
net_equity = sum(clean_ptf_dict.values())
total_allocation = net_equity + cash + residual
gross_exposure = sum(abs(w) for w in clean_ptf_dict.values())
print(
f"\n{'Net Equity':15} {net_equity:>8.{rounding}f} "
f"({net_equity * 100:>6.2f}%)"
)
print(
f"{'Total Portfolio':15} {total_allocation:>8.{rounding}f} "
f"({total_allocation * 100:>6.2f}%)"
)
print(
f"{'Gross Exposure':15} {gross_exposure:>8.{rounding}f} "
f"({gross_exposure * 100:>6.2f}%)"
)
print(f"{'-' * 40}")
return (clean_ptf_dict, float(cash))
def calculate_portfolio_expected_return(self, mean):
"""
Calculate portfolio expected return from asset mean returns.
Parameters
----------
mean : np.ndarray
Mean returns for each asset (shape: n_assets)
Returns
-------
float
Portfolio expected return
"""
assert (
mean.shape[0] == self._n_assets
), f"Incorrect mean vector size! Expecting: {self._n_assets}."
return mean @ self.weights
def calculate_portfolio_variance(self, covariance):
"""
Calculate portfolio variance from asset covariance matrix.
Parameters
----------
covariance : np.ndarray
Asset covariance matrix (shape: n_assets x n_assets)
Returns
-------
float
Portfolio variance
"""
assert (
covariance.shape[0] == self._n_assets
or covariance.shape[1] != self._n_assets
), (
f"Incorrect covariance size! Expecting: {self._n_assets} by "
+ f"{self._n_assets}."
)
return self.weights.T @ covariance @ self.weights
def plot_portfolio(
self,
show_plot=False,
ax=None,
title=None,
figsize=(12, 8),
style="modern",
cutoff=1e-3,
min_percentage=0.0,
sort_by_weight=True,
save_path=None,
dpi=300,
):
"""
Create a portfolio allocation visualization with gradient colors.
Uses color gradients based on position weights:
- Long positions: Blue gradient (light to dark based on weight)
- Short positions: Red gradient (light to dark based on absolute weight)
- Cash: Yellow (separate category at bottom)
Only displays assets above the minimum percentage threshold for cleaner plots.
Parameters
----------
show_plot : bool, default False
Whether to display the plot immediately.
ax : matplotlib.axes.Axes, optional
Existing axes to plot on. If None, creates new figure.
title : str, optional
Custom plot title. If None, auto-generates from portfolio info.
figsize : tuple, default (12, 8)
Figure size (width, height) in inches.
style : str, default "modern"
Visual style ("modern", "classic", "minimal").
cutoff : float, default 1e-3
Minimum weight to display (smaller positions grouped as "Other").
min_percentage : float, default 0.0
Minimum percentage threshold (0-100) for displaying assets.
Only assets with absolute allocation >= min_percentage% will be shown.
Example: min_percentage=1.0 shows only assets with 1% allocation.
sort_by_weight : bool, default True
Whether to sort positions by absolute weight (largest first).
save_path : str, optional
Path to save the plot. If None, plot is not saved.
dpi : int, default 300
Resolution for saved figure.
Returns
-------
matplotlib.axes.Axes
The axes object containing the plot.
"""
# Color schemes consistent with rebalance plot
color_schemes = {
"modern": {
"long": "#7cd7fe", # Light blue (frontier)
"short": "#ff8181", # Pink/Red (custom)
"cash": "#fcde7b", # Purple (assets)
"background": "#ffffff",
"grid": "#E0E0E0",
"text": "#000000",
}
}
colors = color_schemes.get(style, color_schemes["modern"])
plt.style.use("seaborn-v0_8-whitegrid")
# Create figure if needed
if ax is None:
fig, ax = plt.subplots(
figsize=figsize, dpi=dpi, facecolor=colors["background"]
)
ax.set_facecolor(colors["background"])
else:
_ = ax.get_figure() # fig (unused)
# Get portfolio data (filtering handled by print_clean)
portfolio_data, filtered_cash = self.print_clean(
cutoff=cutoff, min_percentage=min_percentage
)
cash = filtered_cash
# Separate long and short positions
long_positions = {k: v for k, v in portfolio_data.items() if v > 0}
short_positions = {k: v for k, v in portfolio_data.items() if v < 0}
# Prepare data for plotting
all_tickers = []
all_weights = []
all_colors = []
# Sort positions if requested
if sort_by_weight:
long_sorted = sorted(
long_positions.items(), key=lambda x: x[1], reverse=True
)
short_sorted = sorted(
short_positions.items(), key=lambda x: abs(x[1]), reverse=True
)
else:
long_sorted = list(long_positions.items())
short_sorted = list(short_positions.items())
# Create color gradients based on weights
# For long positions: gradient from light to dark blue
if long_positions:
max_long = max(long_positions.values())
min_long = (
min(long_positions.values())
if len(long_positions) > 1
else max_long * 0.1
)
long_range = max_long - min_long if max_long != min_long else max_long
# Create blue gradient colormap
long_cmap = mcolors.LinearSegmentedColormap.from_list(
"long_gradient", ["#7cd7fe", "#0046a4"], N=100
)
# For short positions: gradient from light to dark red
if short_positions:
max_short = max(abs(w) for w in short_positions.values())
min_short = (
min(abs(w) for w in short_positions.values())
if len(short_positions) > 1
else max_short * 0.1
)
short_range = max_short - min_short if max_short != min_short else max_short
# Create red gradient colormap
short_cmap = mcolors.LinearSegmentedColormap.from_list(
"short_gradient", ["#ff8181", "#961515"], N=100
)
# Add long positions with gradient colors
for ticker, weight in long_sorted:
all_tickers.append(ticker)
all_weights.append(weight)
if long_range > 0:
# Normalize weight to [0, 1] for colormap
intensity = (weight - min_long) / long_range
color = long_cmap(intensity)
else:
color = long_cmap(0.8) # Default intensity
all_colors.append(color)
# Add short positions with gradient colors
for ticker, weight in short_sorted:
all_tickers.append(ticker)
all_weights.append(weight)
if short_range > 0:
# Normalize absolute weight to [0, 1] for colormap
intensity = (abs(weight) - min_short) / short_range
color = short_cmap(intensity)
else:
color = short_cmap(0.8) # Default intensity
all_colors.append(color)
# Add cash at the bottom as separate category (yellow)
if abs(cash) > cutoff:
all_tickers.append("CASH")
all_weights.append(cash)
all_colors.append(colors["cash"]) # Gold/Yellow color for cash
# Create horizontal bar chart with extra space before cash
cash_gap = 0.8 # Extra space between equity positions and cash
y_positions = []
# Calculate positions with gap before cash
for i, ticker in enumerate(all_tickers):
if (
ticker == "CASH" and i > 0
): # Add gap before cash if it's not the only item
y_positions.append(i + cash_gap)
else:
y_positions.append(i)
_ = ax.barh( # bars (unused)
y_positions,
all_weights,
color=all_colors,
edgecolor="white",
linewidth=0.8,
alpha=0.8,
)
# Customize appearance - reverse y-axis so first items appear at top
ax.set_yticks(y_positions)
ax.set_yticklabels(all_tickers, fontsize=9, color=colors["text"])
ax.invert_yaxis() # Reverse y-axis so long positions appear at top,
# cash at bottom
ax.set_xlabel("Portfolio Weight", fontsize=10, color=colors["text"])
ax.set_ylabel("Assets", fontsize=10, color=colors["text"])
# Set title
if title is None:
portfolio_name = self.name if self.name else "Portfolio"
if self.time_range:
title = (
f"{portfolio_name} Allocation\n{self.time_range[0]} to "
f"{self.time_range[1]}"
)
else:
title = f"{portfolio_name} Allocation"
ax.set_title(title, fontsize=11, pad=15, color=colors["text"])
# Percentage labels removed for cleaner appearance
# Set x-axis limits with padding
max_abs_weight = max(abs(w) for w in all_weights) if all_weights else 0.1
padding = max_abs_weight * 0.15
ax.set_xlim(-max_abs_weight - padding, max_abs_weight + padding)
# Add horizontal separator line between cash and risky assets
if abs(cash) > cutoff and len(all_tickers) > 1:
# Find cash position and position separator in the middle of the gap
cash_ticker_index = next(
(i for i, ticker in enumerate(all_tickers) if ticker == "CASH"), -1
)
if cash_ticker_index >= 0:
cash_y_pos = y_positions[cash_ticker_index]
# Find the last risky asset position (just before cash)
last_risky_y_pos = (
y_positions[cash_ticker_index - 1] if cash_ticker_index > 0 else 0
)
# Position separator in the middle of the gap
separator_y = (cash_y_pos + last_risky_y_pos) / 2
ax.axhline(
separator_y,
color=colors["text"],
linewidth=1,
alpha=0.6,
linestyle="--",
)
# Grid and styling - subtle grid similar to backtest
ax.grid(True, alpha=0.3, color=colors["grid"])
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["left"].set_color(colors["grid"])
ax.spines["bottom"].set_color(colors["grid"])
# Tick styling
ax.tick_params(axis="both", colors=colors["text"], labelsize=9)
ax.tick_params(axis="x", which="both", bottom=True, top=False)
ax.tick_params(axis="y", which="both", left=False, right=False)
# Legend with gradient representation
legend_elements = []
if long_positions:
# Use darkest blue for legend
num_long = len(long_positions)
legend_elements.append(
plt.Rectangle(
(0, 0),
1,
1,
facecolor="#1F5F8B",
label=f"Long Positions ({num_long})",
)
)
if short_positions:
# Use darkest red for legend
num_short = len(short_positions)
legend_elements.append(
plt.Rectangle(
(0, 0),
1,
1,
facecolor="#8B0000",
label=f"Short Positions ({num_short})",
)
)
if abs(cash) > cutoff:
legend_elements.append(
plt.Rectangle((0, 0), 1, 1, facecolor="#FFD700", label="Cash")
)
if legend_elements:
ax.legend(
handles=legend_elements,
loc="upper left",
frameon=True,
fancybox=True,
shadow=True,
framealpha=0.9,
fontsize=8,
)
plt.tight_layout()
# Save if requested
if save_path:
save_name = f"{self.name.lower()}_allocation.png"
plt.savefig(
os.path.join(save_path, save_name),
dpi=dpi,
bbox_inches="tight",
facecolor=colors["background"],
edgecolor="none",
)
print(f"Portfolio plot saved: {save_path}")
# Show if requested
if show_plot:
plt.show()
return ax
def save_portfolio(self, save_path):
"""Save portfolio to JSON file."""
save_weights = self.weights.tolist()
portfolio = {
"name": self.name,
"weights": save_weights,
"cash": self.cash,
"tickers": self.tickers,
"time_range": self.time_range,
}
with open(save_path, "w") as json_file:
json.dump(portfolio, json_file, indent=4)
def load_portfolio_from_json(self, load_path):
"""Load portfolio from JSON file and update current instance."""
with open(load_path, "r") as json_file:
data = json.load(json_file)
self.name = data["name"]
self.tickers = data["tickers"]
self._n_assets = len(self.tickers)
weights = np.array(data["weights"])
self._check_self_financing(weights, data["cash"])
self.weights = weights
self.cash = data["cash"]
self.time_range = data["time_range"]

View File

@ -0,0 +1,213 @@
# GPU-Accelerated Portfolio Optimization
The NVIDIA Quantitative Portfolio Optimization developer example uses NVIDIA cuOpt and CUDA-X data science libraries to transform portfolio optimization from a slow, batch process into a fast, iterative workflow. GPU-accelerated portfolio optimization pipeline enables scalable strategy backtesting and interactive analysis.
## Overview
This package provides a comprehensive suite of tools for quantitative portfolio management, including risk-aware optimization, backtesting, and dynamic rebalancing. The library leverages GPU acceleration through NVIDIA's cuOpt solver to handle large-scale portfolio optimization problems efficiently.
## Key Features
- **GPU-Accelerated CVaR Optimization**: Utilize NVIDIA cuOpt for fast, scalable portfolio optimization
- **Multiple Modeling APIs**: Compatible with CVXPY (CPU and GPU) and cuOpt Python API (GPU)
- **Advanced Risk Management**: CVaR-based downside risk control with customizable constraints
- **Dynamic Rebalancing**: Systematic portfolio rebalancing with configurable trigger conditions
- **Comprehensive Backtesting**: Performance evaluation against benchmarks with multiple metrics
- **Scenario Generation**: Synthetic data generation using Geometric Brownian Motion
- **Flexible Constraints**: Weight bounds, leverage limits, turnover restrictions, cardinality constraints
## Module Structure
### Core Optimization
#### `cvar_optimizer.CVaR`
Main CVaR portfolio optimizer class supporting Mean-CVaR optimization with multiple solver interfaces.
**Key capabilities:**
- CVXPY solver integration (CPU)
- cuOpt solver integration (GPU)
- Customizable constraint framework
- Support for weight bounds, leverage limits, CVaR hard limits, turnover restrictions, and cardinality constraints
#### `base_optimizer.BaseOptimizer`
Abstract base class providing common functionality for optimization algorithms including weight constraint handling and portfolio state management.
#### `cvar_parameters.CvarParameters`
Configuration class for CVaR optimization parameters, constraints, and solver settings.
#### `cvar_data.CvarData`
Data container for return scenarios, asset information, and optimization inputs.
#### `cvar_utils`
Utility functions for CVaR calculations, portfolio evaluation and visualization, and optimization solver benchmark helper methods.
### Portfolio Management
#### `portfolio.Portfolio`
Portfolio class for managing asset allocations, cash holdings, and portfolio analysis.
**Features:**
- Weight and cash management
- Self-financing constraint validation
- Portfolio visualization
- Performance metrics calculation
- JSON serialization support
### Performance Analysis
#### `backtest.portfolio_backtester`
Backtesting framework for evaluating portfolio strategies against historical data and benchmarks.
**Supported methods:**
- Historical data backtesting
- KDE (Kernel Density Estimation) simulation
- Gaussian simulation
**Metrics:**
- Sharpe ratio
- Sortino ratio
- Maximum drawdown
- Cumulative returns
- Volatility measures
#### `rebalance.rebalance_portfolio`
Dynamic portfolio rebalancing system with CVaR optimization and configurable trigger conditions.
**Rebalancing triggers:**
- Portfolio drift thresholds
- Performance percentage changes
- Maximum drawdown limits
**Features:**
- Rolling CVaR optimization
- Transaction cost modeling
- Performance visualization
- Baseline comparison
### Data Generation
#### `scenario_generation.ForwardPathSimulator`
Synthetic financial data generation using stochastic processes.
**Methods:**
- Geometric Brownian Motion (log_gbm)
- Path simulation for forward-looking scenarios
- Calibration from historical data
### Utilities
#### `utils`
General-purpose utilities for data processing and portfolio calculations.
**Key functions:**
- `get_input_data()`: Multi-format data loading (CSV, Parquet, Excel, JSON)
- `calculate_returns()`: Return calculation with log/linear transformations
- `calculate_log_returns()`: Log return computation
- Performance metrics and visualization helpers
## Installation
For installation instructions and prerequisites, please refer to the [main README](../README.md).
## Quick Start
### Basic Mean-CVaR Optimization
#### CVXPY
```python
from src import CvarData, CvarParameters
from src.cvar_optimizer import CVaR
import cvxpy as cp
# Load and prepare return data
returns_dict = {
'returns': returns_data, # Historical return scenarios
'tickers': ['AAPL', 'MSFT', 'GOOGL'],
'mean': mean_returns,
'covariance': cov_matrix
}
# Configure optimization parameters
cvar_params = CvarParameters(
alpha=0.95, # CVaR confidence level
risk_aversion=1.0, # Risk-return tradeoff
weight_lower_bound=0.0, # Min weight per asset
weight_upper_bound=0.3, # Max weight per asset
leverage=1.0 # No leverage
)
# Create optimizer and solve
optimizer = CVaR(returns_dict, cvar_params)
result, portfolio = optimizer.solve_optimization_problem(
{"solver": cp.CUOPT}
) #can replace with other CPU solvers
```
#### cuOpt Python API
```python
# Use cuOpt for GPU acceleration
api_settings = {"api": "cuopt_python"}
optimizer = CVaR(returns_dict, cvar_params, api_settings=api_settings)
result, portfolio = optimizer.solve_optimization_problem({
"time_limit": 60
})
```
### Backtesting
```python
from src.backtest import portfolio_backtester
# Initialize backtester
backtester = portfolio_backtester(
test_portfolio=portfolio,
returns_dict=returns_dict,
risk_free_rate=0.02,
test_method="historical"
)
# Run backtest
metrics = backtester.backtest()
print(f"Sharpe Ratio: {metrics['sharpe_ratio']:.3f}")
print(f"Max Drawdown: {metrics['max_drawdown']:.2%}")
```
### Dynamic Rebalancing
```python
from src.rebalance import rebalance_portfolio
# Configure rebalancing strategy
rebalancer = rebalance_portfolio(
dataset_directory="data/prices.csv",
trading_start="2023-01-01",
trading_end="2024-01-01",
look_back_window=252,
look_forward_window=21,
cvar_params=cvar_params,
solver_settings={"solver": cp.CLARABEL},
re_optimize_criteria={
"type": "drift",
"threshold": 0.05
},
return_type="LOG"
)
# Execute rebalancing strategy
results = rebalancer.rebalance()
```
## Performance Considerations
- **GPU Acceleration**: For portfolios with 100+ assets or 5000+ scenarios, cuOpt can provide 10-100x speedup over CPU solvers
- **Constraint Handling**: Using parameter-based constraints in CVXPY can improve warm-start performance
- **Memory Management**: Large scenario sets may require chunking for GPU memory constraints
## References
For detailed API documentation and advanced usage examples, refer to the jupyter notebooks in the [`notebooks/`](../notebooks/) directory.

View File

@ -0,0 +1,908 @@
# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Dynamic portfolio rebalancing with CVaR optimization.
Implements systematic rebalancing strategies using Conditional Value-at-Risk
optimization with configurable trigger conditions based on portfolio drift,
performance thresholds, or maximum drawdown.
Key Features
------------
* Dynamic rebalancing with multiple trigger conditions
* Rolling CVaR optimization over trading period
* Transaction cost modeling
* Performance visualization and baseline comparison
* Support for various rebalancing criteria
Classes
-------
rebalance_portfolio
Main class for implementing dynamic rebalancing strategies with CVaR
optimization and configurable trigger conditions.
"""
import os
from datetime import datetime
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from matplotlib.lines import Line2D
from . import backtest, cvar_optimizer, cvar_parameters, cvar_utils, portfolio, utils
class rebalance_portfolio:
"""
Dynamic portfolio rebalancing with CVaR optimization.
Performs rolling CVaR optimization over a trading period, triggering portfolio
rebalancing when specified criteria are met (drift, percentage change, or
maximum drawdown).
Parameters
----------
dataset_directory : str
Path to asset universe data.
returns_compute_settings : dict
Settings for computing returns.
scenario_generation_settings : dict
Settings for generating return scenarios.
trading_start, trading_end : str
Trading period boundaries in YYYY-MM-DD format.
look_forward_window : int
Backtest evaluation window size in trading days.
look_back_window : int
Historical data window for optimization in trading days.
cvar_params : CvarParameters
CVaR optimization parameters and constraints.
solver_settings : dict
Solver configuration for optimization backend.
re_optimize_criteria : dict
Rebalancing trigger conditions with type and threshold.
print_opt_result : bool, default False
Whether to print detailed optimization results.
"""
def __init__(
self,
dataset_directory: str,
returns_compute_settings: dict,
scenario_generation_settings: dict,
trading_start: str,
trading_end: str,
look_forward_window: int,
look_back_window: int,
cvar_params: cvar_parameters.CvarParameters,
solver_settings: dict,
re_optimize_criteria: dict,
print_opt_result: bool = False,
):
"""Initialize rebalancing portfolio with optimization parameters."""
self.dataset_directory = dataset_directory
self.trading_start = pd.to_datetime(trading_start)
self.trading_end = pd.to_datetime(trading_end)
self.look_forward_window = look_forward_window
self.look_back_window = look_back_window
self.cvar_params = cvar_params
self.solver_settings = solver_settings
self.returns_compute_settings = returns_compute_settings
self.scenario_generation_settings = scenario_generation_settings
self.print_opt_result = print_opt_result
self.re_optimize_criteria = re_optimize_criteria
self.re_optimize_type = re_optimize_criteria["type"].lower()
self.re_optimize_threshold = re_optimize_criteria["threshold"]
self._get_price_data()
self.dates_range = self.price_data.loc[trading_start:trading_end].index
(
self.buy_and_hold_results,
self.buy_and_hold_cumulative_portfolio_value,
) = self._get_buy_and_hold_results()
def _get_price_data(self):
"""Load price data and calculate returns based on specified return type."""
self.price_data = pd.read_csv(
self.dataset_directory, index_col=0, parse_dates=True
)
price_data_start = self.trading_start - pd.Timedelta(days=self.look_back_window)
assert (
price_data_start >= self.price_data.index[0]
), "Invalid start date - choose a later date!"
def re_optimize(
self,
transaction_cost_factor: float = 0,
plot_results: bool = False,
existing_portfolio: portfolio.Portfolio = None,
run_re_optimize: bool = True,
save_plot: bool = False,
results_dir: str = "results",
):
"""Execute rebalancing strategy over the trading period.
Performs rolling optimization and backtesting, triggering rebalancing
when specified criteria are met.
Parameters
----------
transaction_cost_factor : float, default 0
Transaction cost as fraction of turnover.
plot_results : bool, default False
Whether to generate performance plots.
existing_portfolio : Portfolio, optional
Starting portfolio allocation.
run_re_optimize : bool, default True
Enable rebalancing logic. If False, uses initial portfolio only.
save_plot : bool, default False
Whether to save generated plots.
results_dir : str, default "results"
Directory for saving plots.
Returns
-------
results_dataframe : pd.DataFrame
Detailed results for each rebalancing decision.
re_optimize_dates : list
Dates when rebalancing was triggered.
cumulative_portfolio_value : pd.Series
Time series of portfolio values.
"""
if run_re_optimize:
print(f"{'=' * 60}")
print("DYNAMIC REBALANCING ANALYSIS")
print(
f"Period: {self.trading_start.strftime('%Y-%m-%d')} to "
f"{self.trading_end.strftime('%Y-%m-%d')}"
)
# Format strategy name with special handling for pct_change
strategy_name = self.re_optimize_type.replace("_", " ").title()
if strategy_name == "Pct Change":
strategy_name = "Percentage Change"
print(f"Strategy: {strategy_name}")
print(f"Threshold: {self.re_optimize_threshold}")
print(f"Look-forward window: {self.look_forward_window} days")
print(f"Look-back window: {self.look_back_window} days")
print(f"{'=' * 60}")
result = 0.0
re_optimize_flag = False
portfolio_value = 1.0
current_portfolio = self.initial_portfolio if run_re_optimize else None
results_dataframe = pd.DataFrame(
columns=[
self.re_optimize_type,
"re_optimized",
"portfolio_value",
"solver_time",
]
)
_ = pd.DataFrame(columns=["portfolio_value"]) # no_re_optimize_results (unused)
cumulative_portfolio_value_array = np.array([])
cumulative_portfolio_value_dates = [] # Track dates for portfolio values
re_optimize_dates = []
backtest_idx = 0
backtest_date = self.trading_start
backtest_final_date = self.dates_range[-self.look_forward_window]
while backtest_date < backtest_final_date:
results_dataframe.loc[backtest_date, self.re_optimize_type] = result
results_dataframe.loc[backtest_date, "re_optimized"] = re_optimize_flag
results_dataframe.loc[backtest_date, "optimal_portfolio"] = (
current_portfolio
)
results_dataframe.loc[backtest_date, "portfolio_value"] = portfolio_value
# Use initial solve time for the first row when rebalancing
# (shared from buy-and-hold)
if backtest_date == self.trading_start and run_re_optimize:
results_dataframe.loc[backtest_date, "solver_time"] = (
self.buy_and_hold_results.iloc[0]["solver_time"]
)
else:
results_dataframe.loc[backtest_date, "solver_time"] = None
existing_portfolio = current_portfolio
# re-optimize if criteria goes beyond threshold
if (
(backtest_date == self.trading_start) and not run_re_optimize
) or re_optimize_flag:
optimize_start = backtest_date - pd.Timedelta(
days=self.look_back_window
)
optimize_regime = {
"name": "re-optimize",
"range": (
optimize_start.strftime("%Y-%m-%d"),
backtest_date.strftime("%Y-%m-%d"),
),
}
optimize_returns_dict = utils.calculate_returns(
self.price_data,
optimize_regime,
self.returns_compute_settings
)
optimize_returns_dict = cvar_utils.generate_cvar_data(
optimize_returns_dict,
self.scenario_generation_settings
)
re_optimize_problem = cvar_optimizer.CVaR(
returns_dict=optimize_returns_dict,
cvar_params=self.cvar_params,
existing_portfolio=existing_portfolio,
)
result_row, current_portfolio = (
re_optimize_problem.solve_optimization_problem(
self.solver_settings,
print_results=self.print_opt_result,
)
)
if (backtest_date == self.trading_start) and not run_re_optimize:
self.initial_portfolio = current_portfolio
# Store solver time information
solve_time = result_row.get("solve time", None)
results_dataframe.at[backtest_date, "solver_time"] = solve_time
re_optimize_dates.append(backtest_date)
if run_re_optimize:
print(
f"Rebalancing triggered on "
f"{backtest_date.strftime('%Y-%m-%d')} | "
f"Event #{len(re_optimize_dates)} | "
f"Portfolio value: ${portfolio_value:,.2f}"
)
# get backtest start and end dates
backtest_start = backtest_date.strftime("%Y-%m-%d")
backtest_end = self.dates_range[backtest_idx + self.look_forward_window]
backtest_end = backtest_end.strftime("%Y-%m-%d")
backtest_regime = {
"name": "backtest",
"range": (backtest_start, backtest_end),
}
# calculate returns
test_returns_dict = utils.calculate_returns(
self.price_data, backtest_regime, self.returns_compute_settings
)
# run backtest
backtester = backtest.portfolio_backtester(
current_portfolio, test_returns_dict, benchmark_portfolios=None
)
backtest_result = backtester.backtest_single_portfolio(current_portfolio)
cur_cumulative_portfolio_returns = (
backtest_result["cumulative returns"].values[0] * portfolio_value
)
cumulative_portfolio_value_array = np.concatenate(
(cumulative_portfolio_value_array, cur_cumulative_portfolio_returns)
)
# Extract actual trading dates from the backtester object
backtest_period_dates = backtester._dates
cumulative_portfolio_value_dates.extend(backtest_period_dates)
# update portfolio value
portfolio_value_pct_change = self._calculate_pct_change(backtest_result)
transaction_cost = self._calculate_transaction_cost(
current_portfolio, existing_portfolio, transaction_cost_factor
)
portfolio_value = portfolio_value * (
1 + portfolio_value_pct_change - transaction_cost
)
# re-optimize criteria check
if run_re_optimize:
if self.re_optimize_type == "pct_change":
result, re_optimize_flag = self._check_pct_change(
portfolio_value_pct_change, backtest_result, results_dataframe
)
elif self.re_optimize_type == "drift_from_optimal":
# Calculate the index for drift checking
result, re_optimize_flag = self._check_drift_from_optimal(
current_portfolio, backtest_idx
)
elif self.re_optimize_type == "max_drawdown":
result = backtest_result["max drawdown"].values[0]
re_optimize_flag = self._check_max_drawdown(result)
elif self.re_optimize_type == "no_re_optimize":
re_optimize_flag = False
result = None
backtest_idx += self.look_forward_window
backtest_date = self.dates_range[backtest_idx]
# Convert to pandas Series with dates as index, ensuring proper datetime format
cumulative_portfolio_value_dates_clean = pd.to_datetime(
cumulative_portfolio_value_dates
)
cumulative_portfolio_value = pd.Series(
cumulative_portfolio_value_array,
index=cumulative_portfolio_value_dates_clean,
name="cumulative_portfolio_value",
)
# Print analysis summary
if run_re_optimize:
total_return = (
cumulative_portfolio_value.iloc[-1] / cumulative_portfolio_value.iloc[0]
- 1
) * 100
print("\nANALYSIS COMPLETE")
print(f"Total rebalancing events: {len(re_optimize_dates)}")
print(f"Final portfolio value: ${cumulative_portfolio_value.iloc[-1]:,.2f}")
print(f"Total return: {total_return:+.2f}%")
print(f"Data points collected: {len(cumulative_portfolio_value):,}")
print(f"{'=' * 60}\n")
if plot_results:
self.plot_results(
results_dataframe,
re_optimize_dates,
cumulative_portfolio_value,
save_plot,
results_dir,
)
return results_dataframe, re_optimize_dates, cumulative_portfolio_value
def _calculate_pct_change(self, backtest_result: pd.DataFrame):
"""Calculate percentage change in portfolio value over backtest period.
Parameters
----------
backtest_result : pd.DataFrame
Backtest results containing cumulative returns.
Returns
-------
float
Percentage change from start to end of period.
"""
cumulative_returns = backtest_result["cumulative returns"][0]
pct_change = (
cumulative_returns[-1] / cumulative_returns[0] - 1
) # percent change
return pct_change
def _calculate_transaction_cost(
self,
current_portfolio: portfolio.Portfolio,
existing_portfolio: portfolio.Portfolio,
transaction_cost_factor: float,
):
"""Calculate transaction costs from portfolio rebalancing.
Parameters
----------
current_portfolio : Portfolio
New target allocation.
existing_portfolio : Portfolio
Previous allocation.
transaction_cost_factor : float
Cost factor as fraction of turnover.
Returns
-------
float
Total transaction cost.
"""
if existing_portfolio is None:
return 0
else:
turnover = np.sum(
np.abs(current_portfolio.weights - existing_portfolio.weights)
)
return turnover * transaction_cost_factor
def _check_pct_change(
self,
pct_change: float,
backtest_result: pd.DataFrame,
results_dataframe: pd.DataFrame,
):
"""Check if percentage change triggers rebalancing.
Evaluates current and cumulative negative returns against threshold.
Parameters
----------
pct_change : float
Current period percentage change.
backtest_result : pd.DataFrame
Current backtest results.
results_dataframe : pd.DataFrame
Historical results for cumulative calculation.
Returns
-------
pct_change : float
Input percentage change.
re_optimize_flag : bool
True if rebalancing should be triggered.
"""
re_optimize_flag = False
prev_total = 0
for idx in reversed(range(results_dataframe.shape[0])):
if (
results_dataframe["pct_change"].iloc[idx] < 0
and not results_dataframe["re_optimized"].iloc[idx]
):
prev_total += results_dataframe["pct_change"].iloc[idx]
else:
break
if (
pct_change < self.re_optimize_threshold
or prev_total + pct_change < self.re_optimize_threshold
):
re_optimize_flag = True
return pct_change, re_optimize_flag
def _check_drift_from_optimal(
self, optimal_portfolio: portfolio.Portfolio, backtest_idx: int
):
"""Check if portfolio drift from optimal triggers rebalancing.
Calculates deviation between current and optimal allocation after
price movements.
Parameters
----------
optimal_portfolio : Portfolio
Target optimal allocation.
backtest_idx : int
Current backtest date index.
Returns
-------
deviation : float
Drift magnitude (L1 or L2 norm).
re_optimize_flag : bool
True if drift exceeds threshold.
"""
re_optimize_flag = False
price_change = self.price_data.iloc[backtest_idx].div(
self.price_data.iloc[backtest_idx + self.look_forward_window]
)
cur_portfolio_weights = price_change.to_numpy() * optimal_portfolio.weights
if self.re_optimize_criteria["norm"] == 2:
deviation = np.sum(
np.abs(cur_portfolio_weights - optimal_portfolio.weights) ** 2
) # squared differences
elif self.re_optimize_criteria["norm"] == 1:
deviation = np.sum(
np.abs(cur_portfolio_weights - optimal_portfolio.weights)
) # 1-norm
if deviation > self.re_optimize_threshold:
re_optimize_flag = True
return deviation, re_optimize_flag
def _check_max_drawdown(self, mdd: float):
"""Check if maximum drawdown triggers rebalancing.
Parameters
----------
mdd : float
Current maximum drawdown.
Returns
-------
bool
True if drawdown exceeds threshold.
"""
re_optimize_flag = False
if mdd > self.re_optimize_threshold:
re_optimize_flag = True
return re_optimize_flag
def plot_results(
self,
results_dataframe: pd.DataFrame,
re_optimize_dates: list,
cumulative_portfolio_value: pd.Series,
save_plot: bool = False,
results_dir: str = "results",
):
"""Generate portfolio performance comparison plots.
Compares dynamic rebalancing strategy against buy-and-hold baseline
with rebalancing event markers.
Parameters
----------
results_dataframe : pd.DataFrame
Rebalancing results and metrics.
re_optimize_dates : list
Dates when rebalancing was triggered.
cumulative_portfolio_value : pd.Series
Time series of portfolio values.
save_plot : bool, default False
Whether to save the plot.
results_dir : str, default "results"
Directory for saving plots.
"""
# Use the same styling as efficient frontier
color_schemes = {
"modern": {
"frontier": "#7cd7fe",
"benchmark": ["#ef9100", "#ff8181", "#0d8473"], #NVIDIA orange, red, dark teal
"assets": "#c359ef",
"custom": "#fc79ca",
"background": "#FFFFFF",
"grid": "#E0E0E0",
}
}
colors = color_schemes["modern"]
# Create figure with same styling as efficient frontier
plt.style.use("seaborn-v0_8-whitegrid")
sns.set_context("paper", font_scale=1.6)
fig, ax = plt.subplots(figsize=(12, 8), dpi=300, facecolor=colors["background"])
ax.set_facecolor(colors["background"])
# Plot rebalancing strategy line with proper datetime handling
ax.plot(
cumulative_portfolio_value.index,
cumulative_portfolio_value.values,
linewidth=3,
color=colors["frontier"],
label="Dynamic Rebalancing",
zorder=3,
alpha=0.9,
)
# Plot no-rebalancing baseline with proper datetime handling
ax.plot(
self.buy_and_hold_cumulative_portfolio_value.index,
self.buy_and_hold_cumulative_portfolio_value.values,
linewidth=2.5,
color=colors["benchmark"][0],
linestyle="-",
label="Buy & Hold",
zorder=2,
alpha=0.8,
)
# Add subtle fill under the rebalancing line
ax.fill_between(
cumulative_portfolio_value.index,
cumulative_portfolio_value.values,
alpha=0.1,
color=colors["frontier"],
zorder=1,
)
# Add rebalancing date markers as scatter points
if re_optimize_dates:
rebalancing_values = []
rebalancing_dates_clean = []
for date in re_optimize_dates:
# Convert date to pandas timestamp if needed
date_ts = pd.to_datetime(date)
# Find the portfolio value at this rebalancing date
if date_ts in cumulative_portfolio_value.index:
rebalancing_values.append(cumulative_portfolio_value[date_ts])
rebalancing_dates_clean.append(date_ts)
else:
# Find nearest date
nearest_idx = cumulative_portfolio_value.index.get_indexer(
[date_ts], method="nearest"
)[0]
if nearest_idx >= 0:
nearest_date = cumulative_portfolio_value.index[nearest_idx]
rebalancing_values.append(
cumulative_portfolio_value[nearest_date]
)
rebalancing_dates_clean.append(nearest_date)
if rebalancing_dates_clean:
# Calculate y-axis range for consistent line heights
y_min = cumulative_portfolio_value.min()
y_max = cumulative_portfolio_value.max()
y_range = y_max - y_min
line_height = y_range * 0.05 # 5% of y-axis range
# Create small vertical line segments at each rebalancing date
for date, value in zip(rebalancing_dates_clean, rebalancing_values):
ax.vlines(
date,
value - line_height,
value + line_height,
color=colors[
"assets"
], # Use purple color for better visibility
linewidth=2.5,
alpha=0.9,
linestyle="--", # Dashed lines for visibility
zorder=5,
)
ax.plot(
date,
value,
"o",
color=colors["assets"],
markersize=7,
markeredgecolor="white",
markeredgewidth=1.5,
zorder=6,
)
# Professional styling
ax.set_xlabel("Date", fontsize=14, fontweight="bold")
ax.set_ylabel("Cumulative Portfolio Value", fontsize=14, fontweight="bold")
# Create title based on rebalancing criteria
rebalance_type = self.re_optimize_type.replace("_", " ").title()
if rebalance_type == "Pct Change":
rebalance_type = "Percentage Change"
title = f"\n{rebalance_type} Rebalancing Strategy"
ax.set_title(title, fontsize=16, fontweight="bold", pad=20)
# Grid and styling
ax.grid(True, alpha=0.3, color=colors["grid"])
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["left"].set_color("#CCCCCC")
ax.spines["bottom"].set_color("#CCCCCC")
# Clear automatic pandas legend and create custom legend
if ax.legend_:
ax.legend_.remove()
legend_elements = [
Line2D(
[0],
[0],
color=colors["frontier"],
linewidth=3,
markeredgecolor="white",
markeredgewidth=1.5,
label="Dynamic Rebalancing",
),
Line2D(
[0],
[0],
color=colors["benchmark"][0],
linewidth=2.5,
label="Buy & Hold",
),
]
# Add rebalancing dates to legend only if they exist
if re_optimize_dates:
legend_elements.append(
Line2D(
[0],
[0],
color=colors["assets"],
linewidth=2.5,
linestyle="--",
marker="o",
markersize=7,
markerfacecolor=colors["assets"],
markeredgecolor="white",
markeredgewidth=1.5,
label="Rebalancing Dates",
)
)
ax.legend(
handles=legend_elements,
loc="upper left",
frameon=True,
fancybox=True,
shadow=True,
framealpha=0.9,
fontsize=12,
)
# Format x-axis for better date display
import matplotlib.dates as mdates
# Ensure the x-axis is treated as datetime
ax.xaxis_date()
# Set reasonable date formatting based on date range
date_range = (
cumulative_portfolio_value.index.max()
- cumulative_portfolio_value.index.min()
)
if date_range.days > 365:
# For longer periods, show year-month
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
ax.xaxis.set_major_locator(mdates.MonthLocator(interval=6))
else:
# For shorter periods, show month-day
ax.xaxis.set_major_formatter(mdates.DateFormatter("%m-%d"))
ax.xaxis.set_major_locator(mdates.MonthLocator(interval=1))
# Rotate x-axis labels for better readability
ax.tick_params(axis="x", labelrotation=45)
# Set x-axis limits to the actual data range
ax.set_xlim(
cumulative_portfolio_value.index.min(),
cumulative_portfolio_value.index.max(),
)
# Set y-axis limits to zoom into the actual data range with some padding
all_values = []
all_values.extend(cumulative_portfolio_value.values)
all_values.extend(self.buy_and_hold_cumulative_portfolio_value.values)
y_min = min(all_values)
y_max = max(all_values)
y_range = y_max - y_min
padding = y_range * 0.05 # 5% padding on top and bottom
ax.set_ylim(y_min - padding, y_max + padding)
# Tight layout
plt.tight_layout()
# Save plot if requested
if save_plot:
# Create results directory if it doesn't exist
os.makedirs(results_dir, exist_ok=True)
# Generate descriptive filename
_ = datetime.now().strftime("%Y%m%d_%H%M%S") # timestamp (unused)
strategy_name = self.re_optimize_type.replace("_", "-")
start_date = self.trading_start.strftime("%Y%m%d")
end_date = self.trading_end.strftime("%Y%m%d")
_ = len(re_optimize_dates) # num_rebalances (unused)
filename = (
f"rebalancing_{strategy_name}_with_threshold_"
f"{self.re_optimize_threshold}_{start_date}-{end_date}events.png"
)
filepath = os.path.join(results_dir, filename)
# Save with high quality
plt.savefig(
filepath,
dpi=300,
bbox_inches="tight",
facecolor="white",
edgecolor="none",
)
print(f"Plot saved: {filepath}")
def _get_buy_and_hold_results(self):
"""Generate baseline buy-and-hold results for comparison.
Optimizes portfolio once at start and maintains allocation throughout
trading period.
Returns
-------
buy_and_hold_results_dataframe : pd.DataFrame
Baseline strategy results.
cumulative_portfolio_value : pd.Series
Time series of baseline portfolio values.
"""
print("=" * 60)
print("BASELINE (BUY & HOLD) ANALYSIS")
print(
f"Period: {self.trading_start.strftime('%Y-%m-%d')} to "
f"{self.trading_end.strftime('%Y-%m-%d')}"
)
print("Strategy: Single optimization at start")
print("=" * 60)
(
buy_and_hold_results_dataframe,
_,
cumulative_portfolio_value,
) = self.re_optimize(
plot_results=False, existing_portfolio=None, run_re_optimize=False
)
# Print baseline summary
total_return = (
cumulative_portfolio_value.iloc[-1] / cumulative_portfolio_value.iloc[0] - 1
) * 100
print("\nBASELINE COMPLETE")
print(f"Final portfolio value: ${cumulative_portfolio_value.iloc[-1]:,.2f}")
print(f"Total return: {total_return:+.2f}%")
print(f"Data points collected: {len(cumulative_portfolio_value):,}")
print(f"{'=' * 60}\n")
return buy_and_hold_results_dataframe, cumulative_portfolio_value
def plot_weights_vs_prices(self, re_optimize_results: pd.DataFrame, ticker: str):
"""Plot portfolio weights evolution against price movements.
Creates dual-axis plot showing asset prices and portfolio weight
allocations over time.
Parameters
----------
re_optimize_results : pd.DataFrame
Rebalancing results with optimal portfolios.
ticker : str
Asset ticker symbol. Must exist in asset universe.
Raises
------
AssertionError
If ticker not found in price data.
"""
assert (
ticker in self.price_data.columns
), "The selected ticker is not in the asset universe!"
ticker_idx = list(self.price_data.columns).index(ticker)
ticker_weights_history = [
current_portfolio.weights[ticker_idx]
for current_portfolio in re_optimize_results["optimal_portfolio"].iloc[1:]
]
fig, ax1 = plt.subplots(figsize=(12, 6))
plot_start_date = re_optimize_results.index[1]
plot_end_date = re_optimize_results.index[-1]
price_data = self.price_data.loc[plot_start_date:plot_end_date, ticker]
ax1.plot(price_data, color="red", label=f"{ticker} prices")
ax1.set_title(f"{ticker} weights vs. prices")
ax2 = ax1.twinx()
ax2.bar(
re_optimize_results.index[1:],
ticker_weights_history,
color="#76b900",
width=30,
label=f"{ticker}",
alpha=0.7,
)
ax2.axhline(y=0, color="black", linewidth=0.8)
ax1.legend()
ax2.legend()
# Show the plot
plt.tight_layout()
plt.show()

View File

@ -0,0 +1,288 @@
# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Scenario generation module for portfolio optimization.
Provides tools for generating synthetic financial data using Geometric Brownian Motion
and other stochastic processes. Used to create forward-looking scenarios for
risk assessment and portfolio optimization.
"""
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
class ForwardPathSimulator:
"""Generates synthetic forward paths for financial assets.
Uses Geometric Brownian Motion to simulate asset price paths based on
historical data calibration.
Parameters
----------
fitting_data : pd.DataFrame
Historical price data for calibration (dates x assets).
generation_dates : pd.DatetimeIndex or list
Date range for synthetic data generation.
n_paths : int
Number of scenarios/forward paths to generate.
method : str, default "log_gbm"
Generation method (currently only "log_gbm" supported).
Attributes
----------
fitting_data : pd.DataFrame
Historical data used for calibration.
dates : pd.DatetimeIndex or list
Generation date range.
n_steps : int
Number of time steps for simulation.
n_paths : int
Number of scenarios generated.
generation_method : str
Method used for generation.
simulated_paths : np.ndarray
Generated synthetic paths (n_paths x n_steps+1 x n_assets).
"""
def __init__(self, fitting_data, generation_dates, n_paths, method="log_gbm"):
"""Initialize scenario generator with data and parameters."""
self.fitting_data = fitting_data
self.dates = generation_dates
self.n_steps = len(generation_dates) - 1
self.n_paths = n_paths
self.generation_method = method.lower()
def generate(self, plot_paths=False, n_plots=0):
"""Generate synthetic forward paths.
Parameters
----------
plot_paths : bool, default False
Whether to plot generated paths.
n_plots : int, default 0
Number of paths to plot if plot_paths is True.
Raises
------
ValueError
If generation method is not recognized.
"""
if self.generation_method == "log_gbm":
mu, sigma, L = self._calibrate_log_process()
self.simulated_paths = self._generate_via_log_gbm(mu, sigma, L)
else:
raise ValueError("Unrecognized generation method.")
if plot_paths:
self._plot_generated_paths(n_plots)
def _calibrate_log_process(self):
"""Calibrate log-normal process parameters from historical data.
Returns
-------
mu : np.ndarray
Drift parameters for each asset.
sigma : np.ndarray
Covariance matrix of log returns.
L : np.ndarray
Cholesky decomposition of covariance matrix.
"""
log_returns = np.log(self.fitting_data / self.fitting_data.shift(1)).dropna()
# Estimate covariance matrix of log returns
sigma = log_returns.cov().values
# Cholesky decomposition of the correlation matrix
L = np.linalg.cholesky(sigma).T
# Estimate drift
total_drift = log_returns.iloc[-1].values - log_returns.iloc[0].values
step_drift = total_drift / self.n_steps
mu = step_drift + 0.5 * np.sum(L**2, axis=1)
return mu, sigma, L
def _generate_via_log_gbm(self, mu, sigma, L, dt=1):
"""Generate paths using log-normal Geometric Brownian Motion.
Parameters
----------
mu : np.ndarray
Drift parameters for each asset.
sigma : np.ndarray
Covariance matrix of log returns.
L : np.ndarray
Cholesky decomposition of covariance matrix.
dt : float, default 1
Time step size.
Returns
-------
np.ndarray
Simulated paths (n_paths x n_steps+1 x n_assets).
"""
# Initial forward rates
last_rates = self.fitting_data.loc[
self.dates[0]
].values # set starting value as the start of the generation period
# Initialize an array for simulated paths
simulated_paths = np.zeros((self.n_paths, self.n_steps + 1, len(mu)))
current_rates = last_rates
simulated_paths[:, 0, :] = current_rates
Z = np.random.normal(size=(self.n_paths, self.n_steps, len(mu)))
dW = np.matmul(Z, L) * np.sqrt(dt)
for t in range(1, self.n_steps + 1):
# compute drift and diffusion
drift = (mu - 0.5 * np.diag(sigma) ** 2) * dt
diffusion = dW[:, t - 1, :]
# Simulate next step forward rates using GBM formula
simulated_paths[:, t, :] = simulated_paths[:, t - 1, :] * np.exp(
drift + diffusion
)
return simulated_paths
def _plot_generated_paths(self, n_plots):
"""Plot randomly selected generated paths.
Parameters
----------
n_plots : int
Number of paths to plot.
"""
# Assuming 'simulated_paths' is your array of simulated paths with shape
# (n_paths, n_steps, n_ccy_pairs)
n_paths = self.simulated_paths.shape[0]
_ = self.simulated_paths.shape[2] # n_ccy_pairs (unused)
# Randomly select indices for the scenarios to plot
random_indices = np.random.choice(n_paths, n_plots, replace=False)
plt.rcParams.update({"font.size": 8})
sns.set(rc={"figure.dpi": 100, "savefig.dpi": 300})
sns.set_palette(palette="tab10")
sns.set_style("white")
# Loop through each selected scenario and create a subplot
for i, idx in enumerate(random_indices):
plt.figure(i, figsize=(10, 7))
selected_paths = pd.DataFrame(
self.simulated_paths[idx, :, :],
index=self.fitting_data.index,
columns=self.fitting_data.columns,
)
selected_paths.plot()
plt.title(f"Scenario {i + 1} - Path {idx + 1}")
plt.xticks(rotation=50, fontsize=8)
plt.ylabel("Forward Rate")
plt.legend()
plt.show()
def get_simulated_paths_ccy_pair(self, ccy_pair):
"""Extract simulated paths for a specific asset.
Parameters
----------
ccy_pair : str
Asset identifier to extract paths for.
Returns
-------
pd.DataFrame
Simulated paths for the specified asset (dates x n_paths).
"""
ccy_pair_idx = list(self.fitting_data.columns).index(ccy_pair)
simulated_paths_ccy_pair = self.simulated_paths[:, :, ccy_pair_idx]
simulated_paths_dataframe = pd.DataFrame(
simulated_paths_ccy_pair, index=self.dates
)
return simulated_paths_dataframe
def generate_synthetic_stock_data(
dataset_directory, num_synthetic, fit_range, generate_range
):
"""Generate synthetic stock data using Geometric Brownian Motion.
Fits GBM parameters to historical data from one period and generates
synthetic time series for another period.
Parameters
----------
dataset_directory : str
Path to CSV file containing historical stock data.
num_synthetic : int
Multiplier for synthetic stocks. Total synthetic stocks will be
num_synthetic * num_assets.
fit_range : tuple of str
Start and end dates for calibration period (start, end).
generate_range : tuple of str
Start and end dates for generation period (start, end).
Returns
-------
pd.DataFrame
Combined dataset with original and synthetic stock data.
Synthetic columns are named as 'ticker-idx' where idx is the
path number.
"""
input_data = pd.read_csv(dataset_directory, index_col=0)
fit_data = input_data.loc[fit_range[0] : fit_range[1]]
n_assets = len(fit_data.columns)
generate_time_range = input_data.loc[generate_range[0] : generate_range[1]].index
scen_gen = ForwardPathSimulator(
fitting_data=fit_data,
generation_dates=generate_time_range,
n_paths=num_synthetic,
method="log_gbm",
)
scen_gen.generate()
synthetic_data = scen_gen.simulated_paths.transpose(1, 0, 2).reshape(
scen_gen.n_steps + 1, (scen_gen.n_paths * n_assets)
)
tickers_list = list(input_data.columns)
synthetic_dataframe = pd.DataFrame(synthetic_data, index=generate_time_range)
augmented_data = pd.concat(
[input_data.loc[generate_range[0] : generate_range[1]], synthetic_dataframe],
axis=1,
)
columns = [
ticker + "-" + str(idx)
for idx in range(scen_gen.n_paths)
for ticker in tickers_list
]
tickers_list += columns
augmented_data.columns = tickers_list
return augmented_data

View File

@ -0,0 +1,534 @@
# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utility functions for portfolio optimization and data processing."""
import os
from typing import Optional, Union
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
import yfinance as yf
def get_input_data(filepath):
"""Load input data from file."""
_, file_extension = os.path.splitext(filepath)
file_extension = file_extension.lower()
if file_extension == ".csv":
df = pd.read_csv(filepath, index_col=0)
elif file_extension == ".parquet":
df = pd.read_parquet(filepath)
elif file_extension in [".xls", ".xlsx"]:
df = pd.read_excel(filepath)
elif file_extension == ".json":
df = pd.read_json(filepath)
else:
raise ValueError(f"Unsupported file extension: {file_extension}")
df = df.dropna(axis=1)
return df
def calculate_returns(
input_dataset: Union[pd.DataFrame, str],
regime_dict: dict = None,
returns_compute_settings: Union[dict, str] = None,
):
"""
preprocess the dat from a particular period of time.
Assuming the returns are log normally distributed, return the mean and
covariance of the log returns and the log returns
Parameters:
:input_dataset: pandas DataFrame or the path to the input dataset
:return_type: str, type of the returns. For example, "LOG" means log returns,
"PNL" means the dataset is already in the format of P&L data.
"NORMAL" means absolute returns.
:regime_dict: dict of the format {'name': , 'range':(start, end)}
:returns_compute_settings: Union[dict, str], dictionary containing returns calculation settings or the return type.
If a string is provided, it is the return type.
If a dictionary is provided, it contains the following keys:
- "return_type": str, type of the returns. For example, "LOG" means log returns,
- "freq": int, frequency of the returns. For example, freq = 1 means daily returns.
- "returns_compute_device": str, device to use for returns calculation. For example, "GPU" or "CPU".
- "verbose": bool, whether to print verbose output.
"""
# set the default values for the returns calculation settings
if returns_compute_settings.get("returns_compute_device") is None:
returns_compute_settings["returns_compute_device"] = "CPU"
if returns_compute_settings.get("verbose") is None:
returns_compute_settings["verbose"] = False
if returns_compute_settings.get("freq") is None:
returns_compute_settings["freq"] = 1
if returns_compute_settings.get("return_type") is None:
returns_compute_settings["return_type"] = "LOG"
return_type = returns_compute_settings["return_type"].upper()
freq = returns_compute_settings["freq"]
if isinstance(input_dataset, str):
input_data = get_input_data(input_dataset)
else:
input_data = input_dataset
if regime_dict is None:
input_data = input_data
else:
start, end = regime_dict["range"]
input_data = input_data.loc[start:end]
input_data = input_data.dropna(axis=1)
if return_type == "LOG":
returns_dataframe = calculate_log_returns(input_data, freq)
elif return_type == "PNL":
returns_dataframe = input_data
elif return_type == "NORMAL":
returns_dataframe = compute_abs_returns(input_data, freq)
else:
raise NotImplementedError("Invalid return type!")
returns_array = returns_dataframe.to_numpy()
m = np.mean(returns_array, axis=0)
cov = np.cov(returns_array.transpose())
returns_dict = {
"return_type": return_type,
"returns": returns_dataframe,
"regime": regime_dict,
"dates": returns_dataframe.index,
"mean": m,
"covariance": cov,
"tickers": list(input_data.columns),
}
return returns_dict
def calculate_log_returns(price_data, freq=1):
"""compute the log returns given a price dataframe"""
# compute the log returns
returns_dataframe = price_data.apply(np.log) - price_data.shift(freq).apply(np.log)
returns_dataframe = returns_dataframe.dropna(how="all")
returns_dataframe = returns_dataframe.fillna(0)
return returns_dataframe
def compute_abs_returns(price_data, freq=1):
"""
compute the absolute returns using freq. For example, freq = 1 means today - yesterday.
"""
returns_dataframe = price_data.diff(freq)
returns_dataframe = returns_dataframe.dropna(how="all")
returns_dataframe = returns_dataframe.fillna(0)
return returns_dataframe
def plot_efficient_frontier(
risk_measure,
result_dataframe,
single_asset_portfolio,
custom_portfolios,
key_portfolios,
verbose=False,
title=None,
show_plot=True,
EF_plot_png_name=None,
notional=1e7,
):
"""
plot the efficient frontier using the optimization results of different
risk-aversion levels in Seaborn.
Parameters:
:risk_measure: str
:result_dataframe: Pandas DataFrame - (num_risks_levels, ?) where each row
records the result of the optimization w.r.t. a certain risk level
:single_asset_portfolio: Pandas DataFrame - (n_assets, #performance metrics)
each row records the performance of the portfolio made up of one single asset
:key_portfolios: dict - {portfolio_name: marker} of names of the portfolios
(and corresponding markers) to highlight on the efficient frontier
(e.g. min var, max Sharpe, max return, etc.)
:custom_portfolios: Pandas DataFrame - (#user inputs, #performance metrics)
each row records the performance of a custom portfolio from user input
:show_plot: bool - whether to show plot
:EF_plot_png_name: str - save the figure under the name EF_plot_png_name
"""
# Apply consistent styling
plt.style.use("seaborn-v0_8-whitegrid")
sns.set_context("paper", font_scale=0.9)
sns.set_palette(palette="Blues_d")
plt.figure(figsize=(10, 7), dpi=300)
# Create scaled versions of the data for plotting
result_dataframe_scaled = result_dataframe.copy()
result_dataframe_scaled[f"{risk_measure}_percent"] = (
result_dataframe_scaled[risk_measure] * 100
)
result_dataframe_scaled["return_scaled"] = (
result_dataframe_scaled["return"] * notional
)
if key_portfolios is not None:
# plot the markers for the key portfolios
example_portfolio = pd.DataFrame({}, columns=result_dataframe.columns)
for portfolio_name, marker in key_portfolios.items():
portfolio_idx = get_portfolio(result_dataframe, portfolio_name)
example_portfolio = pd.concat(
[example_portfolio, result_dataframe.iloc[portfolio_idx].to_frame().T]
)
portfolio_data_scaled = (
result_dataframe_scaled.iloc[portfolio_idx].to_frame().T
)
sns.scatterplot(
data=portfolio_data_scaled,
x=f"{risk_measure}_percent",
y="return_scaled",
marker=marker,
s=100,
color="darkorange",
label=portfolio_name,
legend=True,
zorder=2,
)
example_portfolio = example_portfolio.reset_index()
if verbose:
# create the annotation box for the key portfolios
_ = [] # annotated_points (unused)
_ = [] # annotation_list (unused)
offset_list = [(-15, -150), (20, -70), (-15, -70)]
for row_idx, row in example_portfolio.iterrows():
point = (row.loc[risk_measure] * 100, row.loc["return"] * notional)
annotation = ""
weights_dict, cash = row["optimal portfolio"]
for ticker, weight in weights_dict.items():
if weight > 5e-2 or weight < -5e-2:
annotation += ticker + f": {weight: .2f}\n"
annotation += f"cash: {cash: .2f}"
annotation = annotation.rstrip("\n")
plt.annotate(
annotation,
xy=point,
ha="left",
xytext=offset_list[row_idx],
textcoords="offset points",
fontsize=8,
bbox=dict(
boxstyle="round,pad=0.4", facecolor="#e8dff5", edgecolor="black"
),
arrowprops=dict(
arrowstyle="->", connectionstyle="arc3,rad=0.3", color="black"
),
)
# create line for efficient frontier
sns.lineplot(
data=result_dataframe_scaled,
x=f"{risk_measure}_percent",
y="return_scaled",
linewidth=3,
zorder=1,
label="Optimal Portfolios",
)
plt.legend()
custom_portfolio_markers = ["s", "^", "v", "<", ">", "p", "h"]
if not custom_portfolios.empty:
for i in range(0, len(custom_portfolios)):
portfolio = custom_portfolios.iloc[i]
annotation = portfolio["portfolio_name"]
plt.scatter(
x=portfolio[risk_measure] * 100, # Convert to percentage
y=portfolio["return"] * notional, # Scale by notional
marker=custom_portfolio_markers[i],
color=".2",
zorder=4,
label=annotation,
)
plt.legend()
# scatter plot the single asset portfolios
single_asset_scaled = single_asset_portfolio.copy()
single_asset_scaled[f"{risk_measure}_percent"] = (
single_asset_scaled[risk_measure] * 100
)
single_asset_scaled["return_scaled"] = single_asset_scaled["return"] * notional
sns.scatterplot(
data=single_asset_scaled,
x=f"{risk_measure}_percent",
y="return_scaled",
hue="variance",
size="variance",
palette="icefire",
legend=False,
zorder=3,
)
for i in range(0, len(single_asset_portfolio)):
plt.annotate(
f"{single_asset_portfolio.index[i]}",
(
single_asset_portfolio[risk_measure][i] * 100,
single_asset_portfolio["return"][i] * notional,
),
textcoords="offset points",
xytext=(2, 3) if i % 2 == 0 else (-4, -6),
fontsize=7,
ha="center",
)
# Set axis labels with proper scaling
plt.xlabel("Conditional Value at Risk (CVaR %)", fontsize=10)
plt.ylabel(f"Expected Return (${notional / 1e6:.0f}M Notional)", fontsize=10)
if not title:
plt.title(
f"Efficient Frontier with {len(single_asset_portfolio)} Stocks",
fontsize=11,
pad=15,
)
else:
plt.title(title, fontsize=11, pad=15)
if EF_plot_png_name:
plt.savefig(EF_plot_png_name)
if show_plot:
plt.show()
def get_portfolio(result, portfolio_name):
"""Extract specific portfolio from optimization results."""
portfolio_name = portfolio_name.lower()
if portfolio_name == "min_var":
min_value = result["risk"].min()
idx = result[result["risk"] == min_value].index[0]
elif portfolio_name == "max_sharpe":
max_sharpe = result["sharpe"].max()
idx = result[result["sharpe"] == max_sharpe].index[0]
elif portfolio_name == "max_return":
max_return = result["return"].max()
idx = result[result["return"] == max_return].index[-1]
else:
raise ValueError(
"portfolio_name should be a string (e.g. min_var, max_sharpe, max_return)"
)
return idx
def portfolio_plot_with_backtest(
portfolio,
backtester,
cut_off_date,
backtest_plot_title,
save_plot=False,
results_dir="results",
):
"""
Create side-by-side portfolio allocation and backtest performance plots.
Displays portfolio allocation as a horizontal bar chart alongside
cumulative returns comparison with benchmarks.
Parameters
----------
portfolio : Portfolio
Portfolio object to display allocation for
backtester : portfolio_backtester
Backtester object containing test portfolio and benchmarks
cut_off_date : str
Date to mark with vertical line on backtest plot
backtest_plot_title : str
Title for the backtest plot
save_plot : bool, default False
Whether to save the combined plot to results directory
results_dir : str, default "results"
Directory path where plots will be saved
"""
# Apply consistent styling without whitegrid for portfolio plot
sns.set_context("paper", font_scale=0.9)
# Create subplots with appropriate sizing for side-by-side display
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 8), dpi=300)
# Plot portfolio allocation
ax1 = portfolio.plot_portfolio(ax=ax1, show_plot=False)
# Completely reset and apply very subtle grid to portfolio plot
ax1.grid(False) # Turn off any existing grid first
ax1.grid(True, axis="x", alpha=0.1, color="#E0E0E0", linestyle="-", linewidth=0.3)
ax1.spines["top"].set_visible(False)
ax1.spines["right"].set_visible(False)
ax1.spines["left"].set_color("#E0E0E0")
ax1.spines["bottom"].set_color("#E0E0E0")
ax1.set_axisbelow(True)
# Apply whitegrid style only to backtest plot
with plt.style.context("seaborn-v0_8-whitegrid"):
# Plot backtest results
_, ax2 = backtester.backtest_against_benchmarks(
plot_returns=True,
ax=ax2,
cut_off_date=cut_off_date,
title=backtest_plot_title,
save_plot=False,
)
# Ensure backtest grid is subtle and consistent
ax2.grid(True, alpha=0.1, color="#E0E0E0", linewidth=0.3)
ax2.set_axisbelow(True)
plt.tight_layout()
# Save combined plot if requested
if save_plot:
import os
# Create results directory if it doesn't exist
os.makedirs(results_dir, exist_ok=True)
# Generate filename
portfolio_name = (
portfolio.name.replace(" ", "_").lower() if portfolio.name else "portfolio"
)
test_method = backtester.test_method.replace("_", "")
filename = f"combined_{portfolio_name}_{test_method}_analysis.png"
filepath = os.path.join(results_dir, filename)
# Save with high quality
plt.savefig(
filepath,
dpi=300,
bbox_inches="tight",
facecolor="white",
edgecolor="none",
)
print(f"Combined plot saved: {filepath}")
plt.show()
def compare_results(gpu_results, cpu_results):
"""
Compare and display results from GPU and CPU solvers in tabular format.
Args:
gpu_results: Results from GPU solver
cpu_results: Results from CPU solver
"""
print("\n" + "=" * 60)
print("SOLVER COMPARISON")
print("=" * 60)
# Collect all available results
solvers = []
if gpu_results is not None:
# Determine GPU solver name based on results structure or default to cuOpt
gpu_name = "cuOpt (GPU)" # Default name for GPU results
solvers.append((gpu_name, gpu_results))
if cpu_results is not None:
solvers.append((f"{cpu_results['solver']} (CPU)", cpu_results))
if len(solvers) == 0:
print("No results available from any solver")
return
# Print header
print(
f"{'Solver':<15} {'Solve Time (s)':<15} {'Objective':<12} "
f"{'Return':<10} {'CVaR':<10}"
)
print("-" * 65)
# Print results for each solver
for solver_name, results in solvers:
solve_time = results.get("solve time", 0)
objective = results.get("obj", 0)
portfolio_return = results.get("return", 0)
cvar = results.get("CVaR", 0)
print(
f"{solver_name:<15} {solve_time:<15.4f} {objective:<12.6f} "
f"{portfolio_return:<10.6f} {cvar:<10.6f}"
)
# Calculate and display objective differences if multiple results available
if len(solvers) > 1:
print("\nObjective Differences:")
for i in range(len(solvers)):
for j in range(i + 1, len(solvers)):
solver1_name, results1 = solvers[i]
solver2_name, results2 = solvers[j]
obj_diff = abs(results1.get("obj", 0) - results2.get("obj", 0))
print(f"{solver1_name} vs {solver2_name}: {obj_diff:.8f}")
print() # Add blank line for better readability
def download_data(dataset_dir):
"""
Download the data for the given dataset name.
"""
tickers = [
'A', 'AAPL', 'ABT', 'ACGL', 'ACN', 'ADBE', 'ADI', 'ADM', 'ADP', 'ADSK', 'AEE', 'AEP', 'AES', 'AFL', 'AIG', 'AIZ', 'AJG', 'AKAM', 'ALB', 'ALGN',
'ALL', 'AMAT', 'AMD', 'AME', 'AMGN', 'AMT', 'AMZN', 'AON', 'AOS', 'APA', 'APD', 'APH', 'ARE', 'ATO', 'AVB', 'AVY', 'AXON', 'AXP', 'AZO',
'BA', 'BAC', 'BALL', 'BAX', 'BBWI', 'BBY', 'BDX', 'BEN', 'BG', 'BIIB', 'BIO', 'BK', 'BKNG', 'BKR', 'BLK', 'BMY', 'BRO', 'BSX', 'BWA', 'BXP',
'C', 'CAG', 'CAH', 'CAT', 'CB', 'CBRE', 'CCI', 'CCL', 'CDNS', 'CHD', 'CHRW', 'CI', 'CINF', 'CL', 'CLX', 'CMA', 'CMCSA', 'CME', 'CMI', 'CMS',
'CNC', 'CNP', 'COF', 'COO', 'COP', 'COR', 'COST', 'CPB', 'CPRT', 'CPT', 'CRL', 'CRM', 'CSCO', 'CSGP', 'CSX', 'CTAS', 'CTRA', 'CTSH', 'CVS', 'CVX',
'D', 'DD', 'DE', 'DECK', 'DGX', 'DHI', 'DHR', 'DIS', 'DLR', 'DLTR', 'DOC', 'DOV', 'DPZ', 'DRI', 'DTE', 'DUK', 'DVA', 'DVN',
'EA', 'EBAY', 'ECL', 'ED', 'EFX', 'EG', 'EIX', 'EL', 'ELV', 'EMN', 'EMR', 'EOG', 'EQIX', 'EQR', 'EQT', 'ES', 'ESS', 'ETN', 'ETR', 'EVRG',
'EW', 'EXC', 'EXPD', 'EXR', 'F', 'FAST', 'FCX', 'FDS', 'FDX', 'FE', 'FFIV', 'FI', 'FICO', 'FIS', 'FITB', 'FMC', 'FRT',
'GD', 'GE', 'GEN', 'GILD', 'GIS', 'GL', 'GLW', 'GOOG', 'GOOGL', 'GPC', 'GPN', 'GRMN', 'GS', 'GWW',
'HAL', 'HAS', 'HBAN', 'HD', 'HIG', 'HOLX', 'HON', 'HPQ', 'HRL', 'HSIC', 'HST', 'HSY', 'HUBB', 'HUM',
'IBM', 'IDXX', 'IEX', 'IFF', 'ILMN', 'INCY', 'INTC', 'INTU', 'IP', 'IPG', 'IRM', 'ISRG', 'IT', 'ITW', 'IVZ',
'J', 'JBHT', 'JBL', 'JCI', 'JKHY', 'JNJ', 'JPM', 'K', 'KEY', 'KIM', 'KLAC', 'KMB', 'KMX', 'KO', 'KR',
'L', 'LEN', 'LH', 'LHX', 'LIN', 'LKQ', 'LLY', 'LMT', 'LNT', 'LOW', 'LRCX', 'LUV', 'LVS',
'MAA', 'MAR', 'MAS', 'MCD', 'MCHP', 'MCK', 'MCO', 'MDLZ', 'MDT', 'MET', 'MGM', 'MHK', 'MKC', 'MKTX', 'MLM', 'MMC', 'MMM', 'MNST', 'MO', 'MOH',
'MOS', 'MPWR', 'MRK', 'MS', 'MSFT', 'MSI', 'MTB', 'MTCH', 'MTD', 'MU',
'NDAQ', 'NDSN', 'NEE', 'NEM', 'NFLX', 'NI', 'NKE', 'NOC', 'NRG', 'NSC', 'NTAP', 'NTRS', 'NUE', 'NVDA', 'NVR',
'O', 'ODFL', 'OKE', 'OMC', 'ON', 'ORCL', 'ORLY', 'OXY',
'PAYX', 'PCAR', 'PCG', 'PEG', 'PEP', 'PFE', 'PFG', 'PG', 'PGR', 'PH', 'PHM', 'PKG', 'PLD', 'PNC', 'PNR', 'PNW', 'POOL', 'PPG', 'PPL', 'PRU',
'PSA', 'PTC', 'PWR', 'QCOM',
'RCL', 'REG', 'REGN', 'RF', 'RHI', 'RJF', 'RL', 'RMD', 'ROK', 'ROL', 'ROP', 'ROST', 'RSG', 'RTX', 'RVTY',
'SBAC', 'SBUX', 'SCHW', 'SHW', 'SJM', 'SLB', 'SNA', 'SNPS', 'SO', 'SPG', 'SPGI', 'SRE', 'STE', 'STLD', 'STT', 'STX', 'STZ', 'SWK', 'SWKS', 'SYK',
'SYY', 'T', 'TAP', 'TDY', 'TECH', 'TER', 'TFC', 'TFX', 'TGT', 'TJX', 'TMO', 'TPR', 'TRMB', 'TROW', 'TRV', 'TSCO', 'TSN', 'TT', 'TTWO', 'TXN',
'TXT', 'TYL', 'UDR', 'UHS', 'UNH', 'UNP', 'UPS', 'URI', 'USB',
'VLO', 'VMC', 'VRSN', 'VRTX', 'VTR', 'VTRS', 'VZ',
'WAB', 'WAT', 'WDC', 'WEC', 'WELL', 'WFC', 'WM', 'WMB', 'WMT', 'WRB', 'WST', 'WTW', 'WY', 'WYNN',
'XEL', 'XOM', 'YUM', 'ZBH', 'ZBRA'
]
start_date = "2005-01-01"
end_date = "2025-01-01"
data = yf.download(tickers, start=start_date, end=end_date, timeout = 30)
data = data['Close'].dropna(axis = 1)
data.to_csv(dataset_dir)

View File

@ -0,0 +1,9 @@
#!/bin/bash
# Get the script's directory, handling symlinks and different invocation methods
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
PARENT_DIR="${SCRIPT_DIR%/*}"
# Print the location
echo "Starting container with volumized data folder at: $PARENT_DIR"
docker run --gpus all --pull always --rm -it --shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 -p 8888:8888 -p 8787:8787 -p 8786:8786 -p 8501:8501 -p 8050:8050 -v $PARENT_DIR:/home/rapids/notebooks/playbook nvcr.io/nvidia/rapidsai/notebooks:25.10-cuda13-py3.13 bash /home/rapids/notebooks/playbook/setup/setup_playbook.sh

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,183 @@
# Single-cell RNA Sequencing
> An end-to-end GPU-powered workflow for scRNA-seq using RAPIDS
## Table of Contents
- [Overview](#overview)
- [Instructions](#instructions)
- [Troubleshooting](#troubleshooting)
---
## Overview
## Basic idea
Single-cell RNA sequencing (scRNA-seq) lets researchers study gene activity in each cell on its own, exposing variation, cell types, and cell states that bulk methods hide. But these large, high-dimensional datasets take heavy compute to handle.
This playbook shows an end-to-end GPU-powered workflow for scRNA-seq using [RAPIDS-singlecell](https://rapids-singlecell.readthedocs.io/en/latest/), a RAPIDS powered library in the [scverse® ecosystem](https://github.com/scverse). It follows the familiar [Scanpy API](https://scanpy.readthedocs.io/en/stable/) and lets researchers run the steps of data preprocessing, quality control (QC) and cleanup, visualization, and investigation faster than CPU tools by working with sparse count matrices directly on the GPU.
## What you'll accomplish
1. GPU-Accelerated Data Loading & Preprocessing
2. QC cells visually to understand the data
3. Filter unusual cells
4. Remove unwanted sources of variation
5. Cluster and visualize PCA nd UMAP data
6. Batch Correction and analysis using Harmony, k-nearest neighbors, UMAP, and tSNE
7. Explore the biological information from the data with differential expression analysis and trajectory analysis
The README elaborates on these steps.
## What to know before starting
- The rapids-singlecell library mimics the Scanpy API from scverse, allowing users familiar with the standard CPU workflow to easily adapt to GPU acceleration through cuPy and NVIDIA RAPIDS cuML and cuGraph.
- Algorithmic Precision: Unlike Scanpy's CPU implementation which uses approximate nearest neighbor search, this GPU implementation computes the exact graph; consequently, small differences in results are expected and valid.
- Parameter Sensitivity: When performing t-SNE, the number of nearest neighbors must be at least 3x to avoid distortion
## Prerequisites
**Hardware Requirements:**
- NVIDIA Grace Blackwell GB10 Superchip System (DGX Spark)
- Minimum 40GB Unified memory free for docker container and GPU accelerated data processing
- At least 30GB available storage space for docker container and data files
- High Speed network connectivity
- High speed internet connection recommended
**Software Requirements:**
- NVIDIA DGX OS
- Docker
## Ancillary files
All required assets can be found [in the Single-cell RNA Sequencing repository](https://github.com/NVIDIA/dgx-spark-playbooks/blob/main/nvidia/single-cell/). In the running playbook, they will all be found under the `playbook` folder.
- `scRNA_analysis_preprocessing.ipynb` - Main playbook notebook.
- `README.md` - Quick Start Guide to the Playbook Environment. It will also be found in the main directory of the Jupyter Lab. Please start there!
- `/setup/start_playbook.sh` - Script to start the install of the playbook in a Docker container
- `/setup/setup_playbook.sh` - Configures the Docker container before user enters jupyterlab environment
- `/setup/requirements.txt` - used as a lists of libraries that commands in setup_playbook will install into the playbook environment
## Time & risk
* **Estimated Time** ~15 minutes for first run
- Total Notebook Processing Time: Approximately 2-3 minutes for the full pipeline (~130 seconds recorded in demo).
- Data Loading: ~1.7 seconds.
- Preprocessing: ~21 seconds.
- Post-processing (Clustering/Diff Exp): ~104 seconds.
- Data: Internet access to download the docker container, libraries, and demo dataset (dli_census.h5ad).
* **Risks**
- GPU Memory Constraints: The workflow is very GPU memory intensive. Large datasets may trigger Out Of Memory (OOM) errors.
- Kernel Management: You may need to kill/restart kernels to free up GPU resources between workflow stages.
- Rollback: If an OOM error occurs, kill all kernels to free GPU memory and restart either the specific notebook or the entire playbook.
* **Last Updated:** 01/06/2026
* First Publication
## Instructions
## Step 1. Verify your environment
Let's first verify that you have a working GPU, git, and Docker. Open up Terminal, then copy and paste in the below commands:
```bash
nvidia-smi
git --version
docker --version
```
- `nvidia-smi` will output information about your GPU. If it doesn't, your GPU is not properly configured.
- `git --version` will print something like `git version 2.43.0`. If you get an error saying that git is not installed, please reinstall it.
- `docker --version` will print something like `Docker version 28.3.3, build 980b856`. If you get an error saying that Docker is not installed, please reinstall it.
## Step 2. Installation
Open up Terminal, then copy and paste in the below commands:
```bash
git clone https://github.com/NVIDIA/dgx-spark-playbooks
cd dgx-spark-playbooks/nvidia/single-cell/assets
bash ./setup/start_playbook.sh
```
start_playbook.sh will:
1. pull the RAPIDS 25.10 Notebooks Docker container
2. build all the environments needed for the playbook in the container using `setup_playbook.sh`
3. start Jupyterlab
Please keep the Terminal window open while using the playbook.
You can access your Jupyterlab server in two ways
1. at `http://127.0.0.1:8888` if running locally on the DGX Spark.
2. at `http://<SPARK_IP>:8888` if using your DGX Spark headless over your network.
Once in Jupyterlab, you'll be greeted with a directory containing `scRNA_analysis_preprocessing.ipynb`, and the folders `cuDF`, `cuML`, `cuGraph`, and `playbook`.
- `scRNA_analysis_preprocessing.ipynb`is the playbook notebook. You will want to open this by double clicking on the file.
- `cuDF`, `cuML`, `cuGraph` folders contain the standard RAPIDS libary example notebooks to help you continue exploring.
- `playbook` contains the playbook files. The contents of this folder are read-only inside of a rootless Docker Container.
If you want to install any of the playbook notebooks on your own system, check out the readmes within the folder that accompanies the notebook
## Step 3. Run the notebook
Once in jupyterlab, there all you have to do is run the `scRNA_analysis_preprocessing.ipynb`. You'll get both these playbook notebooks as well as the standard RAPIDS libary example notebooks to help you get going.
You can use `Shift + Enter` to manually run each cell at your own pace, or `Run > Run All` to run all the cells.
Once you're done with exploring the `scRNA_analysis_preprocessing` notebook, you can explore other RAPIDS notebooks by going into the folders, selecting other notebooks, and doing the same thing.
## Step 4. Download your work
Since the docker container cannot priviledged write back to the host system, you can use Jupyterlab to download any files you may want to keep once the docker container is shut down.
Simply right click the file you want, in the browser, and click `Download` in the drop down.
## Step 5. Cleanup
Once you have downloaded all your work, Go back to the Terminal window where you started running the playbook.
In the Terminal window,
1. Type `Ctrl + C`
2. Quickly either enter `y` and then hit `Enter` at the prompt or hit `Ctrl + C` again
3. The Docker container will proceed to shut down
{When and why someone might need this step.}
> [!WARNING]
> This will delete ALL data that wasn't already downloaded from the Docker container. The browser window may still show cached files if it is still open.
## Troubleshooting
<!--
TROUBLESHOOTING TEMPLATE: Although optional, this resource can significantly help users resolve common issues.
Replace all placeholder content in {} with your actual troubleshooting information.
Remove these comment blocks when you're done.
PURPOSE: Provide quick solutions to problems users are likely to encounter.
FORMAT: Use the table format for easy scanning. Add detailed notes when needed.
-->
| Symptom | Cause | Fix |
|---------|-------|-----|
| Docker is not found. | Docker may have been uninstalled, as it is preinstalled on your DGX Spark | Please install Docker using their convenience script here: `curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh`. You will be prompted for your password. |
| Docker command unexpectedly exits with "permissions" error | Your user is not part of the `docker` group | Open Terminal and run these commands: `sudo groupadd docker $$ sudo usermod -aG docker $USER`. You will be prompted for your password. Then, close the Terminal, open a new one, and try again |
| Docker container download, environment build, or data download fails | There was either a connectivity issue or a resource may be temporariliy unavailable. | You may need to try again later. If this persist, please reach out to us! |
<!--
Space reserved for some common known issues that might be relevant to your project. Assess potential consequences before changing or deleting.
-->
> [!NOTE]
> DGX Spark uses a Unified Memory Architecture (UMA), which enables dynamic memory sharing between the GPU and CPU.
> With many applications still updating to take advantage of UMA, you may encounter memory issues even when within
> the memory capacity of DGX Spark. If that happens, manually flush the buffer cache with:
```bash
sudo sh -c 'sync; echo 3 > /proc/sys/vm/drop_caches'
```
For latest known issues, please review the [DGX Spark User Guide](https://docs.nvidia.com/dgx/dgx-spark/known-issues.html).

View File

@ -0,0 +1,116 @@
# **START HERE with the Single-Cell Analytics Playbook**
___
## **Table of Contents**
___
<br>
- [Get Started Now](#Get-Started-Now!)
- [Playbook Structure](#Playbook-Structure)
- [Next Steps](#Next-Steps)
<br>
___
## **Get Started Now!**
___
<br>
The **[scRNA Analysis Preprocessing Notebook](scRNA_analysis_preprocessing.ipynb)** is an end to end GPU-accelerated single-cell analysis workflow using [RAPIDS-singlecell](https://rapids-singlecell.readthedocs.io/en/latest/), a GPU accelerated library developed by [scverse®](https://github.com/scverse). In this notebook, we understand the cells, run ETL on the data set then visiualize and explore the results. It should take less than 3 minutes to complete the workflow.
### [CLICK HERE TO BEGIN](scRNA_analysis_preprocessing.ipynb)
Using the DGX Spark can help you easily GPU Accelerate your Data Science and Machine Learning based workflows using the [RAPIDS Open Source ecosystem](https://rapids.ai) so that you can go from data to information to insights faster than ever before!
![cells](assets/rsc.png)
# <div align="left"><img src="https://rapids.ai/assets/images/rapids_logo.png" width="90px"/>&nbsp; <div align="left"><img src="https://canada1.discourse-cdn.com/flex035/uploads/forum11/original/1X/dfb6d71c9b8deb73aa10aa9bc47a0f8948d5304b.png" width="90px"/>&nbsp;
<br>
___
## **Deep Dive**
___
<br>
This Notebook is for those who are new to doing basic analysis for single cell data, as the end to end analysis of is the best place to start, where you are walked through the steps of data preprocessing, quality control (QC) and cleanup, visualization, and investigation. Let's deep dive the process!
![layout architecture](assets/scdiagram.png)
1. Load and Preprocess the data
- Load a sparse matrix in h5ad format using Scanpy
- Preprocess the data, implementing standard QC metrics to assess cell and gene quality per cell, as well as per gen
2. QC cells visually to understand the data
- Users will learn how to visually inspect 5 different plots that help reflect quality control metrics for single cell data to:
- Identify stressed or dying cells undergoing apoptosis
- Empty droplets or dead cells
- Cells with abnormal gene counts
- Low quality or overly dominant cells
3. Filter unusual cells
- Users will learn how to remove cells with an extreme number of genes expressed
- Users will filter out cells with an unusual amount of mitochondrial content
4. Remove unwanted sources of variation
- Select most variable genes to better inform analysis and increase computational efficiency
- Regress out additional technical variation that we observed in the visual plots (Note, this can actually remove biologically relevant information, and would need to be carefully considered with a more complex data set)
- Standardize by using a z-score transformation
5. Cluster and visualize data
- Implement PCA to reduce computational complexity. We use the GPU-accelerated PCA implementation from cuML, which significantly speeds up computation compared to CPU-based methods.
- Identify batch effects visually by generating a UMAP plot with graph-based clustering
6. Batch Correction and analysis
- Remove assay-specific batch effects using Harmony
- Re-compute the k-nearest neighbors graph and visualize using the UMAP.
- Perform graph-based clustering
- Visualize using other methods (tSNE)
7. Explore the biological information from the data
- Differential expression analysis: Identifying marker genes for cell types
- Implement logistic regression
- Rank genes that distinguish cell types
- Trajectory analysis
- Implement a diffusion map to understand the progress of cell types
These notebooks will be valuable for single-cell scientists who want to quickly evaluate ease of use as well as explore the biological interpretability of RAPIDS-singlecell results. Secondarily, scientists will find value in learning to apply these methods to very large data sets. This repository is also broadly useful for any data scientist or developer who wants to run and evaluate single cell methods leveraging RAPIDS-singlecell. Data sets used for this tutorial were made [publicly available by 10X](https://www.10xgenomics.com/datasets) as well as [CZ cellxgene](https://cellxgene.cziscience.com/).
If you like this notebook and the GPU accelerated capability, please do these two things:
1. Explore the rest of the single cell notebooks, through the [Single Cell Analysis AI Blueprint](https://github.com/NVIDIA-AI-Blueprints/single-cell-analysis-blueprint/tree/main)
1. Support scverse's efforts by please [learn more about them here](https://scverse.org/about/) as well as [consider joining their community](https://scverse.org/join/).
<br>
<br>
___
## **Directory Structure**
___
<br>
- **[scRNA_analysis_preprocessing.ipynb](scRNA_analysis_preprocessing.ipynb)** - Main playbook notebook
- `START_HERE.md` - Quick Start Guide to the Playbook Environment. It will also be found in the main directory of the Jupyter Lab. Please start there!
- `cuDF, cuML, and cuGraph folders` - more example notebooks to continue your GPU Accelerated Data Science Journey.
<br>
___
## **DIY Installation**
___
<br>
If you like what you see and want to run more GPU accelerated for Genomics, BioInfomatics, or Single Cell research, please do the following
```bash
pip install -r ./setup/requirements.txt
```
Inside this requirements file, are some pinned versions of all the libraries needed. When `pinned`, it will only download that specific version, which will ensure that you have stablity as a product. When `unpinned`, pip or uv will download the latest versions where everything should work. If you're planning to upgrade to the lastest technological stack, you should unpin the libaries, however your mileage may vary.
<br>
<br>
___
## **Support**
___
<br>
If you have any questions about these notebooks or need support, please open an Issue on the [Single Cell Analysis AI Blueprint](https://github.com/NVIDIA-AI-Blueprints/single-cell-analysis-blueprint/tree/main) repository and we will respond there.

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,42 @@
anndata==0.12.6
array-api-compat==1.12.0
contourpy==1.3.2
cycler==0.12.1
fonttools==4.58.0
h5py==3.13.0
imageio==2.37.0
joblib==1.5.1
kiwisolver==1.4.8
lazy-loader==0.4
legacy-api-wrap==1.4.1
llvmlite==0.44.0
matplotlib==3.10.6
natsort==8.4.0
networkx==3.5
numba==0.61.2
numpy==2.2.6
optuna==4.6.0
pandas==2.3.2
patsy==1.0.1
pillow==11.3.0
pynndescent==0.5.13
rapids-singlecell==0.13.4
scanpy==1.11.3
scikit-learn==1.7.2
scikit-image==0.24.0
scikit-misc==0.5.2
scipy==1.15.3
seaborn==0.13.2
session-info2==0.2.3
statsmodels==0.14.4
streamlit==1.51.0
threadpoolctl==3.6.0
tifffile==2025.5.10
tqdm==4.67.1
tzdata==2025.2
umap-learn==0.5.9.post2
wget==3.2
deprecated==1.2.18
numcodecs==0.15.1
wrapt==1.17.2
zarr==3.1.5

View File

@ -0,0 +1,16 @@
#/bin/bash
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
uv --version
mamba install -c conda-forge compilers -y
cd /home/rapids/notebooks/playbook/setup
uv pip install --system -r requirements.txt
cp -r ~/notebooks/playbook/assets ~/notebooks/assets
cp ~/notebooks/playbook/README.md ~/notebooks/START_HERE.md
cp ~/notebooks/playbook/scRNA_analysis_preprocessing.ipynb ~/notebooks/scRNA_analysis_preprocessing.ipynb
set -m
# Start the primary process and put it in the background
jupyter-lab --notebook-dir=/home/rapids/notebooks/ --ip=0.0.0.0 --no-browser --NotebookApp.token='' --NotebookApp.allow_origin='*'

View File

@ -0,0 +1,9 @@
#!/bin/bash
# Get the script's directory, parent directory, and handle symlinks and different invocation methods
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
PARENT_DIR="${SCRIPT_DIR%/*}"
# Print the location
echo "Starting container with volumized data folder at: $PARENT_DIR"
docker run --gpus all --pull always --rm -it --shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 -p 8888:8888 -p 8787:8787 -p 8786:8786 -p 8501:8501 -p 8050:8050 -v $PARENT_DIR:/home/rapids/notebooks/playbook nvcr.io/nvidia/rapidsai/notebooks:25.10-cuda13-py3.13 bash /home/rapids/notebooks/playbook/setup/setup_playbook.sh

View File

@ -6,11 +6,8 @@
- [Overview](#overview)
- [Instructions](#instructions)
- [Step 1. Configure Docker permissions](#step-1-configure-docker-permissions)
- [Step 2. Run draft-target speculative decoding](#step-2-run-draft-target-speculative-decoding)
- [Step 3. Test the draft-target setup](#step-3-test-the-draft-target-setup)
- [Step 5. Cleanup](#step-5-cleanup)
- [Step 6. Next Steps](#step-6-next-steps)
- [Option 1: EAGLE-3](#option-1-eagle-3)
- [Option 2: Draft Target](#option-2-draft-target)
- [Troubleshooting](#troubleshooting)
---
@ -24,7 +21,7 @@ This way, the big model doesn't need to predict every token step-by-step, reduci
## What you'll accomplish
You'll explore speculative decoding using TensorRT-LLM on NVIDIA Spark using the traditional Draft-Target approach.
You'll explore speculative decoding using TensorRT-LLM on NVIDIA Spark using two approaches: EAGLE-3 and Draft-Target.
These examples demonstrate how to accelerate large language model inference while maintaining output quality.
## What to know before starting
@ -40,13 +37,9 @@ These examples demonstrate how to accelerate large language model inference whil
- Docker with GPU support enabled
```bash
docker run --gpus all nvcr.io/nvidia/tensorrt-llm/release:spark-single-gpu-dev nvidia-smi
```
- HuggingFace authentication configured (if needed for model downloads)
```bash
huggingface-cli login
docker run --gpus all nvcr.io/nvidia/tensorrt-llm/release:1.2.0rc6 nvidia-smi
```
- Active HuggingFace Token for model access
- Network connectivity for model downloads
@ -55,12 +48,13 @@ These examples demonstrate how to accelerate large language model inference whil
* **Duration:** 10-20 minutes for setup, additional time for model downloads (varies by network speed)
* **Risks:** GPU memory exhaustion with large models, container registry access issues, network timeouts during downloads
* **Rollback:** Stop Docker containers and optionally clean up downloaded model cache.
* **Last Updated:** 10/12/2025
* First publication
* **Last Updated:** 01/02/2026
* Upgrade to latest container v1.2.0rc6
* Add EAGLE-3 Speculative Decoding example with GPT-OSS-120B
## Instructions
### Step 1. Configure Docker permissions
## Step 1. Configure Docker permissions
To easily manage containers without sudo, you must be in the `docker` group. If you choose to skip this step, you will need to run Docker commands with sudo.
@ -77,16 +71,88 @@ sudo usermod -aG docker $USER
newgrp docker
```
## Step 2. Set Environment Variables
### Step 2. Run draft-target speculative decoding
Set up the environment variables for downstream services:
Execute the following command to set up and run traditional speculative decoding:
```bash
export HF_TOKEN=<your_huggingface_token>
```
## Step 3. Run Speculative Decoding Methods
### Option 1: EAGLE-3
Run EAGLE-3 Speculative Decoding by executing the following command:
```bash
docker run \
-e HF_TOKEN=$HF_TOKEN \
-v $HOME/.cache/huggingface/:/root/.cache/huggingface/ \
--rm -it --ulimit memlock=-1 --ulimit stack=67108864 \
--gpus=all --ipc=host --network host nvcr.io/nvidia/tensorrt-llm/release:spark-single-gpu-dev \
--gpus=all --ipc=host --network host \
nvcr.io/nvidia/tensorrt-llm/release:1.2.0rc6 \
bash -c '
hf download openai/gpt-oss-120b && \
hf download nvidia/gpt-oss-120b-Eagle3-long-context \
--local-dir /opt/gpt-oss-120b-Eagle3/ && \
cat > /tmp/extra-llm-api-config.yml <<EOF
enable_attention_dp: false
disable_overlap_scheduler: false
enable_autotuner: false
cuda_graph_config:
max_batch_size: 1
speculative_config:
decoding_type: Eagle
max_draft_len: 5
speculative_model_dir: /opt/gpt-oss-120b-Eagle3/
kv_cache_config:
free_gpu_memory_fraction: 0.9
enable_block_reuse: false
EOF
export TIKTOKEN_ENCODINGS_BASE="/tmp/harmony-reqs" && \
mkdir -p $TIKTOKEN_ENCODINGS_BASE && \
wget -P $TIKTOKEN_ENCODINGS_BASE https://openaipublic.blob.core.windows.net/encodings/o200k_base.tiktoken && \
wget -P $TIKTOKEN_ENCODINGS_BASE https://openaipublic.blob.core.windows.net/encodings/cl100k_base.tiktoken
trtllm-serve openai/gpt-oss-120b \
--backend pytorch --tp_size 1 \
--max_batch_size 1 \
--extra_llm_api_options /tmp/extra-llm-api-config.yml'
```
Once the server is running, test it by making an API call from another terminal:
```bash
## Test completion endpoint
curl -X POST http://localhost:8000/v1/completions \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-oss-120b",
"prompt": "Solve the following problem step by step. If a train travels 180 km in 3 hours, and then slows down by 20% for the next 2 hours, what is the total distance traveled? Show all intermediate calculations and provide a final numeric answer.",
"max_tokens": 300,
"temperature": 0.7
}'
```
**Key Features of EAGLE-3 Speculative Decoding**
- **Simpler deployment** — Instead of managing a separate draft model, EAGLE-3 uses a built-in drafting head that generates speculative tokens internally.
- **Better accuracy** — By fusing features from multiple layers of the model, draft tokens are more likely to be accepted, reducing wasted computation.
- **Faster generation** — Multiple tokens are verified in parallel per forward pass, cutting down the latency of autoregressive inference.
### Option 2: Draft Target
Execute the following command to set up and run draft target speculative decoding:
```bash
docker run \
-e HF_TOKEN=$HF_TOKEN \
-v $HOME/.cache/huggingface/:/root/.cache/huggingface/ \
--rm -it --ulimit memlock=-1 --ulimit stack=67108864 \
--gpus=all --ipc=host --network host nvcr.io/nvidia/tensorrt-llm/release:1.2.0rc6 \
bash -c "
# # Download models
hf download nvidia/Llama-3.3-70B-Instruct-FP4 && \
@ -114,8 +180,6 @@ EOF
"
```
### Step 3. Test the draft-target setup
Once the server is running, test it by making an API call from another terminal:
```bash
@ -137,7 +201,7 @@ curl -X POST http://localhost:8000/v1/completions \
- **Memory efficient**: Uses FP4 quantized models for reduced memory footprint
- **Compatible models**: Uses Llama family models with consistent tokenization
### Step 5. Cleanup
## Step 4. Cleanup
Stop the Docker container when finished:
@ -150,7 +214,7 @@ docker stop <container_id>
## rm -rf $HOME/.cache/huggingface/hub/models--*gpt-oss*
```
### Step 6. Next Steps
## Step 5. Next Steps
- Experiment with different `max_draft_len` values (1, 2, 3, 4, 8)
- Monitor token acceptance rates and throughput improvements

View File

@ -1,20 +1,11 @@
# TRT LLM for Inference
> Install and use TensorRT-LLM on DGX Spark Sparks
> Install and use TensorRT-LLM on DGX Spark
## Table of Contents
- [Overview](#overview)
- [Single Spark](#single-spark)
- [Step 1. Configure Docker permissions](#step-1-configure-docker-permissions)
- [Step 2. Verify environment prerequisites](#step-2-verify-environment-prerequisites)
- [Step 3. Set environment variables](#step-3-set-environment-variables)
- [Step 4. Validate TensorRT-LLM installation](#step-4-validate-tensorrt-llm-installation)
- [Step 5. Create cache directory](#step-5-create-cache-directory)
- [Step 6. Validate setup with quickstart_advanced](#step-6-validate-setup-with-quickstartadvanced)
- [Step 7. Validate setup with quickstart_multimodal](#step-7-validate-setup-with-quickstartmultimodal)
- [Step 8. Serve LLM with OpenAI-compatible API](#step-8-serve-llm-with-openai-compatible-api)
- [Step 10. Cleanup and rollback](#step-10-cleanup-and-rollback)
- [Run on two Sparks](#run-on-two-sparks)
- [Step 1. Configure network connectivity](#step-1-configure-network-connectivity)
- [Step 2. Configure Docker permissions](#step-2-configure-docker-permissions)
@ -51,8 +42,7 @@ TRT-LLM integrates with frameworks like Hugging Face and PyTorch, making it easi
## What you'll accomplish
You'll set up TensorRT-LLM to optimize and deploy large language models on NVIDIA Spark with
Blackwell GPUs, achieving significantly higher throughput and lower latency than standard PyTorch
You'll set up TensorRT-LLM to optimize and deploy large language models on your DGX Spark, achieving significantly higher throughput and lower latency than standard PyTorch
inference through kernel-level optimizations, efficient memory layouts, and advanced quantization.
## What to know before starting
@ -65,11 +55,11 @@ inference through kernel-level optimizations, efficient memory layouts, and adva
## Prerequisites
- NVIDIA Spark device with Blackwell architecture GPUs
- DGX Spark device
- NVIDIA drivers compatible with CUDA 12.x: `nvidia-smi`
- Docker installed and GPU support configured: `docker run --rm --gpus all nvcr.io/nvidia/tensorrt-llm/release:spark-single-gpu-dev nvidia-smi`
- Docker installed and GPU support configured: `docker run --rm --gpus all nvcr.io/nvidia/tensorrt-llm/release:1.2.0rc6 nvidia-smi`
- Hugging Face account with token for model access: `echo $HF_TOKEN`
- Sufficient GPU VRAM (16GB+ recommended for 70B models)
- Sufficient GPU VRAM (40GB+ recommended for 70B models)
- Internet connectivity for downloading models and container images
- Network: open TCP ports 8355 (LLM) and 8356 (VLM) on host for OpenAI-compatible serving
@ -78,7 +68,6 @@ inference through kernel-level optimizations, efficient memory layouts, and adva
All required assets can be found [here on GitHub](https://github.com/NVIDIA/dgx-spark-playbooks/blob/main)
- [**trtllm-mn-entrypoint.sh**](https://github.com/NVIDIA/dgx-spark-playbooks/blob/main/nvidia/trt-llm/assets/trtllm-mn-entrypoint.sh) — container entrypoint script for multi-node setup
- [**docker-compose.yml**](https://github.com/NVIDIA/dgx-spark-playbooks/blob/main/nvidia/trt-llm/assets/docker-compose.yml) — Docker Compose configuration for multi-node deployment
## Model Support Matrix
@ -100,10 +89,7 @@ The following models are supported with TensorRT-LLM on Spark. All listed models
| **Phi-4-multimodal-instruct** | NVFP4 | ✅ | `nvidia/Phi-4-multimodal-instruct-FP4` |
| **Phi-4-reasoning-plus** | FP8 | ✅ | `nvidia/Phi-4-reasoning-plus-FP8` |
| **Phi-4-reasoning-plus** | NVFP4 | ✅ | `nvidia/Phi-4-reasoning-plus-FP4` |
| **Llama-3_3-Nemotron-Super-49B-v1_5** | FP8 | ✅ | `nvidia/Llama-3_3-Nemotron-Super-49B-v1_5-FP8` |
| **Qwen3-30B-A3B** | NVFP4 | ✅ | `nvidia/Qwen3-30B-A3B-FP4` |
| **Qwen2.5-VL-7B-Instruct** | FP8 | ✅ | `nvidia/Qwen2.5-VL-7B-Instruct-FP8` |
| **Qwen2.5-VL-7B-Instruct** | NVFP4 | ✅ | `nvidia/Qwen2.5-VL-7B-Instruct-FP4` |
| **Llama-4-Scout-17B-16E-Instruct** | NVFP4 | ✅ | `nvidia/Llama-4-Scout-17B-16E-Instruct-FP4` |
| **Qwen3-235B-A22B (two Sparks only)** | NVFP4 | ✅ | `nvidia/Qwen3-235B-A22B-FP4` |
@ -117,12 +103,13 @@ Reminder: not all model architectures are supported for NVFP4 quantization.
* **Duration**: 45-60 minutes for setup and API server deployment
* **Risk level**: Medium - container pulls and model downloads may fail due to network issues
* **Rollback**: Stop inference servers and remove downloaded models to free resources.
* **Last Updated:** 12/11/2025
* **Last Updated:** 01/02/2026
* Improve TRT-LLM Run on Two Sparks workflow
* Upgrade to the latest TRT-LLM container v1.2.0rc6
## Single Spark
### Step 1. Configure Docker permissions
## Step 1. Configure Docker permissions
To easily manage containers without sudo, you must be in the `docker` group. If you choose to skip this step, you will need to run Docker commands with sudo.
@ -139,7 +126,7 @@ sudo usermod -aG docker $USER
newgrp docker
```
### Step 2. Verify environment prerequisites
## Step 2. Verify environment prerequisites
Confirm your Spark device has the required GPU access and network connectivity for downloading
models and containers.
@ -149,35 +136,36 @@ models and containers.
nvidia-smi
## Verify Docker GPU support
docker run --rm --gpus all nvcr.io/nvidia/tensorrt-llm/release:spark-single-gpu-dev nvidia-smi
docker run --rm --gpus all nvcr.io/nvidia/tensorrt-llm/release:1.2.0rc6 nvidia-smi
```
### Step 3. Set environment variables
Set `HF_TOKEN` for model access.
## Step 3. Set environment variables
```bash
## Set `HF_TOKEN` for model access.
export HF_TOKEN=<your-huggingface-token>
export DOCKER_IMAGE="nvcr.io/nvidia/tensorrt-llm/release:1.2.0rc6"
```
### Step 4. Validate TensorRT-LLM installation
## Step 4. Validate TensorRT-LLM installation
After confirming GPU access, verify that TensorRT-LLM can be imported inside the container.
```bash
docker run --rm -it --gpus all \
nvcr.io/nvidia/tensorrt-llm/release:spark-single-gpu-dev \
$DOCKER_IMAGE \
python -c "import tensorrt_llm; print(f'TensorRT-LLM version: {tensorrt_llm.__version__}')"
```
Expected output:
```
[TensorRT-LLM] TensorRT-LLM version: 1.1.0rc3
TensorRT-LLM version: 1.1.0rc3
[TensorRT-LLM] TensorRT-LLM version: 1.2.0rc6
TensorRT-LLM version: 1.2.0rc6
```
### Step 5. Create cache directory
## Step 5. Create cache directory
Set up local caching to avoid re-downloading models on subsequent runs.
@ -186,7 +174,7 @@ Set up local caching to avoid re-downloading models on subsequent runs.
mkdir -p $HOME/.cache/huggingface/
```
### Step 6. Validate setup with quickstart_advanced
## Step 6. Validate setup with quickstart_advanced
This quickstart validates your TensorRT-LLM setup end-to-end by testing model loading, inference engine initialization, and GPU execution with real text generation. It's the fastest way to confirm everything works before starting the inference API server.
@ -202,7 +190,7 @@ docker run \
-v $HOME/.cache/huggingface/:/root/.cache/huggingface/ \
--rm -it --ulimit memlock=-1 --ulimit stack=67108864 \
--gpus=all --ipc=host --network host \
nvcr.io/nvidia/tensorrt-llm/release:spark-single-gpu-dev \
$DOCKER_IMAGE \
bash -c '
hf download $MODEL_HANDLE && \
python examples/llm-api/quickstart_advanced.py \
@ -222,7 +210,7 @@ docker run \
-v $HOME/.cache/huggingface/:/root/.cache/huggingface/ \
--rm -it --ulimit memlock=-1 --ulimit stack=67108864 \
--gpus=all --ipc=host --network host \
nvcr.io/nvidia/tensorrt-llm/release:spark-single-gpu-dev \
$DOCKER_IMAGE \
bash -c '
export TIKTOKEN_ENCODINGS_BASE="/tmp/harmony-reqs" && \
mkdir -p $TIKTOKEN_ENCODINGS_BASE && \
@ -246,7 +234,7 @@ docker run \
-v $HOME/.cache/huggingface/:/root/.cache/huggingface/ \
--rm -it --ulimit memlock=-1 --ulimit stack=67108864 \
--gpus=all --ipc=host --network host \
nvcr.io/nvidia/tensorrt-llm/release:spark-single-gpu-dev \
$DOCKER_IMAGE \
bash -c '
export TIKTOKEN_ENCODINGS_BASE="/tmp/harmony-reqs" && \
mkdir -p $TIKTOKEN_ENCODINGS_BASE && \
@ -259,33 +247,13 @@ docker run \
--max_tokens 64
'
```
### Step 7. Validate setup with quickstart_multimodal
## Step 7. Validate setup with quickstart_multimodal
**VLM quickstart example**
This demonstrates vision-language model capabilities by running inference with image understanding. The example uses multimodal inputs to validate both text and vision processing pipelines.
#### Qwen2.5-VL-7B-Instruct
```bash
export MODEL_HANDLE="nvidia/Qwen2.5-VL-7B-Instruct-FP4"
docker run \
-e MODEL_HANDLE=$MODEL_HANDLE \
-e HF_TOKEN=$HF_TOKEN \
-v $HOME/.cache/huggingface/:/root/.cache/huggingface/ \
--rm -it --ulimit memlock=-1 --ulimit stack=67108864 \
--gpus=all --ipc=host --network host \
nvcr.io/nvidia/tensorrt-llm/release:spark-single-gpu-dev \
bash -c '
python3 examples/llm-api/quickstart_multimodal.py \
--model_dir $MODEL_HANDLE \
--modality image \
--media "https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/seashore.png" \
--prompt "What is happening in this image?" \
'
```
#### Phi-4-multimodal-instruct
This model requires LoRA (Low-Rank Adaptation) configuration as it uses parameter-efficient fine-tuning. The `--load_lora` flag enables loading the LoRA weights that adapt the base model for multimodal instruction following.
@ -298,7 +266,7 @@ docker run \
-v $HOME/.cache/huggingface/:/root/.cache/huggingface/ \
--rm -it --ulimit memlock=-1 --ulimit stack=67108864 \
--gpus=all --ipc=host --network host \
nvcr.io/nvidia/tensorrt-llm/release:spark-single-gpu-dev \
$DOCKER_IMAGE \
bash -c '
python3 examples/llm-api/quickstart_multimodal.py \
--model_type phi4mm \
@ -318,7 +286,7 @@ docker run \
sudo sh -c 'sync; echo 3 > /proc/sys/vm/drop_caches'
```
### Step 8. Serve LLM with OpenAI-compatible API
## Step 8. Serve LLM with OpenAI-compatible API
Serve with OpenAI-compatible API via trtllm-serve:
@ -330,7 +298,7 @@ docker run --name trtllm_llm_server --rm -it --gpus all --ipc host --network hos
-e HF_TOKEN=$HF_TOKEN \
-e MODEL_HANDLE="$MODEL_HANDLE" \
-v $HOME/.cache/huggingface/:/root/.cache/huggingface/ \
nvcr.io/nvidia/tensorrt-llm/release:spark-single-gpu-dev \
$DOCKER_IMAGE \
bash -c '
hf download $MODEL_HANDLE && \
cat > /tmp/extra-llm-api-config.yml <<EOF
@ -358,7 +326,7 @@ docker run --name trtllm_llm_server --rm -it --gpus all --ipc host --network hos
-e HF_TOKEN=$HF_TOKEN \
-e MODEL_HANDLE="$MODEL_HANDLE" \
-v $HOME/.cache/huggingface/:/root/.cache/huggingface/ \
nvcr.io/nvidia/tensorrt-llm/release:spark-single-gpu-dev \
$DOCKER_IMAGE \
bash -c '
export TIKTOKEN_ENCODINGS_BASE="/tmp/harmony-reqs" && \
mkdir -p $TIKTOKEN_ENCODINGS_BASE && \
@ -394,7 +362,7 @@ curl -s http://localhost:8355/v1/chat/completions \
}'
```
### Step 10. Cleanup and rollback
## Step 9. Cleanup and rollback
Remove downloaded models and containers to free up space when testing is complete.
@ -408,7 +376,7 @@ rm -rf $HOME/.cache/huggingface/
## Clean up Docker images
docker image prune -f
docker rmi nvcr.io/nvidia/tensorrt-llm/release:spark-single-gpu-dev
docker rmi $DOCKER_IMAGE
```
## Run on two Sparks
@ -490,7 +458,7 @@ docker run -d --rm \
-e OMPI_ALLOW_RUN_AS_ROOT_CONFIRM="1" \
-v ~/.cache/huggingface/:/root/.cache/huggingface/ \
-v ~/.ssh:/tmp/.ssh:ro \
nvcr.io/nvidia/tensorrt-llm/release:1.0.0rc3 \
nvcr.io/nvidia/tensorrt-llm/release:1.2.0rc6 \
sh -c "curl https://raw.githubusercontent.com/NVIDIA/dgx-spark-playbooks/refs/heads/main/nvidia/trt-llm/assets/trtllm-mn-entrypoint.sh | sh"
```
@ -509,7 +477,7 @@ You should see output similar to:
```
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
abc123def456 nvcr.io/nvidia/tensorrt-llm/release:1.0.0rc3 "sh -c 'curl https:…" 10 seconds ago Up 8 seconds trtllm-multinode
abc123def456 nvcr.io/nvidia/tensorrt-llm/release:1.2.0rc6 "sh -c 'curl https:…" 10 seconds ago Up 8 seconds trtllm-multinode
```
### Step 6. Copy hostfile to primary container
@ -554,7 +522,7 @@ export HF_TOKEN=<your-huggingface-token>
docker exec \
-e MODEL="nvidia/Qwen3-235B-A22B-FP4" \
-e HF_TOKEN=$HF_TOKEN \
-it $TRTLLM_MN_CONTAINER bash -c 'mpirun -x HF_TOKEN bash -c "huggingface-cli download $MODEL"'
-it $TRTLLM_MN_CONTAINER bash -c 'mpirun -x HF_TOKEN bash -c "hf download $MODEL"'
```
### Step 10. Serve the model

View File

@ -47,15 +47,45 @@ support for ARM64.
- Network access to download packages and container images
## Model Support Matrix
The following models are supported with vLLM on Spark. All listed models are available and ready to use:
| Model | Quantization | Support Status | HF Handle |
|-------|-------------|----------------|-----------|
| **GPT-OSS-20B** | MXFP4 | ✅ | `openai/gpt-oss-20b` |
| **GPT-OSS-120B** | MXFP4 | ✅ | `openai/gpt-oss-120b` |
| **Llama-3.1-8B-Instruct** | FP8 | ✅ | `nvidia/Llama-3.1-8B-Instruct-FP8` |
| **Llama-3.1-8B-Instruct** | NVFP4 | ✅ | `nvidia/Llama-3.1-8B-Instruct-FP4` |
| **Llama-3.3-70B-Instruct** | NVFP4 | ✅ | `nvidia/Llama-3.3-70B-Instruct-FP4` |
| **Qwen3-8B** | FP8 | ✅ | `nvidia/Qwen3-8B-FP8` |
| **Qwen3-8B** | NVFP4 | ✅ | `nvidia/Qwen3-8B-FP4` |
| **Qwen3-14B** | FP8 | ✅ | `nvidia/Qwen3-14B-FP8` |
| **Qwen3-14B** | NVFP4 | ✅ | `nvidia/Qwen3-14B-FP4` |
| **Qwen3-32B** | NVFP4 | ✅ | `nvidia/Qwen3-32B-FP4` |
| **Qwen2.5-VL-7B-Instruct** | NVFP4 | ✅ | `nvidia/Qwen2.5-VL-7B-Instruct-FP4` |
| **Phi-4-multimodal-instruct** | FP8 | ✅ | `nvidia/Phi-4-multimodal-instruct-FP8` |
| **Phi-4-multimodal-instruct** | NVFP4 | ✅ | `nvidia/Phi-4-multimodal-instruct-FP4` |
| **Phi-4-reasoning-plus** | FP8 | ✅ | `nvidia/Phi-4-reasoning-plus-FP8` |
| **Phi-4-reasoning-plus** | NVFP4 | ✅ | `nvidia/Phi-4-reasoning-plus-FP4` |
> [!NOTE]
> The Phi-4-multimodal-instruct models require `--trust-remote-code` when launching vLLM.
> [!NOTE]
> You can use the NVFP4 Quantization documentation to generate your own NVFP4-quantized checkpoints for your favorite models. This enables you to take advantage of the performance and memory benefits of NVFP4 quantization even for models not already published by NVIDIA.
Reminder: not all model architectures are supported for NVFP4 quantization.
## Time & risk
* **Duration:** 30 minutes for Docker approach
* **Risks:** Container registry access requires internal credentials
* **Rollback:** Container approach is non-destructive.
* **Last Updated:** 12/22/2025
* Upgrade vLLM container to latest version nvcr.io/nvidia/vllm:25.11-py3
* Improve cluster setup instructions for Run on two Sparks
* Add docker container permission setup instructioins
* **Last Updated:** 01/02/2026
* Add supported Model Matrix (25.11-py3)
* Improve cluster setup instructions
## Instructions
@ -64,7 +94,6 @@ support for ARM64.
To easily manage containers without sudo, you must be in the `docker` group. If you choose to skip this step, you will need to run Docker commands with sudo.
Open a new terminal and test Docker access. In the terminal, run:
```bash
docker ps
```
@ -78,9 +107,15 @@ newgrp docker
## Step 2. Pull vLLM container image
Find the latest container build from https://catalog.ngc.nvidia.com/orgs/nvidia/containers/vllm?version=25.11-py3
```
docker pull nvcr.io/nvidia/vllm:25.11-py3
Find the latest container build from https://catalog.ngc.nvidia.com/orgs/nvidia/containers/vllm
```bash
export LATEST_VLLM_VERSION=<latest_container_version>
## example
## export LATEST_VLLM_VERSION=25.11-py3
docker pull nvcr.io/nvidia/vllm:${LATEST_VLLM_VERSION}
```
## Step 3. Test vLLM in container
@ -89,7 +124,7 @@ Launch the container and start vLLM server with a test model to verify basic fun
```bash
docker run -it --gpus all -p 8000:8000 \
nvcr.io/nvidia/vllm:25.11-py3 \
nvcr.io/nvidia/vllm:${LATEST_VLLM_VERSION} \
vllm serve "Qwen/Qwen2.5-Math-1.5B-Instruct"
```
@ -117,7 +152,7 @@ Expected response should contain `"content": "204"` or similar mathematical calc
For container approach (non-destructive):
```bash
docker rm $(docker ps -aq --filter ancestor=nvcr.io/nvidia/vllm:25.11-py3)
docker rm $(docker ps -aq --filter ancestor=nvcr.io/nvidia/vllm:${LATEST_VLLM_VERSION})
docker rmi nvcr.io/nvidia/vllm
```