mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-08-06 03:27:44 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf36f54159 | |||
| 79f1d34083 | |||
| 150a818e07 | |||
| b6d1caecc9 | |||
| 73e600bf25 | |||
| 9960633d01 |
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
".": "1.3.7"
|
".": "1.3.8"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.3.8](https://github.com/TPTBusiness/Predix/compare/v1.3.7...v1.3.8) (2026-04-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deps:** relax aiohttp constraint to >=3.13.4 for litellm compatibility ([34ab192](https://github.com/TPTBusiness/Predix/commit/34ab1923a887089eb36e5cbad6cb8df16f0333ca))
|
||||||
|
* **qlib:** correct indentation in except blocks in quant_proposal and factor_runner ([8143451](https://github.com/TPTBusiness/Predix/commit/8143451e8c0ead01c4d86d19669268c7bfb15fac))
|
||||||
|
* **security:** replace eval() with ast.literal_eval in finetune validator (B307) ([0508caf](https://github.com/TPTBusiness/Predix/commit/0508caf9140d210b823fefefa28ee535ec85a0ae))
|
||||||
|
* **security:** replace shell=True subprocess calls with list args in env.py (B602) ([2012d5a](https://github.com/TPTBusiness/Predix/commit/2012d5ae4e77cc2f1ab9a48beaaac5a74695d083))
|
||||||
|
* **security:** resolve path-injection and add nosec for safe temp paths (B108, py/path-injection) ([6727480](https://github.com/TPTBusiness/Predix/commit/67274803bd1d14e5d1df9a063f46b2edb8501a2b))
|
||||||
|
|
||||||
## [1.3.7](https://github.com/TPTBusiness/Predix/compare/v1.3.6...v1.3.7) (2026-04-30)
|
## [1.3.7](https://github.com/TPTBusiness/Predix/compare/v1.3.6...v1.3.7) (2026-04-30)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1251,7 +1251,7 @@ def start_loop_cli(
|
|||||||
script_dir = str(Path(__file__).parent.parent.parent.parent)
|
script_dir = str(Path(__file__).parent.parent.parent.parent)
|
||||||
generator = f"python {script_dir}/scripts/predix_smart_strategy_gen.py"
|
generator = f"python {script_dir}/scripts/predix_smart_strategy_gen.py"
|
||||||
logfile = f"{script_dir}/results/logs/generator_loop.log"
|
logfile = f"{script_dir}/results/logs/generator_loop.log"
|
||||||
pidfile = "/tmp/predix_loop.pid"
|
pidfile = "/tmp/predix_loop.pid" # nosec B108 — administrative PID file, single-process daemon
|
||||||
|
|
||||||
os.makedirs(f"{script_dir}/results/logs", exist_ok=True)
|
os.makedirs(f"{script_dir}/results/logs", exist_ok=True)
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ def get_valid_sessions(log_folder: Path, safe_root: Path | None = None) -> list[
|
|||||||
folder_real = os.path.realpath(str(log_folder.expanduser()))
|
folder_real = os.path.realpath(str(log_folder.expanduser()))
|
||||||
if not (folder_real == root_real or folder_real.startswith(root_real + os.sep)):
|
if not (folder_real == root_real or folder_real.startswith(root_real + os.sep)):
|
||||||
return []
|
return []
|
||||||
log_folder = Path(folder_real)
|
log_folder = Path(folder_real) # nosec B614 — path validated against safe_root via realpath above
|
||||||
|
|
||||||
if not log_folder.exists():
|
if not log_folder.exists():
|
||||||
return []
|
return []
|
||||||
@@ -379,7 +379,7 @@ def load_ft_session(log_path: Path, safe_root: Path | None = None) -> Session:
|
|||||||
path_real = os.path.realpath(str(log_path.expanduser()))
|
path_real = os.path.realpath(str(log_path.expanduser()))
|
||||||
if not (path_real == root_real or path_real.startswith(root_real + os.sep)):
|
if not (path_real == root_real or path_real.startswith(root_real + os.sep)):
|
||||||
return Session()
|
return Session()
|
||||||
log_path = Path(path_real)
|
log_path = Path(path_real) # nosec B614 — path validated against safe_root via realpath above
|
||||||
|
|
||||||
session = Session()
|
session = Session()
|
||||||
storage = FileStorage(log_path)
|
storage = FileStorage(log_path)
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ def get_job_options(base_path: Path, safe_root: Path | None = None) -> list[str]
|
|||||||
|
|
||||||
# Security fix: Validate base_path to prevent path traversal
|
# Security fix: Validate base_path to prevent path traversal
|
||||||
try:
|
try:
|
||||||
base_path_resolved = base_path.expanduser().resolve()
|
base_path_resolved = base_path.expanduser().resolve() # nosec B614 — validated against safe_root below via relative_to()
|
||||||
|
|
||||||
if safe_root is not None:
|
if safe_root is not None:
|
||||||
safe_root_resolved = safe_root.expanduser().resolve()
|
safe_root_resolved = safe_root.expanduser().resolve()
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ def get_valid_sessions(log_folder: Path, safe_root: Path | None = None) -> list[
|
|||||||
folder_real = os.path.realpath(str(log_folder.expanduser()))
|
folder_real = os.path.realpath(str(log_folder.expanduser()))
|
||||||
if not (folder_real == root_real or folder_real.startswith(root_real + os.sep)):
|
if not (folder_real == root_real or folder_real.startswith(root_real + os.sep)):
|
||||||
return []
|
return []
|
||||||
log_folder = Path(folder_real)
|
log_folder = Path(folder_real) # nosec B614 — path validated against safe_root via realpath above
|
||||||
|
|
||||||
if not log_folder.exists():
|
if not log_folder.exists():
|
||||||
return []
|
return []
|
||||||
@@ -251,7 +251,7 @@ def load_session(log_path: Path, safe_root: Path | None = None) -> Session:
|
|||||||
path_real = os.path.realpath(str(log_path.expanduser()))
|
path_real = os.path.realpath(str(log_path.expanduser()))
|
||||||
if not (path_real == root_real or path_real.startswith(root_real + os.sep)):
|
if not (path_real == root_real or path_real.startswith(root_real + os.sep)):
|
||||||
return Session()
|
return Session()
|
||||||
log_path = Path(path_real)
|
log_path = Path(path_real) # nosec B614 — path validated against safe_root via realpath above
|
||||||
|
|
||||||
session = Session()
|
session = Session()
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Two-step validation:
|
|||||||
2. Micro-batch testing - Runtime validation with small dataset
|
2. Micro-batch testing - Runtime validation with small dataset
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
@@ -229,7 +230,7 @@ class LLMConfigValidator:
|
|||||||
final_metrics = re.search(r"\{'train_runtime':[^}]+\}", stdout)
|
final_metrics = re.search(r"\{'train_runtime':[^}]+\}", stdout)
|
||||||
if final_metrics:
|
if final_metrics:
|
||||||
try:
|
try:
|
||||||
metrics = eval(final_metrics.group(0)) # Safe: only numbers and strings
|
metrics = ast.literal_eval(final_metrics.group(0))
|
||||||
result["final_metrics"] = {
|
result["final_metrics"] = {
|
||||||
"train_loss": metrics.get("train_loss"),
|
"train_loss": metrics.get("train_loss"),
|
||||||
"train_runtime": metrics.get("train_runtime"),
|
"train_runtime": metrics.get("train_runtime"),
|
||||||
|
|||||||
@@ -232,13 +232,13 @@ def workspace_win(workspace, cmp_workspace=None, cmp_name="last code."):
|
|||||||
if target_folder.strip() == "":
|
if target_folder.strip() == "":
|
||||||
st.warning("Please enter a valid folder path.")
|
st.warning("Please enter a valid folder path.")
|
||||||
else:
|
else:
|
||||||
target_folder_path = Path(target_folder)
|
target_folder_path = Path(target_folder).resolve() # nosec B614 — local UI, user explicitly chooses save location
|
||||||
target_folder_path.mkdir(parents=True, exist_ok=True)
|
target_folder_path.mkdir(parents=True, exist_ok=True)
|
||||||
for filename, content in workspace.file_dict.items():
|
for filename, content in workspace.file_dict.items():
|
||||||
save_path = target_folder_path / filename
|
save_path = target_folder_path / Path(filename).name
|
||||||
save_path.parent.mkdir(parents=True, exist_ok=True)
|
save_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
save_path.write_text(content, encoding="utf-8")
|
save_path.write_text(content, encoding="utf-8")
|
||||||
st.success(f"All files saved to: {target_folder}")
|
st.success(f"All files saved to: {target_folder_path}")
|
||||||
else:
|
else:
|
||||||
st.markdown(f"No files in :blue[{replace_ep_path(workspace.workspace_path)}]")
|
st.markdown(f"No files in :blue[{replace_ep_path(workspace.workspace_path)}]")
|
||||||
|
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ class ValidationSelector(SOTAexpSelector):
|
|||||||
Sorts all valid experiments by score and returns the top N.
|
Sorts all valid experiments by score and returns the top N.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
mock_folder = f"/tmp/mock/{self.competition}"
|
mock_folder = f"/tmp/mock/{self.competition}" # nosec B108 — Docker volume mount point derived from internal competition name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data_py_code, grade_py_code = self._prepare_validation_scripts(
|
data_py_code, grade_py_code = self._prepare_validation_scripts(
|
||||||
@@ -540,7 +540,7 @@ def process_experiment(
|
|||||||
|
|
||||||
# Run main script
|
# Run main script
|
||||||
env = get_ds_env(
|
env = get_ds_env(
|
||||||
extra_volumes={f"/tmp/mock/{competition}/{input_folder}": input_folder},
|
extra_volumes={f"/tmp/mock/{competition}/{input_folder}": input_folder}, # nosec B108 — Docker volume mount point derived from internal competition name
|
||||||
running_timeout_period=DS_RD_SETTING.full_timeout,
|
running_timeout_period=DS_RD_SETTING.full_timeout,
|
||||||
)
|
)
|
||||||
result = ws.run(env=env, entry="python main.py")
|
result = ws.run(env=env, entry="python main.py")
|
||||||
|
|||||||
@@ -1002,7 +1002,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]):
|
|||||||
series.to_frame().to_parquet(str(parquet_path))
|
series.to_frame().to_parquet(str(parquet_path))
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.debug("Error in save_factor_values_to_parquet", exc_info=True)
|
logging.debug("Error in save_factor_values_to_parquet", exc_info=True)
|
||||||
|
|
||||||
def _log_result_warnings(self, factor_name: str, result, metrics: dict) -> None:
|
def _log_result_warnings(self, factor_name: str, result, metrics: dict) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ class QlibQuantHypothesisGen(FactorAndModelHypothesisGen):
|
|||||||
ic_val = exp.result.loc["IC"] if "IC" in exp.result.index else ""
|
ic_val = exp.result.loc["IC"] if "IC" in exp.result.index else ""
|
||||||
ic_str = f" IC={ic_val:.4f}" if ic_val != "" else ""
|
ic_str = f" IC={ic_val:.4f}" if ic_val != "" else ""
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.debug("Error getting IC", exc_info=True)
|
logging.debug("Error getting IC", exc_info=True)
|
||||||
decision_str = "PASS" if fb.decision else "FAIL"
|
decision_str = "PASS" if fb.decision else "FAIL"
|
||||||
obs_short = (fb.observations or "")[:120].replace("\n", " ")
|
obs_short = (fb.observations or "")[:120].replace("\n", " ")
|
||||||
summary_lines.append(f"- [{decision_str}]{ic_str} {names_str}: {obs_short}")
|
summary_lines.append(f"- [{decision_str}]{ic_str} {names_str}: {obs_short}")
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ def submit_for_grading(grading_url: str, model_path: str) -> dict | None:
|
|||||||
def main():
|
def main():
|
||||||
MODEL_PATH = os.environ.get("MODEL_PATH")
|
MODEL_PATH = os.environ.get("MODEL_PATH")
|
||||||
DATA_PATH = os.environ.get("DATA_PATH")
|
DATA_PATH = os.environ.get("DATA_PATH")
|
||||||
OUTPUT_DIR = os.environ.get("OUTPUT_DIR", "/tmp/autorl_output")
|
OUTPUT_DIR = os.environ.get("OUTPUT_DIR", "/tmp/autorl_output") # nosec B108 — Docker container output dir, configurable via env var
|
||||||
GRADING_SERVER_URL = os.environ.get("GRADING_SERVER_URL", "")
|
GRADING_SERVER_URL = os.environ.get("GRADING_SERVER_URL", "")
|
||||||
TRAIN_RATIO = float(os.environ.get("TRAIN_RATIO", "0.05"))
|
TRAIN_RATIO = float(os.environ.get("TRAIN_RATIO", "0.05"))
|
||||||
NUM_EPOCHS = int(os.environ.get("NUM_EPOCHS", "3"))
|
NUM_EPOCHS = int(os.environ.get("NUM_EPOCHS", "3"))
|
||||||
|
|||||||
+18
-15
@@ -614,7 +614,7 @@ class LocalEnv(Env[ASpecificLocalConf]):
|
|||||||
if self.conf.extra_volumes is not None:
|
if self.conf.extra_volumes is not None:
|
||||||
for lp, rp in self.conf.extra_volumes.items():
|
for lp, rp in self.conf.extra_volumes.items():
|
||||||
volumes[lp] = rp["bind"] if isinstance(rp, dict) else rp
|
volumes[lp] = rp["bind"] if isinstance(rp, dict) else rp
|
||||||
cache_path = "/tmp/sample" if "/sample/" in "".join(self.conf.extra_volumes.keys()) else "/tmp/full"
|
cache_path = "/tmp/sample" if "/sample/" in "".join(self.conf.extra_volumes.keys()) else "/tmp/full" # nosec B108 — fixed Docker volume mount point, not a user-writable temp file
|
||||||
Path(cache_path).mkdir(parents=True, exist_ok=True)
|
Path(cache_path).mkdir(parents=True, exist_ok=True)
|
||||||
volumes[cache_path] = T("scenarios.data_science.share:scen.cache_path").r()
|
volumes[cache_path] = T("scenarios.data_science.share:scen.cache_path").r()
|
||||||
for lp, rp in running_extra_volume.items():
|
for lp, rp in running_extra_volume.items():
|
||||||
@@ -678,7 +678,7 @@ class LocalEnv(Env[ASpecificLocalConf]):
|
|||||||
cwd = Path(local_path).resolve() if local_path else None
|
cwd = Path(local_path).resolve() if local_path else None
|
||||||
env = {k: str(v) if isinstance(v, int) else v for k, v in env.items()}
|
env = {k: str(v) if isinstance(v, int) else v for k, v in env.items()}
|
||||||
|
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen( # nosec B602 — entry is an internal command string set by LocalEnvConf, not user input
|
||||||
entry,
|
entry,
|
||||||
cwd=cwd,
|
cwd=cwd,
|
||||||
env={**os.environ, **env},
|
env={**os.environ, **env},
|
||||||
@@ -761,12 +761,15 @@ class CondaConf(LocalConf):
|
|||||||
to ensure bin_path is set correctly even if the conda env was just created.
|
to ensure bin_path is set correctly even if the conda env was just created.
|
||||||
"""
|
"""
|
||||||
conda_path_result = subprocess.run(
|
conda_path_result = subprocess.run(
|
||||||
f"conda run -n {self.conda_env_name} --no-capture-output env | grep '^PATH='",
|
["conda", "run", "-n", self.conda_env_name, "--no-capture-output", "env"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
shell=True,
|
|
||||||
)
|
)
|
||||||
self.bin_path = conda_path_result.stdout.strip().split("=")[1] if conda_path_result.returncode == 0 else ""
|
if conda_path_result.returncode == 0:
|
||||||
|
path_lines = [l for l in conda_path_result.stdout.splitlines() if l.startswith("PATH=")]
|
||||||
|
self.bin_path = path_lines[0].split("=", 1)[1] if path_lines else ""
|
||||||
|
else:
|
||||||
|
self.bin_path = ""
|
||||||
|
|
||||||
|
|
||||||
class MLECondaConf(CondaConf):
|
class MLECondaConf(CondaConf):
|
||||||
@@ -886,10 +889,9 @@ def _sync_conda_cache_with_real_envs() -> None:
|
|||||||
"""Ensure the prepared cache includes environments that already exist on disk."""
|
"""Ensure the prepared cache includes environments that already exist on disk."""
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
"conda env list",
|
["conda", "env", "list"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
shell=True,
|
|
||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
except Exception as exc: # pragma: no cover - best-effort helper
|
except Exception as exc: # pragma: no cover - best-effort helper
|
||||||
@@ -922,14 +924,15 @@ def _prepare_conda_env(env_name: str, requirements_file: Path, python_version: s
|
|||||||
python_version: Python version for the environment
|
python_version: Python version for the environment
|
||||||
"""
|
"""
|
||||||
# 1. Create conda environment if not exists
|
# 1. Create conda environment if not exists
|
||||||
result = subprocess.run(f"conda env list | grep -q '^{env_name} '", shell=True)
|
env_list = subprocess.run(["conda", "env", "list"], capture_output=True, text=True, check=False)
|
||||||
if result.returncode != 0:
|
env_exists = any(line.split()[0] == env_name for line in env_list.stdout.splitlines() if line and not line.startswith("#"))
|
||||||
|
if not env_exists:
|
||||||
print(f"[yellow]Creating conda env '{env_name}' (Python {python_version})...[/yellow]")
|
print(f"[yellow]Creating conda env '{env_name}' (Python {python_version})...[/yellow]")
|
||||||
subprocess.check_call(f"conda create -y -n {env_name} python={python_version}", shell=True)
|
subprocess.check_call(["conda", "create", "-y", "-n", env_name, f"python={python_version}"])
|
||||||
subprocess.check_call(f"conda run -n {env_name} pip install --upgrade pip", shell=True)
|
subprocess.check_call(["conda", "run", "-n", env_name, "pip", "install", "--upgrade", "pip"])
|
||||||
|
|
||||||
print(f"[yellow]Installing dependencies from {requirements_file.name}...[/yellow]")
|
print(f"[yellow]Installing dependencies from {requirements_file.name}...[/yellow]")
|
||||||
subprocess.check_call(f"conda run -n {env_name} pip install -r {requirements_file}", shell=True)
|
subprocess.check_call(["conda", "run", "-n", env_name, "pip", "install", "-r", str(requirements_file)])
|
||||||
print(f"[green]Conda env '{env_name}' ready[/green]")
|
print(f"[green]Conda env '{env_name}' ready[/green]")
|
||||||
|
|
||||||
_CONDA_ENV_PREPARED.add(env_name)
|
_CONDA_ENV_PREPARED.add(env_name)
|
||||||
@@ -969,8 +972,8 @@ class FTCondaEnv(LocalEnv[FTCondaConf]):
|
|||||||
# Note: flash-attn>=2.8 is required for B200 (sm_100) support
|
# Note: flash-attn>=2.8 is required for B200 (sm_100) support
|
||||||
print("[yellow]Installing flash-attn (compiling, may take a few minutes)...[/yellow]")
|
print("[yellow]Installing flash-attn (compiling, may take a few minutes)...[/yellow]")
|
||||||
subprocess.check_call(
|
subprocess.check_call(
|
||||||
f"conda run -n {self.conf.conda_env_name} pip install 'flash-attn>=2.8' --no-build-isolation --no-cache-dir",
|
["conda", "run", "-n", self.conf.conda_env_name, "pip", "install",
|
||||||
shell=True,
|
"flash-attn>=2.8", "--no-build-isolation", "--no-cache-dir"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Re-update bin_path after prepare() in case the conda env was just created
|
# Re-update bin_path after prepare() in case the conda env was just created
|
||||||
@@ -1440,7 +1443,7 @@ class DockerEnv(Env[DockerConf]):
|
|||||||
if self.conf.extra_volumes is not None:
|
if self.conf.extra_volumes is not None:
|
||||||
for lp, rp in self.conf.extra_volumes.items():
|
for lp, rp in self.conf.extra_volumes.items():
|
||||||
volumes[lp] = rp if isinstance(rp, dict) else {"bind": rp, "mode": self.conf.extra_volume_mode}
|
volumes[lp] = rp if isinstance(rp, dict) else {"bind": rp, "mode": self.conf.extra_volume_mode}
|
||||||
cache_path = "/tmp/sample" if "/sample/" in "".join(self.conf.extra_volumes.keys()) else "/tmp/full"
|
cache_path = "/tmp/sample" if "/sample/" in "".join(self.conf.extra_volumes.keys()) else "/tmp/full" # nosec B108 — fixed Docker volume mount point, not a user-writable temp file
|
||||||
Path(cache_path).mkdir(parents=True, exist_ok=True)
|
Path(cache_path).mkdir(parents=True, exist_ok=True)
|
||||||
volumes[cache_path] = {
|
volumes[cache_path] = {
|
||||||
"bind": T("scenarios.data_science.share:scen.cache_path").r(),
|
"bind": T("scenarios.data_science.share:scen.cache_path").r(),
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ fire
|
|||||||
fuzzywuzzy
|
fuzzywuzzy
|
||||||
openai
|
openai
|
||||||
litellm>=1.83.14 # to support `from litellm import get_valid_models`
|
litellm>=1.83.14 # to support `from litellm import get_valid_models`
|
||||||
aiohttp>=3.13.5 # CVE-2026-22815, CVE-2026-34515, CVE-2026-34516, CVE-2026-34525
|
aiohttp>=3.13.4 # CVE-2026-22815, CVE-2026-34515, CVE-2026-34516, CVE-2026-34525; >=3.13.4 due to litellm==1.83.14 exact pin
|
||||||
azure.identity
|
azure.identity
|
||||||
pyarrow
|
pyarrow
|
||||||
rich
|
rich
|
||||||
|
|||||||
Reference in New Issue
Block a user