Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f681fd9
add lightweight PR CI checks and pre-commit config
jgoldberg-nvidia Apr 7, 2026
6005d95
test commit to trigger pipeline
jgoldberg-nvidia Apr 7, 2026
c4db845
format existing code with black and isort
jgoldberg-nvidia Apr 7, 2026
76330ce
changing to ruff
jgoldberg-nvidia Apr 7, 2026
316ec6c
Unit tests and new pipeline stage for them
jgoldberg-nvidia Apr 7, 2026
666af32
fix unit test paths
jgoldberg-nvidia Apr 7, 2026
4ebbced
fix unit tests breaking
jgoldberg-nvidia Apr 7, 2026
e0036c1
fix unit test solverror
jgoldberg-nvidia Apr 7, 2026
fba3037
More unit tests for eff frontier and rebalancing
jgoldberg-nvidia Apr 7, 2026
af9645a
Reverting the one per line list of tickets
jgoldberg-nvidia Apr 7, 2026
216d894
Revert notebooks to OG and remove linting from them
jgoldberg-nvidia Apr 7, 2026
cf602f3
pre commit version locks
jgoldberg-nvidia Apr 7, 2026
dffdae9
add uv lock --locked check to PR CI to catch uv sync resolution breaks
jgoldberg-nvidia Apr 21, 2026
90cf7e6
matrix install-and-test over Python 3.10 and 3.12 to catch min-versio…
jgoldberg-nvidia Apr 21, 2026
26be0b3
regenerate uv.lock to match pyproject.toml so --locked passes
jgoldberg-nvidia Apr 21, 2026
b95d78d
drop 3.10 from CI matrix; minimum supported is now 3.11
jgoldberg-nvidia Apr 21, 2026
341bd1e
merge origin/fix/python-311-floor-for-cuml-26.4 into add-pr-ci-checks
jgoldberg-nvidia Apr 21, 2026
0bf77bd
make src/ ruff-compliant, fix tests for pydantic settings API
jgoldberg-nvidia Apr 21, 2026
1cadd55
Merge pull request #1 from jgoldberg-nvidia/add-pr-ci-checks
jgoldberg-nvidia Apr 21, 2026
03623d9
Merge remote-tracking branch 'origin/main' into add-pr-ci-checks
jgoldberg-nvidia Apr 21, 2026
11bcbe8
Merge branch 'add-pr-ci-checks' into fork-main
jgoldberg-nvidia Apr 21, 2026
f5afac0
Update src/backtest.py
jgoldberg-nvidia Apr 21, 2026
4ac0d3c
pin uv to 0.11.7 in pr.yaml for CI reproducibility
jgoldberg-nvidia Apr 21, 2026
b916016
fix notebook cuOpt presolve setting: False -> 0 for cuOpt 26.4 compat
jgoldberg-nvidia Apr 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,34 @@ concurrency:
cancel-in-progress: true

jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: uv sync --extra dev

- name: Check formatting
run: uv run ruff format --check src/

- name: Lint
run: uv run ruff check src/

- name: Run tests
run: uv run pytest tests/ -v

run-notebooks:
needs: [lint]
runs-on: arc-runners-org-nvidia-ai-bp-1-gpu
env:
PYTHON_VERSION: 3.12
Expand Down
91 changes: 91 additions & 0 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: PR Checks

on:
pull_request:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
lockfile-check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "0.11.7"

Comment thread
jgoldberg-nvidia marked this conversation as resolved.
- name: Verify lockfile resolves across all extras and Python range
run: uv lock --locked

lint:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "0.11.7"

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: uv sync --extra dev

- name: Check formatting
run: uv run ruff format --check src/

- name: Lint
run: uv run ruff check src/

install-and-test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12"]
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "0.11.7"

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install package
run: uv sync --extra dev --python ${{ matrix.python-version }}

- name: Verify import
run: uv run python -c "import cufolio"

- name: Run tests
run: uv run pytest tests/ -v

