mirror of
https://github.com/NVIDIA/dgx-spark-playbooks.git
synced 2026-04-22 18:13:52 +00:00
chore: Regenerate all playbooks
This commit is contained in:
parent
a0d99066db
commit
a5431dd77a
@ -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
190
nvidia/isaac/README.md
Normal 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 |
|
||||
393
nvidia/live-vlm-webui/README.md
Normal file
393
nvidia/live-vlm-webui/README.md
Normal 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 |
|
||||
222
nvidia/portfolio-optimization/README.md
Normal file
222
nvidia/portfolio-optimization/README.md
Normal 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 risk–return 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).
|
||||
118
nvidia/portfolio-optimization/assets/README.md
Normal file
118
nvidia/portfolio-optimization/assets/README.md
Normal file
@ -0,0 +1,118 @@
|
||||
# **Portfolio Optimization Notebook on DGX Spark**
|
||||
___
|
||||
|
||||
## **Overview**
|
||||
___
|
||||
<br>
|
||||
|
||||

|
||||
|
||||
**[`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.
|
||||
|
||||

|
||||
<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 risk–return 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)
|
||||
|
||||
BIN
nvidia/portfolio-optimization/assets/assets/arch_diagram.png
Normal file
BIN
nvidia/portfolio-optimization/assets/assets/arch_diagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
BIN
nvidia/portfolio-optimization/assets/assets/cvar.png
Normal file
BIN
nvidia/portfolio-optimization/assets/assets/cvar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
2774
nvidia/portfolio-optimization/assets/cvar_basic.ipynb
Normal file
2774
nvidia/portfolio-optimization/assets/cvar_basic.ipynb
Normal file
File diff suppressed because one or more lines are too long
370
nvidia/portfolio-optimization/assets/setup/CONTRIBUTING.md
Normal file
370
nvidia/portfolio-optimization/assets/setup/CONTRIBUTING.md
Normal 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.
|
||||
```
|
||||
201
nvidia/portfolio-optimization/assets/setup/LICENSE
Normal file
201
nvidia/portfolio-optimization/assets/setup/LICENSE
Normal 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.
|
||||
117
nvidia/portfolio-optimization/assets/setup/LICENSE-3rd-party.txt
Normal file
117
nvidia/portfolio-optimization/assets/setup/LICENSE-3rd-party.txt
Normal 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
|
||||
24
nvidia/portfolio-optimization/assets/setup/SECURITY.md
Normal file
24
nvidia/portfolio-optimization/assets/setup/SECURITY.md
Normal 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
|
||||
76
nvidia/portfolio-optimization/assets/setup/pyproject.toml
Normal file
76
nvidia/portfolio-optimization/assets/setup/pyproject.toml
Normal 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",
|
||||
]
|
||||
30
nvidia/portfolio-optimization/assets/setup/setup_playbook.sh
Normal file
30
nvidia/portfolio-optimization/assets/setup/setup_playbook.sh
Normal 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='*'
|
||||
23
nvidia/portfolio-optimization/assets/setup/src/__init__.py
Normal file
23
nvidia/portfolio-optimization/assets/setup/src/__init__.py
Normal 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"]
|
||||
633
nvidia/portfolio-optimization/assets/setup/src/backtest.py
Normal file
633
nvidia/portfolio-optimization/assets/setup/src/backtest.py
Normal 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
|
||||
122
nvidia/portfolio-optimization/assets/setup/src/base_optimizer.py
Normal file
122
nvidia/portfolio-optimization/assets/setup/src/base_optimizer.py
Normal 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
|
||||
59
nvidia/portfolio-optimization/assets/setup/src/cvar_data.py
Normal file
59
nvidia/portfolio-optimization/assets/setup/src/cvar_data.py
Normal 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
|
||||
1183
nvidia/portfolio-optimization/assets/setup/src/cvar_optimizer.py
Normal file
1183
nvidia/portfolio-optimization/assets/setup/src/cvar_optimizer.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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:
|
||||
"""
|
||||
User‑tunable 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.)"
|
||||
)
|
||||
1717
nvidia/portfolio-optimization/assets/setup/src/cvar_utils.py
Normal file
1717
nvidia/portfolio-optimization/assets/setup/src/cvar_utils.py
Normal file
File diff suppressed because it is too large
Load Diff
640
nvidia/portfolio-optimization/assets/setup/src/portfolio.py
Normal file
640
nvidia/portfolio-optimization/assets/setup/src/portfolio.py
Normal 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"]
|
||||
213
nvidia/portfolio-optimization/assets/setup/src/readme.md
Normal file
213
nvidia/portfolio-optimization/assets/setup/src/readme.md
Normal 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.
|
||||
|
||||
908
nvidia/portfolio-optimization/assets/setup/src/rebalance.py
Normal file
908
nvidia/portfolio-optimization/assets/setup/src/rebalance.py
Normal 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()
|
||||
@ -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
|
||||
534
nvidia/portfolio-optimization/assets/setup/src/utils.py
Normal file
534
nvidia/portfolio-optimization/assets/setup/src/utils.py
Normal 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)
|
||||
@ -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
|
||||
4351
nvidia/portfolio-optimization/assets/setup/uv.lock
generated
Normal file
4351
nvidia/portfolio-optimization/assets/setup/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
183
nvidia/single-cell/README.md
Normal file
183
nvidia/single-cell/README.md
Normal 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).
|
||||
116
nvidia/single-cell/assets/README.md
Normal file
116
nvidia/single-cell/assets/README.md
Normal 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!
|
||||
|
||||

|
||||
|
||||
|
||||
# <div align="left"><img src="https://rapids.ai/assets/images/rapids_logo.png" width="90px"/> <div align="left"><img src="https://canada1.discourse-cdn.com/flex035/uploads/forum11/original/1X/dfb6d71c9b8deb73aa10aa9bc47a0f8948d5304b.png" width="90px"/>
|
||||
<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!
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
|
||||
BIN
nvidia/single-cell/assets/assets/rsc.png
Normal file
BIN
nvidia/single-cell/assets/assets/rsc.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 361 KiB |
BIN
nvidia/single-cell/assets/assets/scdiagram.png
Normal file
BIN
nvidia/single-cell/assets/assets/scdiagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
1478
nvidia/single-cell/assets/scRNA_analysis_preprocessing.ipynb
Normal file
1478
nvidia/single-cell/assets/scRNA_analysis_preprocessing.ipynb
Normal file
File diff suppressed because one or more lines are too long
42
nvidia/single-cell/assets/setup/requirements.txt
Normal file
42
nvidia/single-cell/assets/setup/requirements.txt
Normal 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
|
||||
16
nvidia/single-cell/assets/setup/setup_playbook.sh
Normal file
16
nvidia/single-cell/assets/setup/setup_playbook.sh
Normal 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='*'
|
||||
9
nvidia/single-cell/assets/setup/start_playbook.sh
Normal file
9
nvidia/single-cell/assets/setup/start_playbook.sh
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user