diff --git a/applications/equitypricemodel/pyproject.toml b/applications/equitypricemodel/pyproject.toml index 97c2f9f89..846541f4d 100644 --- a/applications/equitypricemodel/pyproject.toml +++ b/applications/equitypricemodel/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ ] [dependency-groups] -dev = ["boto3-stubs[s3]>=1.38.0"] +dev = ["boto3-stubs[s3,ssm]>=1.38.0"] [tool.uv.sources] internal = { workspace = true } diff --git a/applications/equitypricemodel/src/equitypricemodel/server.py b/applications/equitypricemodel/src/equitypricemodel/server.py index ccfa27a85..2e8eab7c8 100644 --- a/applications/equitypricemodel/src/equitypricemodel/server.py +++ b/applications/equitypricemodel/src/equitypricemodel/server.py @@ -59,6 +59,7 @@ logger = structlog.get_logger() DATAMANAGER_BASE_URL = os.getenv("FUND_DATAMANAGER_BASE_URL", "http://datamanager:8080") +MODEL_VERSION_SSM_PARAMETER = "/fund/equitypricemodel/model_version" def find_latest_artifact_key( @@ -156,6 +157,49 @@ def _safe_tar_filter( temp_path.unlink(missing_ok=True) +def _resolve_artifact_key( + s3_client: "S3Client", + bucket: str, + artifact_path: str, +) -> str: + """Resolve the S3 artifact key using SSM version pinning or latest artifact.""" + normalized_artifact_prefix = artifact_path.rstrip("/") + "/" + model_version = "" + try: + ssm_client = boto3.client("ssm") + response = ssm_client.get_parameter(Name=MODEL_VERSION_SSM_PARAMETER) + model_version = response["Parameter"]["Value"].strip().strip("/") + if model_version and model_version != "latest": + logger.info( + "Resolved artifact key from pinned model version", + version=model_version, + ) + else: + model_version = "" + except ClientError as error: + error_code = error.response["Error"]["Code"] + if error_code == "ParameterNotFound": + logger.info( + "SSM parameter not found, falling back to latest model artifact", + parameter=MODEL_VERSION_SSM_PARAMETER, + ) + else: + logger.exception( + "SSM parameter read failed", + parameter=MODEL_VERSION_SSM_PARAMETER, + error_code=error_code, + ) + + if model_version: + return f"{normalized_artifact_prefix}{model_version}/output/model.tar.gz" + + return find_latest_artifact_key( + s3_client=s3_client, + bucket=bucket, + prefix=normalized_artifact_prefix, + ) + + @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: """Load model artifacts from S3 at startup.""" @@ -175,10 +219,10 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: if artifact_path.endswith(".tar.gz"): artifact_key = artifact_path else: - artifact_key = find_latest_artifact_key( + artifact_key = _resolve_artifact_key( s3_client=s3_client, bucket=bucket, - prefix=artifact_path, + artifact_path=artifact_path, ) download_and_extract_artifacts( diff --git a/applications/equitypricemodel/src/equitypricemodel/tide_data.py b/applications/equitypricemodel/src/equitypricemodel/tide_data.py index 1432754f7..50127a8cc 100644 --- a/applications/equitypricemodel/src/equitypricemodel/tide_data.py +++ b/applications/equitypricemodel/src/equitypricemodel/tide_data.py @@ -317,12 +317,9 @@ def _get_prediction_data( def get_dimensions(self) -> dict[str, int]: return { - "encoder_categorical_features": len(self.categorical_columns), - "encoder_continuous_features": len(self.continuous_columns), - "decoder_categorical_features": len(self.categorical_columns), - "decoder_continuous_features": 0, # not using decoder_continuous_features for now # noqa: E501 + "past_continuous_features": len(self.continuous_columns), + "past_categorical_features": len(self.categorical_columns), "static_categorical_features": len(self.static_categorical_columns), - "static_continuous_features": 0, # not using static_continuous_features for now # noqa: E501 } def get_batches( # noqa: C901 @@ -417,10 +414,10 @@ def get_batches( # noqa: C901 # Use numpy slicing (much faster than DataFrame slicing) for i in window_indices: sample = { - "encoder_categorical": cat_array[i : i + input_length].copy(), - "encoder_continuous": cont_array[i : i + input_length].copy(), - "decoder_categorical": cat_array[ - i + input_length : i + input_length + output_length + "past_continuous": cont_array[i : i + input_length].copy(), + # Calendar features are known for both lookback and forecast windows + "past_categorical": cat_array[ + i : i + input_length + output_length ].copy(), "static_categorical": static_array.copy(), } @@ -441,14 +438,11 @@ def get_batches( # noqa: C901 batch_samples = samples[i : i + batch_size] batch = { - "encoder_categorical_features": Tensor( - np.stack([s["encoder_categorical"] for s in batch_samples]) - ), - "encoder_continuous_features": Tensor( - np.stack([s["encoder_continuous"] for s in batch_samples]) + "past_continuous_features": Tensor( + np.stack([s["past_continuous"] for s in batch_samples]) ), - "decoder_categorical_features": Tensor( - np.stack([s["decoder_categorical"] for s in batch_samples]) + "past_categorical_features": Tensor( + np.stack([s["past_categorical"] for s in batch_samples]) ), "static_categorical_features": Tensor( np.stack([s["static_categorical"] for s in batch_samples]) diff --git a/applications/equitypricemodel/src/equitypricemodel/tide_model.py b/applications/equitypricemodel/src/equitypricemodel/tide_model.py index 5be29211b..562763172 100644 --- a/applications/equitypricemodel/src/equitypricemodel/tide_model.py +++ b/applications/equitypricemodel/src/equitypricemodel/tide_model.py @@ -1,6 +1,7 @@ import json import os import random +from pathlib import Path from typing import cast import numpy as np @@ -197,10 +198,11 @@ def forward(self, x: Tensor) -> Tensor: return predictions_first.stack(*predictions_rest, dim=1) def _validate_batch(self, batch: dict[str, Tensor], _batch_idx: int) -> dict: - """Check a batch for NaN/Inf values and return statistics.""" + """Check a batch for NaN/Inf values, shape consistency, rank, and dtype.""" + numpy_cache = {key: tensor.numpy() for key, tensor in batch.items()} + issues = {} - for key, tensor in batch.items(): - data = tensor.numpy() + for key, data in numpy_cache.items(): nan_count = int(np.isnan(data).sum()) inf_count = int(np.isinf(data).sum()) if nan_count > 0 or inf_count > 0: @@ -210,6 +212,34 @@ def _validate_batch(self, batch: dict[str, Tensor], _batch_idx: int) -> dict: "total_elements": data.size, "nan_pct": f"{(nan_count / data.size) * 100:.2f}%", } + + feature_keys = [k for k in batch if k != "targets"] + + batch_sizes = {k: numpy_cache[k].shape[0] for k in feature_keys} + if len(set(batch_sizes.values())) > 1: + issues["shape_mismatch"] = {"batch_sizes": batch_sizes} + + for key in feature_keys: + data = numpy_cache[key] + if data.ndim != 3: # noqa: PLR2004 + issues.setdefault("rank_errors", {})[key] = { + "ndim": data.ndim, + "expected": 3, + } + + for key in feature_keys: + data = numpy_cache[key] + if "continuous" in key and data.dtype != np.float32: + issues.setdefault("dtype_errors", {})[key] = { + "dtype": str(data.dtype), + "expected": "float32", + } + if "categorical" in key and not np.issubdtype(data.dtype, np.integer): + issues.setdefault("dtype_errors", {})[key] = { + "dtype": str(data.dtype), + "expected": "integer", + } + return issues def validate_training_data( @@ -261,6 +291,7 @@ def train( # noqa: PLR0913, PLR0912, PLR0915, C901 validation_sample_size: int = 10, early_stopping_patience: int | None = 3, early_stopping_min_delta: float = 0.001, + checkpoint_directory: str | None = None, ) -> list: """Train the TiDE model using quantile loss. @@ -275,10 +306,13 @@ def train( # noqa: PLR0913, PLR0912, PLR0915, C901 Set to False to skip validation if data quality is already guaranteed. validation_sample_size: Number of batches to sample during validation. Larger values provide more thorough validation but take longer. - The first and last batches are always checked. Default is 10. + Default is 10. early_stopping_patience: Stop if no improvement for N epochs (None to disable) early_stopping_min_delta: Minimum improvement to reset patience counter + checkpoint_directory: Directory to save best-loss checkpoints during + training. If provided, the best checkpoint is automatically restored + after training completes. Defaults to None (no checkpointing). Performance Notes: - Data validation runs once before training starts @@ -293,6 +327,10 @@ def train( # noqa: PLR0913, PLR0912, PLR0915, C901 message = "validation_sample_size must be positive" raise ValueError(message) + if not train_batches: + message = "train_batches must not be empty" + raise ValueError(message) + if validate_data: is_valid = self.validate_training_data( train_batches, @@ -311,7 +349,9 @@ def train( # noqa: PLR0913, PLR0912, PLR0915, C901 total_batches = len(train_batches) best_loss = float("inf") + best_saved_loss = float("inf") epochs_without_improvement = 0 + best_checkpoint_saved = False try: for epoch in range(epochs): @@ -376,33 +416,55 @@ def train( # noqa: PLR0913, PLR0912, PLR0915, C901 losses.append(epoch_loss) - if early_stopping_patience is not None: - if epoch_loss < best_loss - early_stopping_min_delta: - best_loss = epoch_loss - epochs_without_improvement = 0 - logger.info( - "New best loss", - best_loss=f"{best_loss:.4f}", - ) - else: - epochs_without_improvement += 1 + if epoch_loss < best_loss - early_stopping_min_delta: + best_loss = epoch_loss + epochs_without_improvement = 0 + logger.info( + "New best loss", + best_loss=f"{best_loss:.4f}", + ) + else: + epochs_without_improvement += 1 + if early_stopping_patience is not None: logger.info( "No improvement", epochs_without_improvement=epochs_without_improvement, patience=early_stopping_patience, ) - if epochs_without_improvement >= early_stopping_patience: - logger.info( - "Early stopping triggered", - epoch=epoch + 1, - best_loss=f"{best_loss:.4f}", - epochs_without_improvement=epochs_without_improvement, - ) - break + if checkpoint_directory is not None and epoch_loss < best_saved_loss: + best_saved_loss = epoch_loss + Path(checkpoint_directory).mkdir(parents=True, exist_ok=True) + safe_save( + get_state_dict(self), + str(Path(checkpoint_directory) / "tide_states.safetensor"), + ) + best_checkpoint_saved = True + + if ( + early_stopping_patience is not None + and epochs_without_improvement >= early_stopping_patience + ): + logger.info( + "Early stopping triggered", + epoch=epoch + 1, + best_loss=f"{best_loss:.4f}", + epochs_without_improvement=epochs_without_improvement, + ) + break finally: Tensor.training = prev_training + if checkpoint_directory is not None and best_checkpoint_saved: + load_state_dict( + self, + safe_load(str(Path(checkpoint_directory) / "tide_states.safetensor")), + ) + logger.info( + "Restored best checkpoint weights", + checkpoint_directory=checkpoint_directory, + ) + return losses def validate(self, validation_batches: list) -> float: @@ -484,20 +546,11 @@ def _combine_input_features( self, inputs: dict[str, Tensor], ) -> tuple[Tensor, Tensor | None, int]: - batch_size = inputs["encoder_continuous_features"].shape[0] + batch_size = inputs["past_continuous_features"].shape[0] - encoder_cont_flat = inputs["encoder_continuous_features"].reshape( - batch_size, -1 - ) - encoder_cat_flat = ( - inputs["encoder_categorical_features"] - .reshape(batch_size, -1) - .cast("float32") - ) - decoder_cat_flat = ( - inputs["decoder_categorical_features"] - .reshape(batch_size, -1) - .cast("float32") + past_cont_flat = inputs["past_continuous_features"].reshape(batch_size, -1) + past_cat_flat = ( + inputs["past_categorical_features"].reshape(batch_size, -1).cast("float32") ) static_cat_flat = ( inputs["static_categorical_features"] @@ -506,13 +559,7 @@ def _combine_input_features( ) return ( - Tensor.cat( - encoder_cont_flat, - encoder_cat_flat, - decoder_cat_flat, - static_cat_flat, - dim=1, - ), + Tensor.cat(past_cont_flat, past_cat_flat, static_cat_flat, dim=1), inputs.get("targets"), int(batch_size), ) diff --git a/applications/equitypricemodel/src/equitypricemodel/trainer.py b/applications/equitypricemodel/src/equitypricemodel/trainer.py index 2aaff6a8a..647d1046b 100644 --- a/applications/equitypricemodel/src/equitypricemodel/trainer.py +++ b/applications/equitypricemodel/src/equitypricemodel/trainer.py @@ -5,7 +5,7 @@ import structlog from equitypricemodel.tide_data import Data from equitypricemodel.tide_model import Model -from tinygrad import Device +from tinygrad.device import Device # Configure structlog for CloudWatch-friendly output structlog.configure( @@ -102,18 +102,15 @@ sample_batch = train_batches[0] -batch_size = sample_batch["encoder_continuous_features"].shape[0] +batch_size = sample_batch["past_continuous_features"].shape[0] logger.info("batch_size_determined", batch_size=batch_size) # calculate each component's flattened size - days * features (e.g. 35 * 7) -encoder_continuous_size = ( - sample_batch["encoder_continuous_features"].reshape(batch_size, -1).shape[1] +past_continuous_size = ( + sample_batch["past_continuous_features"].reshape(batch_size, -1).shape[1] ) -encoder_categorical_size = ( - sample_batch["encoder_categorical_features"].reshape(batch_size, -1).shape[1] -) -decoder_categorical_size = ( - sample_batch["decoder_categorical_features"].reshape(batch_size, -1).shape[1] +past_categorical_size = ( + sample_batch["past_categorical_features"].reshape(batch_size, -1).shape[1] ) static_categorical_size = ( sample_batch["static_categorical_features"].reshape(batch_size, -1).shape[1] @@ -121,10 +118,7 @@ input_size = cast( "int", - encoder_continuous_size - + encoder_categorical_size - + decoder_categorical_size - + static_categorical_size, + past_continuous_size + past_categorical_size + static_categorical_size, ) logger.info("input_size_calculated", input_size=input_size) @@ -146,6 +140,7 @@ train_batches=train_batches, epochs=int(configuration["epoch_count"]), learning_rate=float(configuration["learning_rate"]), + checkpoint_directory=model_output_path, ) logger.info( diff --git a/applications/equitypricemodel/tests/test_server.py b/applications/equitypricemodel/tests/test_server.py new file mode 100644 index 000000000..a73f3e0d4 --- /dev/null +++ b/applications/equitypricemodel/tests/test_server.py @@ -0,0 +1,224 @@ +from unittest.mock import MagicMock, patch + +import pytest +from botocore.exceptions import ClientError +from equitypricemodel.server import ( + MODEL_VERSION_SSM_PARAMETER, + _resolve_artifact_key, +) + + +def _make_ssm_response(value: str) -> dict: + return {"Parameter": {"Value": value}} + + +def _make_client_error(code: str) -> ClientError: + return ClientError( + error_response={"Error": {"Code": code, "Message": code}}, + operation_name="GetParameter", + ) + + +@patch("equitypricemodel.server.find_latest_artifact_key") +@patch("equitypricemodel.server.boto3.client") +def test_resolve_artifact_key_pinned_version( + mock_boto_client: MagicMock, + mock_find_latest: MagicMock, +) -> None: + ssm_mock = MagicMock() + ssm_mock.get_parameter.return_value = _make_ssm_response( + "equitypricemodel-trainer-2026-01-14-15-00-26-204" + ) + mock_boto_client.return_value = ssm_mock + + s3_mock = MagicMock() + result = _resolve_artifact_key( + s3_client=s3_mock, + bucket="my-bucket", + artifact_path="artifacts/", + ) + + expected = ( + "artifacts/equitypricemodel-trainer-2026-01-14-15-00-26-204/output/model.tar.gz" + ) + assert result == expected + mock_find_latest.assert_not_called() + + +@patch("equitypricemodel.server.find_latest_artifact_key") +@patch("equitypricemodel.server.boto3.client") +def test_resolve_artifact_key_pinned_version_normalizes_path( + mock_boto_client: MagicMock, + mock_find_latest: MagicMock, +) -> None: + ssm_mock = MagicMock() + ssm_mock.get_parameter.return_value = _make_ssm_response("some-version") + mock_boto_client.return_value = ssm_mock + + s3_mock = MagicMock() + result = _resolve_artifact_key( + s3_client=s3_mock, + bucket="my-bucket", + artifact_path="artifacts", # no trailing slash + ) + + assert result == "artifacts/some-version/output/model.tar.gz" + mock_find_latest.assert_not_called() + + +@patch("equitypricemodel.server.find_latest_artifact_key") +@patch("equitypricemodel.server.boto3.client") +def test_resolve_artifact_key_sentinel_latest_falls_back( + mock_boto_client: MagicMock, + mock_find_latest: MagicMock, +) -> None: + ssm_mock = MagicMock() + ssm_mock.get_parameter.return_value = _make_ssm_response("latest") + mock_boto_client.return_value = ssm_mock + + mock_find_latest.return_value = "artifacts/newest/output/model.tar.gz" + s3_mock = MagicMock() + + result = _resolve_artifact_key( + s3_client=s3_mock, + bucket="my-bucket", + artifact_path="artifacts/", + ) + + assert result == "artifacts/newest/output/model.tar.gz" + mock_find_latest.assert_called_once_with( + s3_client=s3_mock, + bucket="my-bucket", + prefix="artifacts/", + ) + + +@patch("equitypricemodel.server.find_latest_artifact_key") +@patch("equitypricemodel.server.boto3.client") +def test_resolve_artifact_key_empty_value_falls_back( + mock_boto_client: MagicMock, + mock_find_latest: MagicMock, +) -> None: + ssm_mock = MagicMock() + ssm_mock.get_parameter.return_value = _make_ssm_response("") + mock_boto_client.return_value = ssm_mock + + mock_find_latest.return_value = "artifacts/newest/output/model.tar.gz" + s3_mock = MagicMock() + + result = _resolve_artifact_key( + s3_client=s3_mock, + bucket="my-bucket", + artifact_path="artifacts/", + ) + + assert result == "artifacts/newest/output/model.tar.gz" + mock_find_latest.assert_called_once() + + +@patch("equitypricemodel.server.find_latest_artifact_key") +@patch("equitypricemodel.server.boto3.client") +def test_resolve_artifact_key_ssm_parameter_not_found_falls_back( + mock_boto_client: MagicMock, + mock_find_latest: MagicMock, +) -> None: + ssm_mock = MagicMock() + ssm_mock.get_parameter.side_effect = _make_client_error("ParameterNotFound") + mock_boto_client.return_value = ssm_mock + + mock_find_latest.return_value = "artifacts/newest/output/model.tar.gz" + s3_mock = MagicMock() + + result = _resolve_artifact_key( + s3_client=s3_mock, + bucket="my-bucket", + artifact_path="artifacts/", + ) + + assert result == "artifacts/newest/output/model.tar.gz" + mock_find_latest.assert_called_once() + + +@patch("equitypricemodel.server.find_latest_artifact_key") +@patch("equitypricemodel.server.boto3.client") +def test_resolve_artifact_key_ssm_access_denied_falls_back( + mock_boto_client: MagicMock, + mock_find_latest: MagicMock, +) -> None: + ssm_mock = MagicMock() + ssm_mock.get_parameter.side_effect = _make_client_error("AccessDeniedException") + mock_boto_client.return_value = ssm_mock + + mock_find_latest.return_value = "artifacts/newest/output/model.tar.gz" + s3_mock = MagicMock() + + result = _resolve_artifact_key( + s3_client=s3_mock, + bucket="my-bucket", + artifact_path="artifacts/", + ) + + assert result == "artifacts/newest/output/model.tar.gz" + mock_find_latest.assert_called_once() + + +@patch("equitypricemodel.server.boto3.client") +def test_resolve_artifact_key_reads_correct_ssm_parameter( + mock_boto_client: MagicMock, +) -> None: + ssm_mock = MagicMock() + ssm_mock.get_parameter.return_value = _make_ssm_response("some-version") + mock_boto_client.return_value = ssm_mock + + s3_mock = MagicMock() + _resolve_artifact_key( + s3_client=s3_mock, + bucket="my-bucket", + artifact_path="artifacts/", + ) + + ssm_mock.get_parameter.assert_called_once_with(Name=MODEL_VERSION_SSM_PARAMETER) + + +@pytest.mark.parametrize( + ("version", "artifact_path", "expected_key"), + [ + ( + "v1", + "artifacts/", + "artifacts/v1/output/model.tar.gz", + ), + ( + "/v1", # leading slash stripped + "artifacts/", + "artifacts/v1/output/model.tar.gz", + ), + ( + "v1", + "artifacts", # no trailing slash + "artifacts/v1/output/model.tar.gz", + ), + ], +) +@patch("equitypricemodel.server.find_latest_artifact_key") +@patch("equitypricemodel.server.boto3.client") +def test_resolve_artifact_key_path_normalization( + mock_boto_client: MagicMock, + mock_find_latest: MagicMock, + version: str, + artifact_path: str, + expected_key: str, +) -> None: + ssm_mock = MagicMock() + ssm_mock.get_parameter.return_value = _make_ssm_response(version) + mock_boto_client.return_value = ssm_mock + + s3_mock = MagicMock() + result = _resolve_artifact_key( + s3_client=s3_mock, + bucket="my-bucket", + artifact_path=artifact_path, + ) + + assert result == expected_key + mock_find_latest.assert_not_called() diff --git a/applications/portfoliomanager/src/portfoliomanager/server.py b/applications/portfoliomanager/src/portfoliomanager/server.py index eb878be93..9ac378fd9 100644 --- a/applications/portfoliomanager/src/portfoliomanager/server.py +++ b/applications/portfoliomanager/src/portfoliomanager/server.py @@ -193,7 +193,7 @@ async def create_portfolio() -> Response: # noqa: PLR0911, PLR0912, PLR0915, C9 "reason": "position_not_found", } ) - except Exception as e: # noqa: PERF203 + except Exception as e: logger.exception( "Failed to close position", ticker=close_position["ticker"], diff --git a/infrastructure/parameters.py b/infrastructure/parameters.py index 69011ac83..d009bda4d 100644 --- a/infrastructure/parameters.py +++ b/infrastructure/parameters.py @@ -22,3 +22,17 @@ description="Maximum inter-quartile range for predictions to be considered valid", tags=tags, ) + +# Equity Price Model Configuration +equitypricemodel_model_version = aws.ssm.Parameter( + "ssm_equitypricemodel_model_version", + name="/fund/equitypricemodel/model_version", + type="String", + value="latest", + description=( + "Artifact folder name for the equity price model. " + 'Use "latest" to use the latest available artifact.' + ), + tags=tags, + opts=pulumi.ResourceOptions(retain_on_delete=True), +) diff --git a/maskfile.md b/maskfile.md index 7faa38781..042272c58 100644 --- a/maskfile.md +++ b/maskfile.md @@ -225,6 +225,11 @@ pulumi import --yes --generate-code=false aws:s3/bucketServerSideEncryptionConfi pulumi import --yes --generate-code=false aws:s3/bucketPublicAccessBlock:BucketPublicAccessBlock model_artifacts_bucket_public_access_block "fund-model-artifacts-${RANDOM_SUFFIX}" 2>/dev/null || true pulumi import --yes --generate-code=false aws:s3/bucketVersioning:BucketVersioning model_artifacts_bucket_versioning "fund-model-artifacts-${RANDOM_SUFFIX}" 2>/dev/null || true +SSM_MODEL_VERSION_ARN=$(aws ssm get-parameter --name "/fund/equitypricemodel/model_version" --query "Parameter.ARN" --output text 2>/dev/null || echo "") +if [ -n "$SSM_MODEL_VERSION_ARN" ]; then + pulumi import --yes --generate-code=false aws:ssm/parameter:Parameter ssm_equitypricemodel_model_version "/fund/equitypricemodel/model_version" 2>/dev/null || true +fi + echo "Importing resources complete" pulumi up --diff --yes diff --git a/pyproject.toml b/pyproject.toml index bbc8861b0..c4b5c3a70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,8 @@ +[project] +name = "fund" +version = "0.0.0" +requires-python = "==3.12.10" + [tool.uv.workspace] members = [ "applications/portfoliomanager", @@ -38,6 +43,9 @@ skip_covered = true [tool.coverage.xml] output = ".coverage_output/python.xml" +[tool.ruff] +target-version = "py312" + [tool.ruff.lint] select = [ "A", # flake8 builtins @@ -112,6 +120,3 @@ include = ["infrastructure/**"] invalid-argument-type = "ignore" missing-argument = "ignore" possibly-missing-attribute = "ignore" - -[tool.pyright] -reportMissingImports = "none" diff --git a/uv.lock b/uv.lock index 0a369cf04..3011174ab 100644 --- a/uv.lock +++ b/uv.lock @@ -10,19 +10,13 @@ resolution-markers = [ [manifest] members = [ "equitypricemodel", + "fund", "infrastructure", "internal", "portfoliomanager", "tools", ] -[manifest.dependency-groups] -dev = [ - { name = "behave", specifier = ">=1.2.6" }, - { name = "coverage", specifier = ">=7.8.0" }, - { name = "pytest", specifier = ">=8.3.5" }, -] - [[package]] name = "alpaca-py" version = "0.43.2" @@ -144,6 +138,9 @@ wheels = [ s3 = [ { name = "mypy-boto3-s3" }, ] +ssm = [ + { name = "mypy-boto3-ssm" }, +] [[package]] name = "botocore" @@ -333,7 +330,7 @@ dependencies = [ [package.dev-dependencies] dev = [ - { name = "boto3-stubs", extra = ["s3"] }, + { name = "boto3-stubs", extra = ["s3", "ssm"] }, ] [package.metadata] @@ -352,7 +349,7 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "boto3-stubs", extras = ["s3"], specifier = ">=1.38.0" }] +dev = [{ name = "boto3-stubs", extras = ["s3", "ssm"], specifier = ">=1.38.0" }] [[package]] name = "fastapi" @@ -370,6 +367,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/b4/023e75a2ec3f5440e380df6caf4d28edc0806d007193e6fb0707237886a4/fastapi-0.133.0-py3-none-any.whl", hash = "sha256:0a78878483d60702a1dde864c24ab349a1a53ef4db6b6f74f8cd4a2b2bc67d2f", size = 104787, upload-time = "2026-02-24T09:53:41.404Z" }, ] +[[package]] +name = "fund" +version = "0.0.0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "behave" }, + { name = "coverage" }, + { name = "pytest" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "behave", specifier = ">=1.2.6" }, + { name = "coverage", specifier = ">=7.8.0" }, + { name = "pytest", specifier = ">=8.3.5" }, +] + [[package]] name = "google-pasta" version = "0.2.0" @@ -653,6 +671,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/06/cb6050ecd72f5fa449bac80ad1a4711719367c4f545201317f36e3999784/mypy_boto3_s3-1.42.37-py3-none-any.whl", hash = "sha256:7c118665f3f583dbfde1013ce47908749f9d2a760f28f59ec65732306ee9cec9", size = 83439, upload-time = "2026-01-28T20:51:49.99Z" }, ] +[[package]] +name = "mypy-boto3-ssm" +version = "1.42.54" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/e9/cde8a9fe2bf061e595256e5542f4c803efdcb2f741611bcae9763f2af993/mypy_boto3_ssm-1.42.54.tar.gz", hash = "sha256:f4bc19a08635757808b66ef94a5b52c3729da998587745962626e60606a1be2c", size = 94255, upload-time = "2026-02-20T20:49:58.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/54/58fa9dba05049adbfd5ecbd755a2e730c118d682367d885e8b35e9b9f0cf/mypy_boto3_ssm-1.42.54-py3-none-any.whl", hash = "sha256:dfd70aa5f60be70437b53482fa6e183bafe922598a50fc6c51f6ad3bd70d8c04", size = 95951, upload-time = "2026-02-20T20:49:54.202Z" }, +] + [[package]] name = "mypy-extensions" version = "1.1.0"