pr-builder:
if: always()
needs: [lockfile-check, lint, install-and-test]
runs-on: ubuntu-latest
steps:
- name: Check job results
run: |
if [[ "${{ needs.lockfile-check.result }}" != "success" || "${{ needs.lint.result }}" != "success" || "${{ needs.install-and-test.result }}" != "success" ]]; then
echo "One or more required jobs failed."
exit 1
fi
12 changes: 12 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.9
hooks:
- id: ruff-check
args: [--fix, --config, pyproject.toml]
- id: ruff-format
2 changes: 1 addition & 1 deletion notebooks/cvar_basic.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -1860,7 +1860,7 @@
")\n",
"\n",
"# Solver settings for cuOpt Python API\n",
"cuopt_settings = {\"log_to_console\":True, \"presolve\": False, \"method\": 1}\n",
"cuopt_settings = {\"log_to_console\":True, \"presolve\": 0, \"method\": 1}\n",
"\n",
"# Solve using cuOpt Python API\n",
"cuopt_gpu_results, cuopt_gpu_portfolio = cvar_problem.solve_optimization_problem(solver_settings=cuopt_settings)\n"
Expand Down
2 changes: 1 addition & 1 deletion notebooks/launchable.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -2225,7 +2225,7 @@
")\n",
"\n",
"# Solver settings for cuOpt Python API\n",
"cuopt_settings = {\"log_to_console\":True, \"presolve\": False, \"method\": 1}\n",
"cuopt_settings = {\"log_to_console\":True, \"presolve\": 0, \"method\": 1}\n",
"\n",
"# Solve using cuOpt Python API\n",
"cuopt_gpu_results, cuopt_gpu_portfolio = cvar_problem.solve_optimization_problem(solver_settings=cuopt_settings)\n"
Expand Down
21 changes: 13 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ dependencies = [

[project.optional-dependencies]
dev = [
"black==25.9.0",
"isort",
"flake8",
"ruff==0.15.9",
"pytest>=9.0",
"pre-commit==4.3.0",
]
cuda12 = [
Expand All @@ -30,13 +29,19 @@ cuda13 = [
"cuopt-cu13==26.04.*",
]

[tool.black]
[tool.ruff]
line-length = 88
target-version = ['py311']
target-version = "py311"

[tool.isort]
profile = "black"
line_length = 88
[tool.ruff.lint]
select = ["E", "F", "W", "I"]
ignore = ["E501"]

[tool.ruff.lint.per-file-ignores]
"**/__init__.py" = ["E402"]

[tool.ruff.lint.isort]
combine-as-imports = true

[tool.setuptools]
packages = ["cufolio"]
Expand Down
4 changes: 1 addition & 3 deletions src/base_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,9 +405,7 @@ def solve_optimization_problem(
)

if print_results:
self._print_results(
result_row, portfolio, time_results, min_percentage=1
)
self._print_results(result_row, portfolio, time_results, min_percentage=1)

return result_row, portfolio

Expand Down
55 changes: 38 additions & 17 deletions src/cvar_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@
import numpy as np
import pandas as pd

from . import base_optimizer
from . import cvar_utils
from . import base_optimizer, cvar_utils
from .cvar_parameters import CvarParameters
from .portfolio import Portfolio
from .settings import ApiSettings
Expand Down Expand Up @@ -402,10 +401,10 @@ def _setup_cuopt_problem(self):
INTEGER,
MAXIMIZE,
MINIMIZE,
Problem,
LinearExpression,
Problem,
)

num_assets = self.n_assets
num_scen = len(self.data.p)

Expand Down Expand Up @@ -471,11 +470,11 @@ def _setup_cuopt_problem(self):
for j in range(num_scen):
# Build constraint using LinearExpression
scenario_vars = [variables["t"], variables["u"][j]] + variables["w"]
scenario_coeffs = [1.0, 1.0] + [float(self.data.R[i, j]) for i in range(num_assets)]
scenario_coeffs = [1.0, 1.0] + [
float(self.data.R[i, j]) for i in range(num_assets)
]
scenario_expr = LinearExpression(scenario_vars, scenario_coeffs, 0.0)
problem.addConstraint(
scenario_expr >= 0.0, name=f"cvar_scenario_{j}"
)
problem.addConstraint(scenario_expr >= 0.0, name=f"cvar_scenario_{j}")
timing_dict["cvar_constraints"] = time.time() - start_time

# Add leverage constraint: sum(|w[i]|) <= L_tar
Expand All @@ -494,10 +493,16 @@ def _setup_cuopt_problem(self):

# Then, add decomposition constraints: w[i] = w_pos[i] - w_neg[i]
for i in range(num_assets):
decomp_vars = [variables["w"][i], variables["w_pos"][i], variables["w_neg"][i]]
decomp_vars = [
variables["w"][i],
variables["w_pos"][i],
variables["w_neg"][i],
]
decomp_coeffs = [1.0, -1.0, 1.0] # w - w_pos + w_neg = 0
decomp_expr = LinearExpression(decomp_vars, decomp_coeffs, 0.0)
problem.addConstraint(decomp_expr == 0.0, name=f"weight_decomposition_{i}")
problem.addConstraint(
decomp_expr == 0.0, name=f"weight_decomposition_{i}"
)

# Leverage constraint: sum(w_pos + w_neg) <= L_tar
leverage_vars = variables["w_pos"] + variables["w_neg"]
Expand Down Expand Up @@ -535,7 +540,7 @@ def _setup_cuopt_problem(self):
lower_coeffs = [1.0, -float(self.params.w_min[i])]
lower_expr = LinearExpression(lower_vars, lower_coeffs, 0.0)
problem.addConstraint(lower_expr >= 0.0, name=f"cardinality_lower_{i}")

# Upper bound: w[i] - w_max[i] * y[i] <= 0
upper_vars = [variables["w"][i], variables["y"][i]]
upper_coeffs = [1.0, -float(self.params.w_max[i])]
Expand Down Expand Up @@ -570,7 +575,11 @@ def _setup_cuopt_problem(self):
# Decomposition constraints: w[i] - w_prev[i] = to_pos[i] - to_neg[i]
# Rewritten: w[i] - to_pos[i] + to_neg[i] = w_prev[i]
for i in range(num_assets):
decomp_vars = [variables["w"][i], variables["turnover_pos"][i], variables["turnover_neg"][i]]
decomp_vars = [
variables["w"][i],
variables["turnover_pos"][i],
variables["turnover_neg"][i],
]
decomp_coeffs = [1.0, -1.0, 1.0]
decomp_expr = LinearExpression(decomp_vars, decomp_coeffs, 0.0)
problem.addConstraint(
Expand Down Expand Up @@ -620,15 +629,20 @@ def _setup_cuopt_problem(self):

# Set up objective function
start_time = time.time()

# Build expected return expression using LinearExpression
expected_return_coeffs = [float(self.data.mean[i]) for i in range(num_assets)]
expected_return_expr = LinearExpression(variables["w"], expected_return_coeffs, 0.0)
expected_return_expr = LinearExpression(
variables["w"], expected_return_coeffs, 0.0
)

# Build CVaR expression using LinearExpression
# CVaR = t + sum(p[j] / (1 - alpha)) * u[j]
cvar_vars = [variables["t"]] + variables["u"]
cvar_coeffs = [1.0] + [float(self.data.p[j] / (1 - self.params.confidence)) for j in range(num_scen)]
cvar_coeffs = [1.0] + [
float(self.data.p[j] / (1 - self.params.confidence))
for j in range(num_scen)
]
cvar_expr = LinearExpression(cvar_vars, cvar_coeffs, 0.0)

if self.params.cvar_limit is None:
Expand All @@ -637,8 +651,14 @@ def _setup_cuopt_problem(self):
obj_vars = [variables["t"]] + variables["u"] + variables["w"]
obj_coeffs = (
[float(self.params.risk_aversion)] # t coefficient
+ [float(self.params.risk_aversion) * float(self.data.p[j] / (1 - self.params.confidence)) for j in range(num_scen)] # u coefficients
+ [-float(self.data.mean[i]) for i in range(num_assets)] # w coefficients (negative for return)
+ [
float(self.params.risk_aversion)
* float(self.data.p[j] / (1 - self.params.confidence))
for j in range(num_scen)
] # u coefficients
+ [
-float(self.data.mean[i]) for i in range(num_assets)
] # w coefficients (negative for return)
)
objective_expr = LinearExpression(obj_vars, obj_coeffs, 0.0)
problem.setObjective(objective_expr, sense=MINIMIZE)
Expand Down Expand Up @@ -699,6 +719,7 @@ def _solve_cuopt_problem(self, solver_settings: dict = None):
"""
# Lazy import
from cuopt.linear_programming.solver_settings import SolverSettings

# Configure solver settings
settings = SolverSettings()
if solver_settings:
Expand Down
Loading
Loading